diff --git a/template/LICENSE b/template/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/template/LICENSE @@ -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/template/Makefile.in b/template/Makefile.in new file mode 100644 index 0000000..de1d63a --- /dev/null +++ b/template/Makefile.in @@ -0,0 +1,102 @@ + +#Will be added by configure before this line: +##PREFIX +#PROGRAM +#VERSION +#This is a makefile and needs tabs, not spaces. + +.PHONY: install uninstall clean gitclean resources calfbox + +all: | calfbox + #Our Program + mkdir -p build + cd build && printf "prefix = \"$(PREFIX)\"" > compiledprefix.py + #Only copy the needed files + cp -r "$(PROGRAM)" __main__.py engine qtgui sitepackages template build + #We only need the installed calfbox in local sitepackages. The repo in template is full with build data anyway, don't zip that in. + rm -rf build/template/calfbox + #Clean all pycache in build + cd build && find . -type d -name "__pycache__" -exec rm -r {} + + python3 -m zipapp "build" --output="$(PROGRAM).bin" --python="/usr/bin/env python3" + rm build/compiledprefix.py + +#A mode that just compiles calfbox locally so you can run the whole program standalone +calfbox: + mkdir -p sitepackages + #First build the shared lib. Instead of running make install we create the lib ourselves directly + cd template/calfbox && echo $(shell pwd) + #cd template/calfbox && make && make install DESTDIR=$(shell pwd)/../../sitepackages + cd template/calfbox && make + cp template/calfbox/.libs/libcalfbox.so.0.0.0 sitepackages/"lib$(PROGRAM).so.$(VERSION)" + #We need to be in the directory, make uses subshells which will forget the work-dir in the next line. So here is a trick: + #cd template/calfbox && python3 setup.py build && python3 setup.py install --user --install-lib ../../sitepackages + #The line above is too much for our specialized use-case. We just copy the few files we need manually. + mkdir -p sitepackages/calfbox + cp template/calfbox/py/cbox.py sitepackages/calfbox + cp template/calfbox/py/_cbox2.py sitepackages/calfbox + cp template/calfbox/py/__init__.py sitepackages/calfbox + cp template/calfbox/py/metadata.py sitepackages/calfbox + cp template/calfbox/py/sfzparser.py sitepackages/calfbox + cp template/calfbox/py/nullbox.py sitepackages/calfbox + +clean: + cd template/calfbox && make distclean && rm -rf build + cd template/calfbox && rm -rf .deps/ Makefile.in aclocal.m4 autom4te.cache/ compile config.guess config.h.in config.h.in~ config.sub configure depcomp install-sh ltmain.sh missing + rm -rf build/ + rm -rf sitepackages + rm -f "$(PROGRAM).bin" + rm Makefile + find . -type d -name "__pycache__" -exec rm -r {} + + +#Convenience function for developing, not used for the build or install process +gitclean: + git clean -f -X -d + +#Convenience function for developing, not used for the build or install process +resources: + cd template/documentation && python3 build.py + cd documentation && sh build-documentation.sh + cd qtgui/resources && sh buildresources.sh + +install: + install -D -m 755 $(PROGRAM).bin $(DESTDIR)$(PREFIX)/bin/$(PROGRAM) + + install -D -m 644 documentation/out/* -t $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM) + install -D -m 644 README.md $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM)/README.md + install -D -m 644 LICENSE $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM)/LICENSE + + install -D -m 644 desktop/desktop.desktop $(DESTDIR)$(PREFIX)/share/applications/org.laborejo.$(PROGRAM).desktop + + install -d $(DESTDIR)$(PREFIX)/share/man/man1 + gzip -c documentation/$(PROGRAM).1 > $(DESTDIR)$(PREFIX)/share/man/man1/$(PROGRAM).1.gz + + + #Icons + for size in 16 32 64 128 256 512 ; do \ + install -D -m 644 desktop/images/"$$size"x"$$size".png $(DESTDIR)$(PREFIX)/share/icons/hicolor/"$$size"x"$$size"/apps/$(PROGRAM).png ; \ + done + install -D -m 644 desktop/images/256x256.png $(DESTDIR)$(PREFIX)/share/pixmaps/$(PROGRAM).png + + install -D -m 755 sitepackages/lib$(PROGRAM).so.$(VERSION) -t $(DESTDIR)$(PREFIX)/lib/$(PROGRAM) + + install -d $(DESTDIR)$(PREFIX)/share/$(PROGRAM) + cp -r engine/resources/* $(DESTDIR)$(PREFIX)/share/$(PROGRAM)/ + install -D -m 644 template/engine/resources/metronome/* -t $(DESTDIR)$(PREFIX)/share/$(PROGRAM)/template/metronome + +uninstall: + #Directories + rm -rf $(DESTDIR)$(PREFIX)/share/template/$(PROGRAM) + rm -rf $(DESTDIR)$(PREFIX)/share/doc/$(PROGRAM) + rm -rf $(DESTDIR)$(PREFIX)/share/$(PROGRAM) + rm -rf $(DESTDIR)$(PREFIX)/lib/$(PROGRAM) + + #Files + rm -f $(DESTDIR)$(PREFIX)/bin/$(PROGRAM) + rm -f $(DESTDIR)$(PREFIX)/share/applications/org.laborejo.$(PROGRAM).desktop + rm -f $(DESTDIR)$(PREFIX)/share/man/man1/$(PROGRAM).1.gz + + #Icons + for size in 16 32 64 128 256 512 ; do \ + rm -f $(DESTDIR)$(PREFIX)/share/icons/hicolor/"$$size"x"$$size"/apps/$(PROGRAM).png ; \ + done + rm -f $(DESTDIR)$(PREFIX)/share/pixmaps/$(PROGRAM).png diff --git a/template/README.md b/template/README.md new file mode 100644 index 0000000..d0e5d79 --- /dev/null +++ b/template/README.md @@ -0,0 +1,34 @@ +# File Structure, Description and How to Update the Template + +The principle of this program is that it can be used in a self-contained "all in one directory" version +but also in a compiled version with files all over the system, following the linux file hirarchy. + +For that reason that are some processes that must be done manually and which will create generated +but static files (not at compile or runtime) that are included in git. For example the qt resources +and translation or documentation html files from asciidoctor sources. + + +## Copied files from 3rd party libs that need to be updated manually. +* For Calfbox copy the whole source directory into template/calfbox and delete its .git +* nsmclient.py from pynsm2 into qtgui. + + +# Menu +There is a menu in the example MainWindow but it is emtpy. +Some default menu entries will be added dynamically by the template. + +You can merge template and client menus. But there are some naming conventions you must uphold: + menuFile + menuEdit + menuHelp + menuDebug + +These menuActions are standard and can only be hidden, deactivated or rerouted. But you can't create them on your own in QtDesigner + actionUndo + actionRedo + actionAbout + actionUser_Manual + + + + diff --git a/template/__init__.py b/template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/template/calfbox/.gitignore b/template/calfbox/.gitignore new file mode 100644 index 0000000..36fd692 --- /dev/null +++ b/template/calfbox/.gitignore @@ -0,0 +1,31 @@ +aclocal.m4 +autom4te.cache +config.h.in +config.h.in~ +config.h +configure +compile +depcomp +install-sh +ltmain.sh +missing +*.o +*.lo +.libs/ +.deps/ +libcalfbox.la +libtool +Makefile.in +Makefile +config.log +config.status +stamp-h1 +calfbox +calfbox_tests + +__pycache__/ +*.py[cod] +*$py.class +build/ +config.guess +config.sub diff --git a/template/calfbox/API b/template/calfbox/API new file mode 100644 index 0000000..07f9522 --- /dev/null +++ b/template/calfbox/API @@ -0,0 +1,133 @@ +@module >> @chorus, @phaser, ... + +@moduleslot/status() -> + /insert_engine(string engine), + /insert_preset(string preset), + /bypass(int bypassed) +@moduleslot/insert_engine(string engine) +@moduleslot/insert_preset(string engine) +@moduleslot/engine/{add: @module} +@moduleslot/set_bypass(int bypassed) + +@track/add_clip(int pos, int offset, int length, string pattern) + +/master/ +/master/status() -> /sample_rate, /tempo, /timesig, /playing, /pos, /pos_ppqn +/master/tell() -> /playing, /pos, /pos_ppqn +/master/set_tempo(float tempo) +/master/set_timesig(int num, int denom) +/master/play() +/master/stop() +/master/seek_samples(int samples) +/master/seek_ppqn(int ppqn) + +/meter/ +/meter/get_peak() -> /peak(float left, float right) +/meter/get_rms() -> /rms(float left, float right) + +/config/ +/config/sections([string prefix]) -> [/section(string name)] +/config/keys(string section, string ?prefix) -> [/section(string name)] +/config/get(string section, string key) -> /value(string value) +/config/set(string section, string key, string value) +/config/delete(string section, string key) +/config/delete_section(string section) +/config/save(string ?filename) + +/engine/ +/engine/status() -> /scene(object scene) +/engine/render_stereo(int nframes) +/engine/master_effect/{add: @moduleslot} +/engine/new_scene() -> uuid +/engine/new_recorder() -> uuid + +/scene/ +/scene/transpose(int semitones) +/scene/clear() +/scene/load(string scene_name) +/scene/add_layer(int layer_pos, string layer_name) +/scene/add_instrument_layer(int layer_pos, string instrument_name) +/scene/delete_layer(int pos) +/scene/move_layer(int oldpos, int newpos) +/scene/instr/ +/scene/instr//status() -> + /engine(string name), + /aux_offset(int first_aux_output_no), + /outputs(int stereo_output_count) +/scene/instr//output//status() -> + /gain_linear(float gain), + /gain(float gain_dB), + /output(int output_bus), + {add: @moduleslot/status()} +/scene/instr//output//gain(float gain_dB), +/scene/instr//output//output(int output_bus) +/scene/instr//output//{add: @moduleslot} +/scene/instr//aux//status() -> + /gain_linear(float gain), + /gain(float gain_dB), + /bus(string output_bus), + {add: @moduleslot/status()} XXXKF ???? +/scene/instr//aux//gain(float gain_dB) +/scene/instr//aux//bus(string bus) +/scene/instr//aux//{add: @moduleslot} +/scene/layer// +/scene/layer//status() -> + /enable(int), + /instrument_name(string iname), + /instrument_uuid(string uuid), + /consume(int consume), + /ignore_scene_transpose(int ignore), + /disable_aftertouch(int disable), + /transpose(int semitones), + /fixed_note(int note), + /low_note(int note), + /high_note(int note), + /in_channel(int channel), + /out_channel(int channel) +/scene/layer//enable(int) +/scene/layer//instrument_name(string iname) +/scene/layer//consume(int consume) +/scene/layer//ignore_scene_transpose(int ignore) +/scene/layer//disable_aftertouch(int disable) +/scene/layer//transpose(int semitones) +/scene/layer//fixed_note(int note) +/scene/layer//low_note(int note) +/scene/layer//high_note(int note) +/scene/layer//in_channel(int channel) +/scene/layer//out_channel(int channel) +/scene/aux//status +/scene/aux//slot/{add: @module} +/scene/load_aux(string name) +/scene/delete_aux(string name) +/scene/status() -> + /name(string), + /title(string), + /transpose(int semitones), + [/layer(string uuid)], + [/instrument(string instance, string engine)], + [/aux(string name, string uuid)] + +/rt/ +/rt/status() -> /audio_channels(int inputs, int outputs) +/song/ +/song/status() -> [/track(int index, string name, int items)], [/pattern(int index, string name, int length)] +/waves/ +/waves/status() -> /bytes(int bytes), /max_bytes(int max_bytes), /count(int count) +/waves/list() -> [/waveform(int id)] +/waves/info(int id) -> /filename(string), /name(string), /bytes(int) +/on_idle() -> {any} +/send_event_to(string output, int) +/send_event_to(string output, int, int) +/send_event_to(string output, int, int, int) +/play_note(int ch, int note, int velocity) (plays a note with duration=1 on the next buffer) +/play_drum_pattern(string pattern) +/play_drum_track(string track) +/play_blob(blob serialized_pattern, int length_ticks) +/stop_pattern() +/get_pattern() -> /pattern(blob serialized_pattern, int length_ticks) +/print_s(string) +/print_i(int) +/print_f(float) +/new_meter() -> /uuid +/new_recorder(string filename) -> /uuid + diff --git a/template/calfbox/AUTHORS b/template/calfbox/AUTHORS new file mode 100644 index 0000000..7c52993 --- /dev/null +++ b/template/calfbox/AUTHORS @@ -0,0 +1 @@ +Krzysztof Foltman diff --git a/template/calfbox/COPYING b/template/calfbox/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/template/calfbox/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/template/calfbox/ChangeLog b/template/calfbox/ChangeLog new file mode 100644 index 0000000..e69de29 diff --git a/template/calfbox/INSTALL b/template/calfbox/INSTALL new file mode 100644 index 0000000..7d1c323 --- /dev/null +++ b/template/calfbox/INSTALL @@ -0,0 +1,365 @@ +Installation Instructions +************************* + +Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005, +2006, 2007, 2008, 2009 Free Software Foundation, Inc. + + Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without warranty of any kind. + +Basic Installation +================== + + Briefly, the shell commands `./configure; make; make install' should +configure, build, and install this package. The following +more-detailed instructions are generic; see the `README' file for +instructions specific to this package. Some packages provide this +`INSTALL' file but do not implement all of the features documented +below. The lack of an optional feature in a given package is not +necessarily a bug. More recommendations for GNU packages can be found +in *note Makefile Conventions: (standards)Makefile Conventions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, and a +file `config.log' containing compiler output (useful mainly for +debugging `configure'). + + It can also use an optional file (typically called `config.cache' +and enabled with `--cache-file=config.cache' or simply `-C') that saves +the results of its tests to speed up reconfiguring. Caching is +disabled by default to prevent problems with accidental use of stale +cache files. + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If you are using the cache, and at +some point `config.cache' contains results you don't want to keep, you +may remove or edit it. + + The file `configure.ac' (or `configure.in') is used to create +`configure' by a program called `autoconf'. You need `configure.ac' if +you want to change it or regenerate `configure' using a newer version +of `autoconf'. + + The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. + + Running `configure' might take a while. While running, it prints + some messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package, generally using the just-built uninstalled binaries. + + 4. Type `make install' to install the programs and any data files and + documentation. When installing into a prefix owned by root, it is + recommended that the package be configured and built as a regular + user, and only the `make install' phase executed with root + privileges. + + 5. Optionally, type `make installcheck' to repeat any self-tests, but + this time using the binaries in their final installed location. + This target does not install anything. Running this target as a + regular user, particularly if the prior `make install' required + root privileges, verifies that the installation completed + correctly. + + 6. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + + 7. Often, you can also type `make uninstall' to remove the installed + files again. In practice, not all packages have tested that + uninstallation works correctly, even though it is required by the + GNU Coding Standards. + + 8. Some packages, particularly those that use Automake, provide `make + distcheck', which can by used by developers to test that all other + targets like `make install' and `make uninstall' work correctly. + This target is generally not run by end users. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. Run `./configure --help' +for details on some of the pertinent environment variables. + + You can give `configure' initial values for configuration parameters +by setting variables in the command line or in the environment. Here +is an example: + + ./configure CC=c99 CFLAGS=-g LIBS=-lposix + + *Note Defining Variables::, for more details. + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you can use GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. This +is known as a "VPATH" build. + + With a non-GNU `make', it is safer to compile the package for one +architecture at a time in the source code directory. After you have +installed the package for one architecture, use `make distclean' before +reconfiguring for another architecture. + + On MacOS X 10.5 and later systems, you can create libraries and +executables that work on multiple system types--known as "fat" or +"universal" binaries--by specifying multiple `-arch' options to the +compiler but only a single `-arch' option to the preprocessor. Like +this: + + ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ + CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ + CPP="gcc -E" CXXCPP="g++ -E" + + This is not guaranteed to produce working output in all cases, you +may have to build one architecture at a time and combine the results +using the `lipo' tool if you have problems. + +Installation Names +================== + + By default, `make install' installs the package's commands under +`/usr/local/bin', include files under `/usr/local/include', etc. You +can specify an installation prefix other than `/usr/local' by giving +`configure' the option `--prefix=PREFIX', where PREFIX must be an +absolute file name. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +pass the option `--exec-prefix=PREFIX' to `configure', the package uses +PREFIX as the prefix for installing programs and libraries. +Documentation and other data files still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=DIR' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. In general, the +default for these options is expressed in terms of `${prefix}', so that +specifying just `--prefix' will affect all of the other directory +specifications that were not explicitly provided. + + The most portable way to affect installation locations is to pass the +correct locations to `configure'; however, many packages provide one or +both of the following shortcuts of passing variable assignments to the +`make install' command line to change installation locations without +having to reconfigure or recompile. + + The first method involves providing an override variable for each +affected directory. For example, `make install +prefix=/alternate/directory' will choose an alternate location for all +directory configuration variables that were expressed in terms of +`${prefix}'. Any directories that were specified during `configure', +but not in terms of `${prefix}', must each be overridden at install +time for the entire installation to be relocated. The approach of +makefile variable overrides for each directory variable is required by +the GNU Coding Standards, and ideally causes no recompilation. +However, some platforms have known limitations with the semantics of +shared libraries that end up requiring recompilation when using this +method, particularly noticeable in packages that use GNU Libtool. + + The second method involves providing the `DESTDIR' variable. For +example, `make install DESTDIR=/alternate/directory' will prepend +`/alternate/directory' before all installation names. The approach of +`DESTDIR' overrides is not required by the GNU Coding Standards, and +does not work on platforms that have drive letters. On the other hand, +it does better at avoiding recompilation issues, and works well even +when some directory options were not specified in terms of `${prefix}' +at `configure' time. + +Optional Features +================= + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + + Some packages offer the ability to configure how verbose the +execution of `make' will be. For these packages, running `./configure +--enable-silent-rules' sets the default to minimal output, which can be +overridden with `make V=1'; while running `./configure +--disable-silent-rules' sets the default to verbose, which can be +overridden with `make V=0'. + +Particular systems +================== + + On HP-UX, the default C compiler is not ANSI C compatible. If GNU +CC is not installed, it is recommended to use the following options in +order to use an ANSI C compiler: + + ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" + +and if that doesn't work, install pre-built binaries of GCC for HP-UX. + + On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot +parse its `' header file. The option `-nodtk' can be used as +a workaround. If GNU CC is not installed, it is therefore recommended +to try + + ./configure CC="cc" + +and if that doesn't work, try + + ./configure CC="cc -nodtk" + + On Solaris, don't put `/usr/ucb' early in your `PATH'. This +directory contains several dysfunctional programs; working variants of +these programs are available in `/usr/bin'. So, if you need `/usr/ucb' +in your `PATH', put it _after_ `/usr/bin'. + + On Haiku, software installed for all users goes in `/boot/common', +not `/usr/local'. It is recommended to use the following options: + + ./configure --prefix=/boot/common + +Specifying the System Type +========================== + + There may be some features `configure' cannot figure out +automatically, but needs to determine by the type of machine the package +will run on. Usually, assuming the package is built to be run on the +_same_ architectures, `configure' can figure that out, but if it prints +a message saying it cannot guess the machine type, give it the +`--build=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name which has the form: + + CPU-COMPANY-SYSTEM + +where SYSTEM can have one of these forms: + + OS + KERNEL-OS + + See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the machine type. + + If you are _building_ compiler tools for cross-compiling, you should +use the option `--target=TYPE' to select the type of system they will +produce code for. + + If you want to _use_ a cross compiler, that generates code for a +platform different from the build platform, you should specify the +"host" platform (i.e., that on which the generated programs will +eventually be run) with `--host=TYPE'. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Defining Variables +================== + + Variables not defined in a site shell script can be set in the +environment passed to `configure'. However, some packages may run +configure again during the build, and the customized values of these +variables may be lost. In order to avoid this problem, you should set +them in the `configure' command line, using `VAR=value'. For example: + + ./configure CC=/usr/local2/bin/gcc + +causes the specified `gcc' to be used as the C compiler (unless it is +overridden in the site shell script). + +Unfortunately, this technique does not work for `CONFIG_SHELL' due to +an Autoconf bug. Until the bug is fixed you can use this workaround: + + CONFIG_SHELL=/bin/bash /bin/bash ./configure CONFIG_SHELL=/bin/bash + +`configure' Invocation +====================== + + `configure' recognizes the following options to control how it +operates. + +`--help' +`-h' + Print a summary of all of the options to `configure', and exit. + +`--help=short' +`--help=recursive' + Print a summary of the options unique to this package's + `configure', and exit. The `short' variant lists options used + only in the top level, while the `recursive' variant lists options + also present in any nested packages. + +`--version' +`-V' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`--cache-file=FILE' + Enable the cache: use and save the results of the tests in FILE, + traditionally `config.cache'. FILE defaults to `/dev/null' to + disable caching. + +`--config-cache' +`-C' + Alias for `--cache-file=config.cache'. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`--prefix=DIR' + Use DIR as the installation prefix. *note Installation Names:: + for more details, including other options available for fine-tuning + the installation locations. + +`--no-create' +`-n' + Run the configure checks, but stop before creating any output + files. + +`configure' also accepts some other, not widely useful, options. Run +`configure --help' for more details. + diff --git a/template/calfbox/Makefile.am b/template/calfbox/Makefile.am new file mode 100644 index 0000000..32dcce2 --- /dev/null +++ b/template/calfbox/Makefile.am @@ -0,0 +1,152 @@ +AM_CPPFLAGS = -I$(srcdir) -Wall -Wsign-compare -D_GNU_SOURCE + +AM_CFLAGS = $(JACK_DEPS_CFLAGS) $(GLIB_DEPS_CFLAGS) $(FLUIDSYNTH_DEPS_CFLAGS) $(PYTHON_DEPS_CFLAGS) $(LIBSMF_DEPS_CFLAGS) $(LIBSNDFILE_DEPS_CFLAGS) $(LIBUSB_DEPS_CFLAGS) $(ARCH_OPT_CFLAGS) $(NCURSES_DEPS_CFLAGS) + +lib_LTLIBRARIES = libcalfbox.la + +bin_PROGRAMS = calfbox +noinst_PROGRAMS = calfbox_tests + +calfbox_SOURCES = \ + appmenu.c \ + main.c \ + menu.c \ + menuitem.c \ + ui.c + +calfbox_LDADD = libcalfbox.la $(JACK_DEPS_LIBS) $(GLIB_DEPS_LIBS) $(FLUIDSYNTH_DEPS_LIBS) $(PYTHON_DEPS_LIBS) $(LIBSMF_DEPS_LIBS) $(LIBSNDFILE_DEPS_LIBS) $(LIBUSB_DEPS_LIBS) $(NCURSES_DEPS_LIBS) -lpthread -luuid -lm -lrt + +calfbox_tests_SOURCES = \ + tests.c + +calfbox_tests_LDADD = libcalfbox.la $(GLIB_DEPS_LIBS) -lpthread -lm -lrt + +libcalfbox_la_SOURCES = \ + app.c \ + auxbus.c \ + blob.c \ + chorus.c \ + cmd.c \ + compressor.c \ + config-api.c \ + delay.c \ + distortion.c \ + dom.c \ + engine.c \ + eq.c \ + errors.c \ + fbr.c \ + fifo.c \ + fluid.c \ + fuzz.c \ + fxchain.c \ + gate.c \ + hwcfg.c \ + instr.c \ + io.c \ + jackinput.c \ + jackio.c \ + layer.c \ + limiter.c \ + master.c \ + meter.c \ + midi.c \ + mididest.c \ + module.c \ + pattern.c \ + pattern-maker.c \ + phaser.c \ + prefetch_pipe.c \ + recsrc.c \ + reverb.c \ + rt.c \ + sampler.c \ + sampler_channel.c \ + sampler_gen.c \ + sampler_layer.c \ + sampler_nif.c \ + sampler_prevoice.c \ + sampler_prg.c \ + sampler_rll.c \ + sampler_voice.c \ + scene.c \ + scripting.c \ + seq.c \ + seq-adhoc.c \ + sfzloader.c \ + sfzparser.c \ + song.c \ + streamplay.c \ + streamrec.c \ + tarfile.c \ + tonectl.c \ + tonewheel.c \ + track.c \ + usbaudio.c \ + usbio.c \ + usbmidi.c \ + usbprobe.c \ + wavebank.c + +libcalfbox_la_LIBADD = $(JACK_DEPS_LIBS) $(GLIB_DEPS_LIBS) $(FLUIDSYNTH_DEPS_LIBS) $(PYTHON_DEPS_LIBS) $(LIBSMF_DEPS_LIBS) $(LIBSNDFILE_DEPS_LIBS) $(LIBUSB_DEPS_LIBS) -lpthread -luuid -lm -lrt + +if USE_SSE +ARCH_OPT_CFLAGS=-msse -ffast-math +else +if USE_NEON +ARCH_OPT_CFLAGS=-mfloat-abi=hard -mfpu=neon -ffast-math +endif +endif + +noinst_HEADERS = \ + app.h \ + auxbus.h \ + biquad-float.h \ + blob.h \ + cmd.h \ + config-api.h \ + dom.h \ + dspmath.h \ + envelope.h \ + engine.h \ + eq.h \ + errors.h \ + fifo.h \ + hwcfg.h \ + instr.h \ + io.h \ + ioenv.h \ + layer.h \ + master.h \ + menu.h \ + menuitem.h \ + meter.h \ + midi.h \ + mididest.h \ + module.h \ + onepole-int.h \ + onepole-float.h \ + pattern.h \ + pattern-maker.h \ + prefetch_pipe.h \ + recsrc.h \ + rt.h \ + sampler.h \ + sampler_impl.h \ + sampler_layer.h \ + sampler_prg.h \ + scene.h \ + scripting.h \ + seq.h \ + sfzloader.h \ + sfzparser.h \ + song.h \ + stm.h \ + tarfile.h \ + tests.h \ + track.h \ + ui.h \ + usbio_impl.h \ + wavebank.h + +EXTRA_DIST = cboxrc-example diff --git a/template/calfbox/NEWS b/template/calfbox/NEWS new file mode 100644 index 0000000..e69de29 diff --git a/template/calfbox/README b/template/calfbox/README new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/template/calfbox/README @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/template/calfbox/README.md b/template/calfbox/README.md new file mode 100644 index 0000000..de03056 --- /dev/null +++ b/template/calfbox/README.md @@ -0,0 +1,63 @@ +# Calfbox + +Website: https://github.com/kfoltman/calfbox + +Calfbox, the "open source musical instrument", offers assorted music-related code. + +Originally intended as a standalone instrument for Linux and embedded devices (USB TV Sticks) +it can be used as Python module as well. + +# Packaging +If you are a packager and want to create a binary package for your distribution please package only the python module. +The binary executable is not maintained and untested at the moment. It should not be used by anyone. + + +# Calfbox as Python Module +Calfbox can be used as a Python module that can be imported to create short scripts or +full fledged programs ( https://www.laborejo.org/software ). + +Most notably it features a midi sequencer and an audio sampler (for sfz files and sf2 via fluidsynth). + +## Building + +A convenience script `cleanpythonbuild.py` has been supplied to quickly build and install the cbox python module. + +``` +make clean +rm build -rf +sh autogen.sh +./configure +make +python3 setup.py build +sudo python3 setup.py install +``` + +## How to write programs with cbox +You can find several `.py` files in the main directory, such as `sampler_api_example.py` or +`song_api_example.py`. + +Also there is a directory `/experiments` which contains a small example framework. + + +# Using Calfbox as standalone instrument + +Using Calfbox as standalone instrument requires a .cfg config file. + +This part of the program is currently unmaintained and untested. Please do not use it. + +# License + +This code 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 . + +For the full license see the file COPYING diff --git a/template/calfbox/adhoc_example.py b/template/calfbox/adhoc_example.py new file mode 100644 index 0000000..04a96ff --- /dev/null +++ b/template/calfbox/adhoc_example.py @@ -0,0 +1,47 @@ +import os +import sys +import struct +import time +import unittest + +sys.path = ["./py"] + sys.path + +import cbox + +global Document +global Transport +Document = cbox.Document +Transport = cbox.Transport + +song = Document.get_song() + +# Delete all the tracks and patterns +song.clear() + +# Create a binary blob that contains the MIDI events +pblob = bytes() +for noteindex in range(20): + # note on + pblob += cbox.Pattern.serialize_event(noteindex * 12, 0x90, 36+noteindex*3, 127) + # note off + pblob += cbox.Pattern.serialize_event(noteindex * 12 + 11, 0x90, 36+noteindex*3, 0) + +# This will be the length of the pattern (in pulses). It should be large enough +# to fit all the events +pattern_len = 10 * 24 * 2 + +# Create a new pattern object using events from the blob +pattern = song.pattern_from_blob(pblob, pattern_len) + +retrig = 10 +i = 0 +while i < 50: + i += 1 + retrig -= 1 + if retrig <= 0: + print ("Triggering adhoc pattern with ID 1") + Document.get_scene().play_pattern(pattern, 240, 0) + retrig = 5 + # Query JACK ports, new USB devices etc. + cbox.call_on_idle() + time.sleep(0.1) diff --git a/template/calfbox/app.c b/template/calfbox/app.c new file mode 100644 index 0000000..cf778a0 --- /dev/null +++ b/template/calfbox/app.c @@ -0,0 +1,391 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "blob.h" +#include "config-api.h" +#include "engine.h" +#include "instr.h" +#include "io.h" +#include "layer.h" +#include "menu.h" +#include "menuitem.h" +#include "meter.h" +#include "midi.h" +#include "module.h" +#include "scene.h" +#include "seq.h" +#include "song.h" +#include "track.h" +#include "ui.h" +#include "wavebank.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static gboolean lookup_midi_merger(const char *output, struct cbox_midi_merger **pmerger, GError **error) +{ + if (*output) + { + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, output, error)) + return FALSE; + + *pmerger = cbox_rt_get_midi_output(app.rt, &uuid); + if (!*pmerger) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown MIDI output UUID: '%s'", output); + return FALSE; + } + } + else + { + if (!app.engine->scene_count) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Scene not set"); + return FALSE; + } + *pmerger = &app.engine->scenes[0]->scene_input_merger; + } + return TRUE; +} + +static gboolean app_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + if (!cmd->command) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "NULL command"); + return FALSE; + } + if (cmd->command[0] != '/') + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid global command path '%s'", cmd->command); + return FALSE; + } + const char *obj = &cmd->command[1]; + const char *pos = strchr(obj, '/'); + if (pos) + { + if (!strncmp(obj, "master/", 7)) + return cbox_execute_sub(&app.engine->master->cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "config/", 7)) + return cbox_execute_sub(&app.config_cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "scene/", 6)) + return cbox_execute_sub(&app.engine->scenes[0]->cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "engine/", 7)) + return cbox_execute_sub(&app.engine->cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "rt/", 3)) + return cbox_execute_sub(&app.rt->cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "io/", 3)) + return cbox_execute_sub(&app.io.cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "song/", 5) && app.engine->master->song) + return cbox_execute_sub(&app.engine->master->song->cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "waves/", 6)) + return cbox_execute_sub(&cbox_waves_cmd_target, fb, cmd, pos, error); + else + if (!strncmp(obj, "doc/", 4)) + return cbox_execute_sub(cbox_document_get_cmd_target(app.document), fb, cmd, pos, error); + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return FALSE; + } + } + else + if (!strcmp(obj, "on_idle") && !strcmp(cmd->arg_types, "")) + { + return cbox_app_on_idle(fb, error); + } + else + if (!strcmp(obj, "send_event_to") && (!strcmp(cmd->arg_types, "siii") || !strcmp(cmd->arg_types, "sii") || !strcmp(cmd->arg_types, "si"))) + { + const char *output = CBOX_ARG_S(cmd, 0); + struct cbox_midi_merger *merger = NULL; + if (!lookup_midi_merger(output, &merger, error)) + return FALSE; + int mcmd = CBOX_ARG_I(cmd, 1); + int arg1 = 0, arg2 = 0; + if (cmd->arg_types[2] == 'i') + { + arg1 = CBOX_ARG_I(cmd, 2); + if (cmd->arg_types[3] == 'i') + arg2 = CBOX_ARG_I(cmd, 3); + } + struct cbox_midi_buffer buf; + cbox_midi_buffer_init(&buf); + cbox_midi_buffer_write_inline(&buf, 0, mcmd, arg1, arg2); + cbox_engine_send_events_to(app.engine, merger, &buf); + return TRUE; + } + else + if (!strcmp(obj, "send_sysex_to") && !strcmp(cmd->arg_types, "sb")) + { + const char *output = CBOX_ARG_S(cmd, 0); + struct cbox_midi_merger *merger = NULL; + if (!lookup_midi_merger(output, &merger, error)) + return FALSE; + const struct cbox_blob *blob = CBOX_ARG_B(cmd, 1); + struct cbox_midi_buffer buf; + cbox_midi_buffer_init(&buf); + cbox_midi_buffer_write_event(&buf, 0, blob->data, blob->size); + cbox_engine_send_events_to(app.engine, merger, &buf); + return TRUE; + } + else + if (!strcmp(obj, "update_playback") && !strcmp(cmd->arg_types, "")) + { + cbox_engine_update_song_playback(app.engine); + return TRUE; + } + else + if (!strcmp(obj, "get_pattern") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (app.engine->master->song && app.engine->master->song->tracks) + { + struct cbox_track *track = app.engine->master->song->tracks->data; + if (track) + { + struct cbox_track_item *item = track->items->data; + struct cbox_midi_pattern *pattern = item->pattern; + int length = 0; + struct cbox_blob *blob = cbox_midi_pattern_to_blob(pattern, &length); + gboolean res = cbox_execute_on(fb, NULL, "/pattern", "bi", error, blob, length); + cbox_blob_destroy(blob); + if (!res) + return FALSE; + } + } + return TRUE; + } + else + if (!strcmp(obj, "new_meter") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_meter *meter = cbox_meter_new(app.document, app.rt->io_env.srate); + + return cbox_execute_on(fb, NULL, "/uuid", "o", error, meter); + } + else + if (!strcmp(obj, "new_engine") && !strcmp(cmd->arg_types, "ii")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_engine *e = cbox_engine_new(app.document, NULL); + e->io_env.srate = CBOX_ARG_I(cmd, 0); + e->io_env.buffer_size = CBOX_ARG_I(cmd, 1); + + return e ? cbox_execute_on(fb, NULL, "/uuid", "o", error, e) : FALSE; + } + else + if (!strcmp(obj, "print_s") && !strcmp(cmd->arg_types, "s")) + { + g_message("Print: %s", CBOX_ARG_S(cmd, 0)); + return TRUE; + } + else + if (!strcmp(obj, "print_i") && !strcmp(cmd->arg_types, "i")) + { + g_message("Print: %d", CBOX_ARG_I(cmd, 0)); + return TRUE; + } + else + if (!strcmp(obj, "print_f") && !strcmp(cmd->arg_types, "f")) + { + g_message("Print: %f", CBOX_ARG_F(cmd, 0)); + return TRUE; + } + else + if (!strcmp(obj, "print_b") && !strcmp(cmd->arg_types, "b")) + { + g_message("Print: %s", (char *)CBOX_ARG_B(cmd, 0)->data); + return TRUE; + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return FALSE; + } +} + +struct config_foreach_data +{ + const char *prefix; + const char *command; + struct cbox_command_target *fb; + GError **error; + gboolean success; +}; + +void api_config_cb(void *user_data, const char *key) +{ + struct config_foreach_data *cfd = user_data; + if (!cfd->success) + return; + if (cfd->prefix && strncmp(cfd->prefix, key, strlen(cfd->prefix))) + return; + + if (!cbox_execute_on(cfd->fb, NULL, cfd->command, "s", cfd->error, key)) + { + cfd->success = FALSE; + return; + } +} + +static gboolean config_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + if (!strcmp(cmd->command, "/sections") && (!strcmp(cmd->arg_types, "") || !strcmp(cmd->arg_types, "s"))) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct config_foreach_data cfd = {cmd->arg_types[0] == 's' ? CBOX_ARG_S(cmd, 0) : NULL, "/section", fb, error, TRUE}; + cbox_config_foreach_section(api_config_cb, &cfd); + return cfd.success; + } + else if (!strcmp(cmd->command, "/keys") && (!strcmp(cmd->arg_types, "s") || !strcmp(cmd->arg_types, "ss"))) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct config_foreach_data cfd = {cmd->arg_types[1] == 's' ? CBOX_ARG_S(cmd, 1) : NULL, "/key", fb, error, TRUE}; + cbox_config_foreach_key(api_config_cb, CBOX_ARG_S(cmd, 0), &cfd); + return cfd.success; + } + else if (!strcmp(cmd->command, "/get") && !strcmp(cmd->arg_types, "ss")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + const char *value = cbox_config_get_string(CBOX_ARG_S(cmd, 0), CBOX_ARG_S(cmd, 1)); + if (!value) + return TRUE; + return cbox_execute_on(fb, NULL, "/value", "s", error, value); + } + else if (!strcmp(cmd->command, "/set") && !strcmp(cmd->arg_types, "sss")) + { + cbox_config_set_string(CBOX_ARG_S(cmd, 0), CBOX_ARG_S(cmd, 1), CBOX_ARG_S(cmd, 2)); + return TRUE; + } + else if (!strcmp(cmd->command, "/delete") && !strcmp(cmd->arg_types, "ss")) + { + cbox_config_remove_key(CBOX_ARG_S(cmd, 0), CBOX_ARG_S(cmd, 1)); + return TRUE; + } + else if (!strcmp(cmd->command, "/delete_section") && !strcmp(cmd->arg_types, "s")) + { + cbox_config_remove_section(CBOX_ARG_S(cmd, 0)); + return TRUE; + } + else if (!strcmp(cmd->command, "/save") && !strcmp(cmd->arg_types, "")) + { + return cbox_config_save(NULL, error); + } + else if (!strcmp(cmd->command, "/save") && !strcmp(cmd->arg_types, "s")) + { + return cbox_config_save(CBOX_ARG_S(cmd, 0), error); + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return FALSE; + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +gboolean cbox_app_on_idle(struct cbox_command_target *fb, GError **error) +{ + if (app.rt->io) + { + GError *error2 = NULL; + if (cbox_io_get_disconnect_status(&app.io, &error2)) + cbox_io_poll_ports(&app.io, fb); + else + { + if (error2) + g_error_free(error2); + int auto_reconnect = cbox_config_get_int("io", "auto_reconnect", 0); + if (auto_reconnect > 0) + { + sleep(auto_reconnect); + GError *error2 = NULL; + if (!cbox_io_cycle(&app.io, fb, &error2)) + { + gboolean suppress = FALSE; + if (fb) + suppress = cbox_execute_on(fb, NULL, "/io/cycle_failed", "s", NULL, error2->message); + if (!suppress) + g_warning("Cannot cycle the I/O: %s", (error2 && error2->message) ? error2->message : "Unknown error"); + g_error_free(error2); + } + else + { + if (fb) + cbox_execute_on(fb, NULL, "/io/cycled", "", NULL); + } + } + } + } + if (app.rt) + { + // Process results of asynchronous commands + cbox_rt_handle_cmd_queue(app.rt); + + if (!cbox_midi_appsink_send_to(&app.engine->appsink, fb, error)) + return FALSE; + } + return TRUE; +} + +struct cbox_app app = +{ + .rt = NULL, + .current_scene_name = NULL, + .cmd_target = + { + .process_cmd = app_process_cmd, + .user_data = &app + }, + .config_cmd_target = + { + .process_cmd = config_process_cmd, + .user_data = &app + }, +}; + diff --git a/template/calfbox/app.h b/template/calfbox/app.h new file mode 100644 index 0000000..df20506 --- /dev/null +++ b/template/calfbox/app.h @@ -0,0 +1,51 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_APP_H +#define CBOX_APP_H + +#include "cmd.h" +#include "dom.h" +#include "io.h" +#include "rt.h" +#include + +struct cbox_song; +struct cbox_tarpool; + +struct cbox_app +{ + struct cbox_io io; + struct cbox_document *document; + struct cbox_rt *rt; + struct cbox_engine *engine; + struct cbox_command_target cmd_target; + struct cbox_command_target config_cmd_target; + gchar *current_scene_name; + struct cbox_tarpool *tarpool; +}; + +struct cbox_menu; + +extern struct cbox_app app; + +struct cbox_menu *create_main_menu(void); + +extern gboolean cbox_app_on_idle(struct cbox_command_target *fb, GError **error); + +#endif diff --git a/template/calfbox/appmenu.c b/template/calfbox/appmenu.c new file mode 100644 index 0000000..7fb7cce --- /dev/null +++ b/template/calfbox/appmenu.c @@ -0,0 +1,466 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "blob.h" +#include "config-api.h" +#include "engine.h" +#include "instr.h" +#include "io.h" +#include "layer.h" +#include "menu.h" +#include "menuitem.h" +#include "meter.h" +#include "midi.h" +#include "module.h" +#include "scene.h" +#include "seq.h" +#include "song.h" +#include "track.h" +#include "ui.h" +#include "wavebank.h" + +#if USE_NCURSES + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int cmd_quit(struct cbox_menu_item_command *item, void *context) +{ + return 1; +} + +static void set_current_scene_name(gchar *name) +{ + if (app.current_scene_name) + g_free(app.current_scene_name); + app.current_scene_name = name; +} + +int cmd_load_scene(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_scene *scene = app.engine->scenes[0]; + cbox_scene_clear(scene); + if (!cbox_scene_load(scene, item->item.item_context, &error)) + cbox_print_error(error); + set_current_scene_name(g_strdup_printf("scene:%s", (const char *)item->item.item_context)); + return 0; +} + +int cmd_load_instrument(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_scene *scene = app.engine->scenes[0]; + cbox_scene_clear(scene); + struct cbox_layer *layer = cbox_layer_new_with_instrument(scene, (char *)item->item.item_context, &error); + + if (layer) + { + if (!cbox_scene_add_layer(scene, layer, &error)) + cbox_print_error(error); + set_current_scene_name(g_strdup_printf("instrument:%s", (const char *)item->item.item_context)); + } + else + { + cbox_print_error(error); + } + return 0; +} + +int cmd_load_layer(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_scene *scene = app.engine->scenes[0]; + cbox_scene_clear(scene); + struct cbox_layer *layer = cbox_layer_new_from_config(scene, (char *)item->item.item_context, &error); + + if (layer) + { + if (!cbox_scene_add_layer(scene, layer, &error)) + cbox_print_error(error); + set_current_scene_name(g_strdup_printf("layer:%s", (const char *)item->item.item_context)); + } + else + { + cbox_print_error(error); + CBOX_DELETE(scene); + } + return 0; +} + +gchar *scene_format_value(const struct cbox_menu_item_static *item, void *context) +{ + if (app.current_scene_name) + return g_strdup(app.current_scene_name); + else + return g_strdup("- No scene -"); +} + +gchar *transport_format_value(const struct cbox_menu_item_static *item, void *context) +{ + // XXXKF + // struct cbox_bbt bbt; + // cbox_master_to_bbt(app.engine->master, &bbt); + if (app.engine->master->spb == NULL) + return g_strdup("N/A"); + if (!strcmp((const char *)item->item.item_context, "pos")) + return g_strdup_printf("%d", (int)app.engine->master->spb->song_pos_samples); + else + return g_strdup_printf("%d", (int)app.engine->master->spb->song_pos_ppqn); +} + +struct cbox_config_section_cb_data +{ + struct cbox_menu *menu; + cbox_menu_item_execute_func func; + const char *prefix; +}; + +static void config_key_process(void *user_data, const char *key) +{ + struct cbox_config_section_cb_data *data = user_data; + + if (!strncmp(key, data->prefix, strlen(data->prefix))) + { + char *title = cbox_config_get_string(key, "title"); + if (title) + cbox_menu_add_item(data->menu, cbox_menu_item_new_command(title, data->func, strdup(key + strlen(data->prefix)), mif_dup_label | mif_free_context)); + else + cbox_menu_add_item(data->menu, cbox_menu_item_new_command(key, data->func, strdup(key + strlen(data->prefix)), mif_dup_label | mif_free_context)); + } +} + +struct cbox_menu *create_scene_menu(struct cbox_menu_item_menu *item, void *menu_context) +{ + struct cbox_menu *scene_menu = cbox_menu_new(); + struct cbox_config_section_cb_data cb = { .menu = scene_menu }; + + cbox_menu_add_item(scene_menu, cbox_menu_item_new_static("Scenes", NULL, NULL, 0)); + cb.prefix = "scene:"; + cb.func = cmd_load_scene; + cbox_config_foreach_section(config_key_process, &cb); + cbox_menu_add_item(scene_menu, cbox_menu_item_new_static("Layers", NULL, NULL, 0)); + cb.prefix = "layer:"; + cb.func = cmd_load_layer; + cbox_config_foreach_section(config_key_process, &cb); + cbox_menu_add_item(scene_menu, cbox_menu_item_new_static("Instruments", NULL, NULL, 0)); + cb.prefix = "instrument:"; + cb.func = cmd_load_instrument; + cbox_config_foreach_section(config_key_process, &cb); + + cbox_menu_add_item(scene_menu, cbox_menu_item_new_ok()); + return scene_menu; +} + +/////////////////////////////////////////////////////////////////////////////// + +static struct cbox_command_target *find_module_target(const struct cbox_menu_item_command *item) +{ + struct cbox_instrument *instr = item->item.item_context; + return &instr->module->cmd_target; + +} + +int cmd_stream_rewind(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_command_target *target = find_module_target(item); + if (target) + cbox_execute_on(target, NULL, "/seek", "i", &error, 0); + cbox_print_error_if(error); + return 0; +} + +int cmd_stream_play(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_command_target *target = find_module_target(item); + if (target) + cbox_execute_on(target, NULL, "/play", "", &error); + cbox_print_error_if(error); + return 0; +} + +int cmd_stream_stop(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_command_target *target = find_module_target(item); + if (target) + cbox_execute_on(target, NULL, "/stop", "", &error); + cbox_print_error_if(error); + return 0; +} + +int cmd_stream_unload(struct cbox_menu_item_command *item, void *context) +{ + GError *error = NULL; + struct cbox_command_target *target = find_module_target(item); + if (target) + cbox_execute_on(target, NULL, "/unload", "", &error); + cbox_print_error_if(error); + return 0; +} + +struct stream_response_data +{ + gchar *filename; + uint32_t pos, length, sample_rate, channels; +}; + +gboolean result_parser_status(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct stream_response_data *res = ct->user_data; + if (!strcmp(cmd->command, "/filename")) + res->filename = g_strdup(cmd->arg_values[0]); + if (!strcmp(cmd->command, "/pos")) + res->pos = *((uint32_t **)cmd->arg_values)[0]; + if (!strcmp(cmd->command, "/length")) + res->length = *((uint32_t **)cmd->arg_values)[0]; + if (!strcmp(cmd->command, "/sample_rate")) + res->sample_rate = *((uint32_t **)cmd->arg_values)[0]; + if (!strcmp(cmd->command, "/channels")) + res->channels = *((uint32_t **)cmd->arg_values)[0]; + //cbox_osc_command_dump(cmd); + return TRUE; +} + +char *cmd_stream_status(const struct cbox_menu_item_static *item, void *context) +{ + struct stream_response_data data = { NULL, 0, 0, 0, 0 }; + struct cbox_command_target response = { &data, result_parser_status }; + GError *error = NULL; + struct cbox_command_target *target = find_module_target((struct cbox_menu_item_command *)item); + if (target) + cbox_execute_on(target, &response, "/status", "", &error); + cbox_print_error_if(error); + gchar *res = NULL; + if (data.filename && data.length && data.sample_rate) + { + double duration = data.length * 1.0 / data.sample_rate; + res = g_strdup_printf("%s (%um%0.2fs, %uch, %uHz) (%0.2f%%)", data.filename, (unsigned)floor(duration / 60), duration - 60 * floor(duration / 60), (unsigned)data.channels, (unsigned)data.sample_rate, data.pos * 100.0 / data.length); + } + else + res = g_strdup("-"); + g_free(data.filename); + return res; +} + +struct load_waveform_context +{ + struct cbox_menu_item_context header; + struct cbox_instrument *instrument; + char *filename; +}; + +static void destroy_load_waveform_context(void *p) +{ + struct load_waveform_context *context = p; + g_free(context->filename); + free(context); +} + +int cmd_stream_load(struct cbox_menu_item_command *item, void *context) +{ + struct load_waveform_context *ctx = item->item.item_context; + GError *error = NULL; + struct cbox_command_target *target = &ctx->instrument->module->cmd_target; + if (target) + cbox_execute_on(target, NULL, "/load", "si", &error, ctx->filename, 0); + cbox_print_error_if(error); + return 0; +} + +struct cbox_menu *create_streamplay_menu(struct cbox_menu_item_menu *item, void *menu_context) +{ + struct cbox_menu *menu = cbox_menu_new(); + struct cbox_instrument *instr = item->item.item_context; + + assert(instr); + cbox_menu_add_item(menu, cbox_menu_item_new_static("Current stream", NULL, NULL, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_static("File", cmd_stream_status, instr, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_static("Module commands", NULL, NULL, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("Play stream", cmd_stream_play, instr, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("Stop stream", cmd_stream_stop, instr, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("Rewind stream", cmd_stream_rewind, instr, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("Unload stream", cmd_stream_unload, instr, 0)); + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Files available", NULL, NULL, 0)); + glob_t g; + gboolean found = (glob("*.wav", GLOB_TILDE_CHECK, NULL, &g) == 0); + found = glob("*.ogg", GLOB_TILDE_CHECK | (found ? GLOB_APPEND : 0), NULL, &g) || found; + if (found) + { + for (size_t i = 0; i < g.gl_pathc; i++) + { + struct load_waveform_context *context = calloc(1, sizeof(struct load_waveform_context)); + context->header.destroy_func = destroy_load_waveform_context; + context->instrument = instr; + context->filename = g_strdup(g.gl_pathv[i]); + cbox_menu_add_item(menu, cbox_menu_item_new_command(g_strdup_printf("Load: %s", g.gl_pathv[i]), cmd_stream_load, context, mif_free_label | mif_context_is_struct)); + } + } + globfree(&g); + + cbox_menu_add_item(menu, cbox_menu_item_new_ok()); + return menu; +} + +/////////////////////////////////////////////////////////////////////////////// + +struct cbox_menu *create_module_menu(struct cbox_menu_item_menu *item, void *menu_context) +{ + struct cbox_menu *menu = cbox_menu_new(); + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Scene instruments", NULL, NULL, 0)); + struct cbox_scene *scene = app.engine->scenes[0]; + for (uint32_t i = 0; i < scene->instrument_count; ++i) + { + struct cbox_instrument *instr = scene->instruments[i]; + create_menu_func menufunc = NULL; + + if (!strcmp(instr->module->engine_name, "stream_player")) + menufunc = create_streamplay_menu; + if (menufunc) + cbox_menu_add_item(menu, cbox_menu_item_new_dynamic_menu(g_strdup_printf("%s (%s)", instr->module->instance_name, instr->module->engine_name), menufunc, instr, mif_free_label)); + else + cbox_menu_add_item(menu, cbox_menu_item_new_static(g_strdup_printf("%s (%s)", instr->module->instance_name, instr->module->engine_name), NULL, NULL, mif_free_label)); + } + cbox_menu_add_item(menu, cbox_menu_item_new_ok()); + return menu; +} + +/////////////////////////////////////////////////////////////////////////////// + +static void restart_song() +{ + cbox_master_stop(app.engine->master); + cbox_master_seek_ppqn(app.engine->master, 0); + cbox_master_play(app.engine->master); +} + +int cmd_pattern_none(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_clear(app.engine->master->song); + cbox_engine_update_song_playback(app.engine); + return 0; +} + +int cmd_pattern_simple(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_new_metronome(app.engine->master->song, 1, app.engine->master->ppqn_factor)); + restart_song(); + return 0; +} + +int cmd_pattern_normal(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_new_metronome(app.engine->master->song, app.engine->master->timesig_num, app.engine->master->ppqn_factor)); + restart_song(); + return 0; +} + +int cmd_load_drumpattern(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_load(app.engine->master->song, item->item.item_context, 1, app.engine->master->ppqn_factor)); + restart_song(); + return 0; +} + +int cmd_load_drumtrack(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_load_track(app.engine->master->song, item->item.item_context, 1, app.engine->master->ppqn_factor)); + restart_song(); + return 0; +} + +int cmd_load_pattern(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_load(app.engine->master->song, item->item.item_context, 0, app.engine->master->ppqn_factor)); + restart_song(); + return 0; +} + +int cmd_load_track(struct cbox_menu_item_command *item, void *context) +{ + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_load_track(app.engine->master->song, item->item.item_context, 0, app.engine->master->ppqn_factor)); + restart_song(); + return 0; +} + +struct cbox_menu *create_pattern_menu(struct cbox_menu_item_menu *item, void *menu_context) +{ + struct cbox_menu *menu = cbox_menu_new(); + struct cbox_config_section_cb_data cb = { .menu = menu }; + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Pattern commands", NULL, NULL, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("No pattern", cmd_pattern_none, NULL, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("Simple metronome", cmd_pattern_simple, NULL, 0)); + cbox_menu_add_item(menu, cbox_menu_item_new_command("Normal metronome", cmd_pattern_normal, NULL, 0)); + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Drum tracks", NULL, NULL, 0)); + cb.prefix = "drumtrack:"; + cb.func = cmd_load_drumtrack; + cbox_config_foreach_section(config_key_process, &cb); + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Melodic tracks", NULL, NULL, 0)); + cb.prefix = "track:"; + cb.func = cmd_load_track; + cbox_config_foreach_section(config_key_process, &cb); + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Drum patterns", NULL, NULL, 0)); + cb.prefix = "drumpattern:"; + cb.func = cmd_load_drumpattern; + cbox_config_foreach_section(config_key_process, &cb); + + cbox_menu_add_item(menu, cbox_menu_item_new_static("Melodic patterns", NULL, NULL, 0)); + cb.prefix = "pattern:"; + cb.func = cmd_load_pattern; + cbox_config_foreach_section(config_key_process, &cb); + + cbox_menu_add_item(menu, cbox_menu_item_new_ok()); + return menu; +} + +struct cbox_menu *create_main_menu() +{ + struct cbox_menu *main_menu = cbox_menu_new(); + cbox_menu_add_item(main_menu, cbox_menu_item_new_static("Current scene:", scene_format_value, NULL, 0)); + cbox_menu_add_item(main_menu, cbox_menu_item_new_dynamic_menu("Set scene", create_scene_menu, NULL, 0)); + cbox_menu_add_item(main_menu, cbox_menu_item_new_dynamic_menu("Module control", create_module_menu, NULL, 0)); + cbox_menu_add_item(main_menu, cbox_menu_item_new_dynamic_menu("Pattern control", create_pattern_menu, NULL, 0)); + + cbox_menu_add_item(main_menu, cbox_menu_item_new_static("Variables", NULL, NULL, 0)); + // cbox_menu_add_item(main_menu, cbox_menu_item_new_int("foo:", &var1, 0, 127, NULL)); + // cbox_menu_add_item(main_menu, "bar:", menu_item_value_double, &mx_double_var2, &var2); + //cbox_menu_add_item(main_menu, cbox_menu_item_new_static("pos:", transport_format_value, "pos", 0)); + //cbox_menu_add_item(main_menu, cbox_menu_item_new_static("bbt:", transport_format_value, "bbt", 0)); + cbox_menu_add_item(main_menu, cbox_menu_item_new_static("Commands", NULL, NULL, 0)); + cbox_menu_add_item(main_menu, cbox_menu_item_new_command("Quit", cmd_quit, NULL, 0)); + return main_menu; +} + +#endif diff --git a/template/calfbox/autogen.sh b/template/calfbox/autogen.sh new file mode 100755 index 0000000..13a0322 --- /dev/null +++ b/template/calfbox/autogen.sh @@ -0,0 +1,6 @@ +aclocal --force || exit 1 +libtoolize --force --automake --copy || exit 1 +autoheader --force || exit 1 +autoconf --force || exit 1 +automake --add-missing --copy || exit 1 + diff --git a/template/calfbox/auxbus.c b/template/calfbox/auxbus.c new file mode 100644 index 0000000..da43be1 --- /dev/null +++ b/template/calfbox/auxbus.c @@ -0,0 +1,102 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "auxbus.h" +#include "scene.h" +#include +#include + +extern gboolean cbox_scene_insert_aux_bus(struct cbox_scene *scene, struct cbox_aux_bus *aux_bus); +extern void cbox_scene_remove_aux_bus(struct cbox_scene *scene, struct cbox_aux_bus *bus); + +CBOX_CLASS_DEFINITION_ROOT(cbox_aux_bus) + +static gboolean cbox_aux_bus_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_aux_bus *aux_bus = ct->user_data; + struct cbox_rt *rt = (struct cbox_rt *)cbox_document_get_service(CBOX_GET_DOCUMENT(aux_bus), "rt"); + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + return cbox_execute_on(fb, NULL, "/name", "s", error, aux_bus->name) && + CBOX_OBJECT_DEFAULT_STATUS(aux_bus, fb, error); + } + else + if (!strncmp(cmd->command, "/slot/", 6)) + { + return cbox_module_slot_process_cmd(&aux_bus->module, fb, cmd, cmd->command + 5, CBOX_GET_DOCUMENT(aux_bus), rt, aux_bus->owner->engine, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +struct cbox_aux_bus *cbox_aux_bus_load(struct cbox_scene *scene, const char *name, struct cbox_rt *rt, GError **error) +{ + struct cbox_module *module = cbox_module_new_from_fx_preset(name, CBOX_GET_DOCUMENT(scene), rt, scene->engine, error); + if (!module) + return NULL; + + struct cbox_aux_bus *p = malloc(sizeof(struct cbox_aux_bus)); + CBOX_OBJECT_HEADER_INIT(p, cbox_aux_bus, CBOX_GET_DOCUMENT(scene)); + cbox_command_target_init(&p->cmd_target, cbox_aux_bus_process_cmd, p); + p->name = g_strdup(name); + p->owner = scene; + p->module = module; + p->refcount = 0; + // XXXKF this work up to buffer size of 8192 floats, this should be determined from JACK settings and updated when + // JACK buffer size changes + p->input_bufs[0] = malloc(8192 * sizeof(float)); + p->input_bufs[1] = malloc(8192 * sizeof(float)); + p->output_bufs[0] = malloc(8192 * sizeof(float)); + p->output_bufs[1] = malloc(8192 * sizeof(float)); + CBOX_OBJECT_REGISTER(p); + cbox_scene_insert_aux_bus(scene, p); + + return p; +} + +void cbox_aux_bus_ref(struct cbox_aux_bus *bus) +{ + ++bus->refcount; +} + +void cbox_aux_bus_unref(struct cbox_aux_bus *bus) +{ + assert(bus->refcount > 0); + --bus->refcount; +} + +void cbox_aux_bus_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_aux_bus *bus = CBOX_H2O(objhdr); + if (bus->owner) + { + cbox_scene_remove_aux_bus(bus->owner, bus); + bus->owner = NULL; + } + CBOX_DELETE(bus->module); + bus->module = NULL; + assert(!bus->refcount); + g_free(bus->name); + free(bus->input_bufs[0]); + free(bus->input_bufs[1]); + free(bus); +} + diff --git a/template/calfbox/auxbus.h b/template/calfbox/auxbus.h new file mode 100644 index 0000000..63159b5 --- /dev/null +++ b/template/calfbox/auxbus.h @@ -0,0 +1,47 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_AUXBUS_H +#define CBOX_AUXBUS_H + +#include "dom.h" +#include "module.h" + +struct cbox_scene; + +CBOX_EXTERN_CLASS(cbox_aux_bus) + +struct cbox_aux_bus +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + struct cbox_scene *owner; + + gchar *name; + struct cbox_module *module; + int refcount; + + float *input_bufs[2]; + float *output_bufs[2]; +}; + +extern struct cbox_aux_bus *cbox_aux_bus_load(struct cbox_scene *scene, const char *name, struct cbox_rt *rt, GError **error); +extern void cbox_aux_bus_ref(struct cbox_aux_bus *bus); +extern void cbox_aux_bus_unref(struct cbox_aux_bus *bus); + +#endif diff --git a/template/calfbox/background_example.py b/template/calfbox/background_example.py new file mode 100644 index 0000000..7addb7c --- /dev/null +++ b/template/calfbox/background_example.py @@ -0,0 +1,6 @@ +import _cbox +import time +while True: + _cbox.do_cmd("/on_idle", None, []) + time.sleep(0.1) + diff --git a/template/calfbox/biquad-float.h b/template/calfbox/biquad-float.h new file mode 100644 index 0000000..f2789ab --- /dev/null +++ b/template/calfbox/biquad-float.h @@ -0,0 +1,398 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_BIQUAD_FLOAT_H +#define CBOX_BIQUAD_FLOAT_H + +#include "config.h" +#include "dspmath.h" + +struct cbox_biquadf_state +{ + float x1; + float y1; + float x2; + float y2; +}; + +struct cbox_biquadf_coeffs +{ + float a0; + float a1; + float a2; + float b1; + float b2; +}; + +static inline void cbox_biquadf_reset(struct cbox_biquadf_state *state) +{ + state->x1 = state->y1 = state->x2 = state->y2 = 0.f; +} + +static inline float cbox_biquadf_is_audible(struct cbox_biquadf_state *state, float level) +{ + return fabs(state->x1) + fabs(state->x2) + fabs(state->y1) + fabs(state->y2) >= level; +} + +// Based on filter coefficient equations by Robert Bristow-Johnson +static inline void cbox_biquadf_set_lp_rbj(struct cbox_biquadf_coeffs *coeffs, float fc, float q, float sr) +{ + float omega=(float)(2*M_PI*fc/sr); + float sn=sin(omega); + float cs=cos(omega); + float alpha=(float)(sn/(2*q)); + float inv=(float)(1.0/(1.0+alpha)); + + coeffs->a2 = coeffs->a0 = (float)(inv*(1 - cs)*0.5f); + coeffs->a1 = coeffs->a0 + coeffs->a0; + coeffs->b1 = (float)(-2*cs*inv); + coeffs->b2 = (float)((1 - alpha)*inv); +} + +static inline void cbox_biquadf_set_lp_rbj_lookup(struct cbox_biquadf_coeffs *coeffs, const struct cbox_sincos *sincos, float q) +{ + float sn=sincos->sine; + float cs=sincos->cosine; + float alpha=(float)(sn/(2*q)); + float inv=(float)(1.0/(1.0+alpha)); + + coeffs->a2 = coeffs->a0 = (float)(inv*(1 - cs)*0.5f); + coeffs->a1 = coeffs->a0 + coeffs->a0; + coeffs->b1 = (float)(-2*cs*inv); + coeffs->b2 = (float)((1 - alpha)*inv); +} + +// Based on filter coefficient equations by Robert Bristow-Johnson +static inline void cbox_biquadf_set_hp_rbj(struct cbox_biquadf_coeffs *coeffs, float fc, float q, float sr) +{ + float omega=(float)(2*M_PI*fc/sr); + float sn=sin(omega); + float cs=cos(omega); + float alpha=(float)(sn/(2*q)); + float inv=(float)(1.0/(1.0+alpha)); + + coeffs->a2 = coeffs->a0 = (float)(inv*(1 + cs)*0.5f); + coeffs->a1 = -2 * coeffs->a0; + coeffs->b1 = (float)(-2*cs*inv); + coeffs->b2 = (float)((1 - alpha)*inv); +} + +static inline void cbox_biquadf_set_hp_rbj_lookup(struct cbox_biquadf_coeffs *coeffs, const struct cbox_sincos *sincos, float q) +{ + float sn=sincos->sine; + float cs=sincos->cosine; + float alpha=(float)(sn/(2*q)); + float inv=(float)(1.0/(1.0+alpha)); + + coeffs->a2 = coeffs->a0 = (float)(inv*(1 + cs)*0.5f); + coeffs->a1 = -2 * coeffs->a0; + coeffs->b1 = (float)(-2*cs*inv); + coeffs->b2 = (float)((1 - alpha)*inv); +} + +// Based on filter coefficient equations by Robert Bristow-Johnson +static inline void cbox_biquadf_set_bp_rbj(struct cbox_biquadf_coeffs *coeffs, float fc, float q, float sr) +{ + float omega=(float)(2*M_PI*fc/sr); + float sn=sin(omega); + float cs=cos(omega); + float alpha=(float)(sn/(2*q)); + float inv=(float)(1.0/(1.0+alpha)); + + coeffs->a0 = (float)(inv*alpha); + coeffs->a1 = 0.f; + coeffs->a2 = -coeffs->a0; + coeffs->b1 = (float)(-2*cs*inv); + coeffs->b2 = (float)((1 - alpha)*inv); +} + +static inline void cbox_biquadf_set_bp_rbj_lookup(struct cbox_biquadf_coeffs *coeffs, const struct cbox_sincos *sincos, float q) +{ + float sn=sincos->sine; + float cs=sincos->cosine; + float alpha=(float)(sn/(2*q)); + float inv=(float)(1.0/(1.0+alpha)); + + coeffs->a0 = (float)(inv*alpha); + coeffs->a1 = 0.f; + coeffs->a2 = -coeffs->a0; + coeffs->b1 = (float)(-2*cs*inv); + coeffs->b2 = (float)((1 - alpha)*inv); +} + + +// Based on filter coefficient equations by Robert Bristow-Johnson +static inline void cbox_biquadf_set_peakeq_rbj(struct cbox_biquadf_coeffs *coeffs, float freq, float q, float peak, float sr) +{ + float A = sqrt(peak); + float w0 = freq * 2 * M_PI * (1.0 / sr); + float alpha = sin(w0) / (2 * q); + float ib0 = 1.0 / (1 + alpha/A); + coeffs->a1 = coeffs->b1 = -2*cos(w0) * ib0; + coeffs->a0 = ib0 * (1 + alpha*A); + coeffs->a2 = ib0 * (1 - alpha*A); + coeffs->b2 = ib0 * (1 - alpha/A); +} + +static inline void cbox_biquadf_set_peakeq_rbj_scaled(struct cbox_biquadf_coeffs *coeffs, float freq, float q, float A, float sr) +{ + float w0 = freq * 2 * M_PI * (1.0 / sr); + float alpha = sin(w0) / (2 * q); + float ib0 = 1.0 / (1 + alpha/A); + coeffs->a1 = coeffs->b1 = -2*cos(w0) * ib0; + coeffs->a0 = ib0 * (1 + alpha*A); + coeffs->a2 = ib0 * (1 - alpha*A); + coeffs->b2 = ib0 * (1 - alpha/A); +} + +// This is my math, and it's rather suspect +static inline void cbox_biquadf_set_1plp(struct cbox_biquadf_coeffs *coeffs, float freq, float sr) +{ + float w = hz2w(freq, sr); + float x = tan (w * 0.5f); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + + coeffs->a0 = a01; + coeffs->a1 = a01; + coeffs->b1 = b1; + coeffs->a2 = 0; + coeffs->b2 = 0; +} + +static inline void cbox_biquadf_set_1php(struct cbox_biquadf_coeffs *coeffs, float freq, float sr) +{ + float w = hz2w(freq, sr); + float x = tan (w * 0.5f); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + + coeffs->a0 = q; + coeffs->a1 = -q; + coeffs->b1 = b1; + coeffs->a2 = 0; + coeffs->b2 = 0; +} + +static inline void cbox_biquadf_set_1p(struct cbox_biquadf_coeffs *coeffs, float a0, float a1, float b1, int two_copies) +{ + if (two_copies) + { + // (a0 + a1z) * (a0 + a1z) = a0^2 + 2*a0*a1*z + a1^2*z^2 + // (1 - b1z) * (1 - b1z) = 1 - 2b1*z + b1^2*z^2 + coeffs->a0 = a0*a0; + coeffs->a1 = 2*a0*a1; + coeffs->b1 = 2 * b1; + coeffs->a2 = a1*a1; + coeffs->b2 = b1*b1; + } + else + { + coeffs->a0 = a0; + coeffs->a1 = a1; + coeffs->b1 = b1; + coeffs->a2 = 0; + coeffs->b2 = 0; + } +} + +static inline void cbox_biquadf_set_1plp_lookup(struct cbox_biquadf_coeffs *coeffs, const struct cbox_sincos *sincos, int two_copies) +{ + float x = sincos->prewarp; + float q = sincos->prewarp2; + float a01 = x*q; + float b1 = a01 - q; + + cbox_biquadf_set_1p(coeffs, a01, a01, b1, two_copies); +} + +static inline void cbox_biquadf_set_1php_lookup(struct cbox_biquadf_coeffs *coeffs, const struct cbox_sincos *sincos, int two_copies) +{ + float x = sincos->prewarp; + float q = sincos->prewarp2; + float a01 = x*q; + float b1 = a01 - q; + + cbox_biquadf_set_1p(coeffs, q, -q, b1, two_copies); +} + +#if USE_NEON + +#include + +static inline void cbox_biquadf_process(struct cbox_biquadf_state *state, struct cbox_biquadf_coeffs *coeffs, float *buffer) +{ + int i; + float32x2_t c0 = {coeffs->a0, 0}; + float32x2_t c1 = {coeffs->a1, -coeffs->b1}; + float32x2_t c2 = {coeffs->a2, -coeffs->b2}; + float32x2_t s1 = {state->x1, state->y1}; + float32x2_t s2 = {state->x2, state->y2}; + + for (i = 0; i < CBOX_BLOCK_SIZE; i ++) + { + float32x2_t in12 = {buffer[i], 0.f}; + + float32x2_t out12 = vmla_f32(vmla_f32(vmul_f32(c1, s1), c2, s2), in12, c0); // [a1 * x1 + a2 * x2 + a0 * in, -b1 * y1 - b2 * y2 + 0] + float32x2x2_t trn = vtrn_f32(out12, in12); // [[a1 * x1 + a2 * x2 + a0 * in, in12], [-b1 * y1 - b2 * y2, 0]] + float32x2_t out120 = vadd_f32(trn.val[0], trn.val[1]); + + s2 = s1; + s1 = vrev64_f32(out120); + buffer[i] = out120[0]; + } + state->x1 = s1[0]; + state->y1 = s1[1]; + state->x2 = s2[0]; + state->y2 = s2[1]; +} + +#else + +static inline void cbox_biquadf_process(struct cbox_biquadf_state *state, struct cbox_biquadf_coeffs *coeffs, float *buffer) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float a2 = coeffs->a2; + float b1 = coeffs->b1; + float b2 = coeffs->b2; + double y1 = state->y1; + double y2 = state->y2; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float in = buffer[i]; + double out = a0 * in + a1 * state->x1 + a2 * state->x2 - b1 * y1 - b2 * y2; + + buffer[i] = out; + state->x2 = state->x1; + state->x1 = in; + y2 = y1; + y1 = out; + } + state->y2 = sanef(y2); + state->y1 = sanef(y1); +} + +#endif + +static inline void cbox_biquadf_process_stereo(struct cbox_biquadf_state *lstate, struct cbox_biquadf_state *rstate, struct cbox_biquadf_coeffs *coeffs, float *buffer) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float a2 = coeffs->a2; + float b1 = coeffs->b1; + float b2 = coeffs->b2; + double ly1 = lstate->y1; + double ly2 = lstate->y2; + double ry1 = rstate->y1; + double ry2 = rstate->y2; + + for (i = 0; i < 2 * CBOX_BLOCK_SIZE; i += 2) + { + float inl = buffer[i], inr = buffer[i + 1]; + float outl = a0 * inl + a1 * lstate->x1 + a2 * lstate->x2 - b1 * ly1 - b2 * ly2; + float outr = a0 * inr + a1 * rstate->x1 + a2 * rstate->x2 - b1 * ry1 - b2 * ry2; + + lstate->x2 = lstate->x1; + lstate->x1 = inl; + ly2 = ly1; + ly1 = outl; + buffer[i] = outl; + + rstate->x2 = rstate->x1; + rstate->x1 = inr; + ry2 = ry1; + ry1 = outr; + buffer[i + 1] = outr; + } + lstate->y2 = sanef(ly2); + lstate->y1 = sanef(ly1); + rstate->y2 = sanef(ry2); + rstate->y1 = sanef(ry1); +} + +static inline double cbox_biquadf_process_sample(struct cbox_biquadf_state *state, struct cbox_biquadf_coeffs *coeffs, double in) +{ + double out = sanef(coeffs->a0 * sanef(in) + coeffs->a1 * state->x1 + coeffs->a2 * state->x2 - coeffs->b1 * state->y1 - coeffs->b2 * state->y2); + + state->x2 = state->x1; + state->x1 = in; + state->y2 = state->y1; + state->y1 = out; + + return out; +} + +static inline void cbox_biquadf_process_to(struct cbox_biquadf_state *state, struct cbox_biquadf_coeffs *coeffs, float *buffer_in, float *buffer_out) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float a2 = coeffs->a2; + float b1 = coeffs->b1; + float b2 = coeffs->b2; + double y1 = state->y1; + double y2 = state->y2; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float in = buffer_in[i]; + double out = a0 * in + a1 * state->x1 + a2 * state->x2 - b1 * y1 - b2 * y2; + + buffer_out[i] = out; + state->x2 = state->x1; + state->x1 = in; + y2 = y1; + y1 = out; + } + state->y2 = sanef(y2); + state->y1 = sanef(y1); +} + +static inline void cbox_biquadf_process_adding(struct cbox_biquadf_state *state, struct cbox_biquadf_coeffs *coeffs, float *buffer_in, float *buffer_out) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float a2 = coeffs->a2; + float b1 = coeffs->b1; + float b2 = coeffs->b2; + double y1 = state->y1; + double y2 = state->y2; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float in = buffer_in[i]; + double out = a0 * in + a1 * state->x1 + a2 * state->x2 - b1 * y1 - b2 * y2; + + buffer_out[i] += out; + state->x2 = state->x1; + state->x1 = in; + y2 = y1; + y1 = out; + } + state->y2 = sanef(y2); + state->y1 = sanef(y1); +} + +#endif diff --git a/template/calfbox/blob.c b/template/calfbox/blob.c new file mode 100644 index 0000000..e8d65f6 --- /dev/null +++ b/template/calfbox/blob.c @@ -0,0 +1,130 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "blob.h" +#include "tarfile.h" +#include +#include +#include +#include +#include +#include +#include + +struct cbox_blob *cbox_blob_new(size_t size) +{ + struct cbox_blob *p = malloc(sizeof(struct cbox_blob)); + if (!p) + return NULL; + p->data = size ? malloc(size) : NULL; + p->size = size; + return p; +} + +struct cbox_blob *cbox_blob_new_copy_data(const void *data, size_t size) +{ + struct cbox_blob *p = cbox_blob_new(size); + if (!p) + return NULL; + memcpy(p, data, size); + return p; +} + +struct cbox_blob *cbox_blob_new_acquire_data(void *data, size_t size) +{ + struct cbox_blob *p = malloc(sizeof(struct cbox_blob)); + if (!p) + return NULL; + p->data = data; + p->size = size; + return p; +} + +static struct cbox_blob *read_from_fd(const char *context_name, const char *pathname, int fd, size_t size, GError **error) +{ + struct cbox_blob *blob = cbox_blob_new(size + 1); + if (!blob) + { + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "%s: cannot allocate memory for file '%s'", context_name, pathname); + return NULL; + } + uint8_t *data = blob->data; + size_t nread = 0; + do { + size_t chunk = size - nread; + if (chunk > 131072) + chunk = 131072; + size_t nv = read(fd, data + nread, chunk); + if (nv == (size_t)-1) + { + if (errno == EINTR) + continue; + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "%s: cannot read '%s': %s", context_name, pathname, strerror(errno)); + cbox_blob_destroy(blob); + return NULL; + } + nread += nv; + } while(nread < size); + // Make sure that the content is 0-padded but still has the original size + // (without extra zero byte) + data[nread] = '\0'; + blob->size = nread; + return blob; +} + +struct cbox_blob *cbox_blob_new_from_file(const char *context_name, struct cbox_tarfile *tarfile, const char *path, const char *name, size_t max_size, GError **error) +{ + gchar *fullpath = g_build_filename(path, name, NULL); + struct cbox_blob *blob = NULL; + if (tarfile) + { + struct cbox_taritem *item = cbox_tarfile_get_item_by_name(tarfile, fullpath, TRUE); + if (item) + { + int fd = cbox_tarfile_openitem(tarfile, item); + if (fd >= 0) + { + blob = read_from_fd(context_name, fullpath, fd, item->size, error); + cbox_tarfile_closeitem(tarfile, item, fd); + } + } + } + else + { + int fd = open(fullpath, O_RDONLY | O_LARGEFILE); + if (fd >= 0) + { + uint64_t size = lseek64(fd, 0, SEEK_END); + if (size <= max_size) + blob = read_from_fd(context_name, fullpath, fd, size, error); + else + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "%s: file '%s' too large (%llu while max size is %u)", context_name, fullpath, (unsigned long long)size, (unsigned)max_size); + close(fd); + } + else + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "%s: cannot open '%s': %s", context_name, fullpath, strerror(errno)); + } + g_free(fullpath); + return blob; +} + +void cbox_blob_destroy(struct cbox_blob *blob) +{ + free(blob->data); + free(blob); +} diff --git a/template/calfbox/blob.h b/template/calfbox/blob.h new file mode 100644 index 0000000..4999b0c --- /dev/null +++ b/template/calfbox/blob.h @@ -0,0 +1,39 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_BLOB_H +#define CBOX_BLOB_H + +#include +#include + +struct cbox_blob +{ + void *data; + size_t size; +}; + +struct cbox_tarfile; + +extern struct cbox_blob *cbox_blob_new(size_t size); +extern struct cbox_blob *cbox_blob_new_from_file(const char *context_name, struct cbox_tarfile *tarfile, const char *path, const char *name, size_t max_size, GError **error); +extern struct cbox_blob *cbox_blob_new_copy_data(const void *data, size_t size); +extern struct cbox_blob *cbox_blob_new_acquire_data(void *data, size_t size); +extern void cbox_blob_destroy(struct cbox_blob *blob); + +#endif diff --git a/template/calfbox/cboxrc-example b/template/calfbox/cboxrc-example new file mode 100644 index 0000000..fba4902 --- /dev/null +++ b/template/calfbox/cboxrc-example @@ -0,0 +1,574 @@ +[io] +inputs=2 +in_1=#1 +in_2=#2 +out_1=#1 +out_2=#2 + +;#midi=alsa_pcm:E-MU-XMidi2X2/midi_capture_2;alsa_pcm:E-MU-Xboard25/midi_capture_1 +;midi=alsa_pcm:E-MU-XMidi2X2/midi_capture_2;alsa_pcm:E-MU-XMidi2X2/midi_capture_1 +;midi=~alsa_pcm:in-.*-E-MU-XMidi2X2-MIDI-1;~alsa_pcm:in-.*-E-MU-XMidi2X2-MIDI-2;~alsa_pcm:in-.*-padKONTROL-MIDI-2 +midi=*.* + +[master] +tempo=100 +beats_per_bar=4 +;effect=cbox_reverb + +[fxpreset:cbox_reverb] +engine=reverb +reverb_time=800 +wet_gain=-12 +dry_gain=0 +stereo=-12 + +[scene:example] +layer1=organ +layer2=piano + +[layer:piano] +instrument=progmega +out_channel=1 +low_note=C3 + +[layer:organ] +instrument=default +out_channel=1 +high_note=B2 + +[instrument:default] +engine=tonewheel_organ +percussion=1 +percussion_3rd=1 +upper_drawbars=888000000 +;upper_drawbars=888888888 +;upper_drawbars=888000008 +;upper_drawbars=800000888 +;upper_drawbars=800064000 +;upper_drawbars=802244220 +lower_drawbars=838000000 +pedal_drawbars=80 +vibrato_upper=1 +vibrato_lower=1 +vibrato_mode=c3 + +[instrument:progmega] +engine=fluidsynth +sf2=ProgMegaBank.sf2 +reverb=1 +chorus=1 +channel1=SP250 +channel2=jRhodes3a +channel3=P5 Brass + +[instrument:progmega_cheap] +engine=fluidsynth +sf2=ProgMegaBank.sf2 +reverb=0 +chorus=0 +channel1=SP250 +channel2=jRhodes3a +channel3=P5 Brass + +[instrument:progmega_fx] +engine=fluidsynth +sf2=ProgMegaBank.sf2 +reverb=0 +chorus=0 +insert=chain +channel1=SP250 +channel2=jRhodes3a +channel3=P5 Brass + +[fxpreset:chain] +engine=fxchain +effect1=stream_phaser +effect2=stream_delay + +[fxpreset:stream_phaser] +engine=phaser +center=500 +mod_depth=300 +lfo_freq=3 + +[fxpreset:stream_delay] +engine=delay +delay=250 +wet_amt=0.25 +feedback_gain=-12 + +[autojack] +soundcard0=Omega +soundcard1=STA +soundcard2=Intel +jack_options=-r -T +alsa_options=-p 128 -n 3 -X raw +jackd=/usr/bin/jackd + +[soundcard:Omega] +usbid=1210:0002 + +[soundcard:STA] +device=DSP24 + +[soundcard:Intel] +device=Intel + +[pattern:walkingminor] +title=Walking-minor +beats=4 +resolution=1 +track1=bass +bass_channel=2 +bass_vel=80 +bass_notes=c1,d1,eb1,g1 + +[track:two] +title=Cm/Ebm +pos1=walkingminor +pos2=walkingminor+3 + +[drumpattern:pat1] +title=Straight - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9... .... 9.6. .... +sd_trigger=.... 9..5 .2.. 9... +hh_trigger=9353 7353 7353 73.3 +ho_trigger=.... .... .... ..3. + +[drumpattern:pat1fill] +title=Straight - Fill +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9... .... 9.6. .... +sd_trigger=..64 9..5 .2.7 99.9 +hh_trigger=9353 7353 7353 73.3 +ho_trigger=.... .... .... ..3. + +[drumpattern:pat2] +title=Enigma - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9..7 ..7. ..6. .... +sd_trigger=.... 9... .... 9... +hh_trigger=9353 7353 7353 7353 +ho_trigger=.... .... ..3. .... + +[drumpattern:pat2fill] +title=Enigma - Fill +beats=4 +resolution=4 +track1=bd +track2=sd +track3=hh +track4=ho +track5=ht +track6=mt +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +ht_note=d2 +mt_note=b1 +bd_trigger=9..7 ..7. ..6. .... +sd_trigger=.... 9... .... 9..9 +hh_trigger=9353 7353 7353 7353 +ho_trigger=.... .... ..3. .... +ht_trigger=.5.. .... ...7 .... +mt_trigger=.... ...5 .... ..5. + +[drumpattern:pat3] +title=Shuffle - Verse +beats=4 +swing=6 +track1=bd +track2=sd +track3=hh +track4=ho +track5=ht +track6=mt +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +ht_note=d2 +mt_note=b1 +bd_trigger=9.8. ...8 ..6. .... +sd_trigger=.... 94D. .54. 9..D +hh_trigger=9353 7353 7353 7353 +ho_trigger=.... .... .... .... +ht_trigger=.... .... .... .... +mt_trigger=.... .... .... .... + +[drumpattern:pat3fill] +title=Shuffle - Fill +beats=4 +swing=6 +track1=bd +track2=sd +track3=hh +track4=ho +track5=ht +track6=mt +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +ht_note=d2 +mt_note=b1 +bd_trigger=9.8. ...8 ..6. .... +sd_trigger=.... 9... F... F..9 +hh_trigger=9353 7353 7353 7353 +ho_trigger=.... .... .... .... +ht_trigger=.... .... ..9. .5.. +mt_trigger=.... .... ...7 ..7. + +[drumpattern:pat4] +title=Swing +beats=4 +resolution=3 +track1=bd +track2=sd +track3=hp +track4=rc +track5=rb +bd_note=c1 +sd_note=d1 +hp_note=g#1 +rc_note=d#2 +rb_note=f2 +bd_trigger=... ... ... ..3 +sd_trigger=... ..3 ..2 .3. +hp_trigger=... 6.. ... 6.. +rc_trigger=2.. ..4 4.. ..3 +rb_trigger=3.. ... 3.. ... + +[drumpattern:pat4fill] +title=Swing +beats=4 +resolution=3 +track1=bd +track2=sd +track3=hp +track4=rc +track5=rb +bd_note=c1 +sd_note=d1 +hp_note=g#1 +rc_note=d#2 +rb_note=f2 +bd_trigger=7.. 5.. ... ..5 +sd_trigger=..5 ..3 .25 43. +hp_trigger=... 6.. ... 6.. +rc_trigger=2.. ..4 4.. ... +rb_trigger=4.. ... 2.. ... + +[drumpattern:pat5] +title=6/8 Blues - Verse +beats=4 +resolution=3 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=7.. ... 7.. .44 +sd_trigger=... 8.. ... 8.. +hh_trigger=7.5 6.4 7.5 6.. +ho_trigger=... ... ... ..3 + +[drumpattern:pat5fill] +title=6/8 Blues - Fill +beats=4 +resolution=3 +track1=bd +track2=sd +track3=hh +track4=ho +track5=ht +track6=mt +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +ht_note=d2 +mt_note=b1 +bd_trigger=7.. ... ... ..4 +sd_trigger=... 8.D 75. 85. +hh_trigger=7.5 6.. 7.. ... +ho_trigger=.3. ... ... ... +ht_trigger=... ... ..7 ... +mt_trigger=... ... ... ..5 + +[drumpattern:pat6] +title=Pompopom - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9.97 ...9 .996 .... +sd_trigger=.... 9... .... 9... +hh_trigger=9.7. 7.7. 9.7. 7.7. +ho_trigger=.... .3.. .... .... + +[drumpattern:pat6fill] +title=Pompopom - Fill +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9..7 ...9 .9.6 .... +sd_trigger=.6.. 9.7. ..7D 97.9 +hh_trigger=9.7. 7.7. 9.7. 7.7. +ho_trigger=.... .3.. .... .... + +[drumpattern:pat7] +title=Rockish +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=98.6 ...7 .95. .... +sd_trigger=.... 9... .... 9..D +hh_trigger=7.5. 7.5. 7.5. 7.5. +ho_trigger=...3 .... .... .... + +[drumpattern:pat7fill] +title=Rockish - Fill +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +track5=mt +track6=ht +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +mt_note=b1 +ht_note=d2 +bd_trigger=98.6 ...7 .95. .... +sd_trigger=.... 9..7 ..9. 9..9 +hh_trigger=7.5. 7.5. 7.5. 7.5. +ho_trigger=.5.3 .... .... .... +mt_trigger=.... .... .... ..8. +ht_trigger=.... .... ...9 .... + +[drumpattern:pat8] +title=80s - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=96.. ..96 ..6. .... +sd_trigger=.... 9... .... 9... +hh_trigger=7.7. 7.7. 7.7. 7.7. +ho_trigger=.... .... .... .... + +[drumpattern:pat8fill] +title=80s - Fill +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +track5=mt +track6=ht +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +mt_note=b1 +ht_note=d2 +bd_trigger=96.. ..96 ..6. .... +sd_trigger=..7. 9... 7.7. 9... +hh_trigger=7.7. 7.7. 7.7. 7... +ho_trigger=.... .... .... .... +ht_trigger=.... .... .9.. .97. +mt_trigger=.... .... ...7 ...5 + +[drumpattern:pat9] +title=Disco - Verse +beats=4 +swing=2 +track1=bd +track2=sd +track3=hh +track4=ho +track5=rs +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +rs_note=c#1 +bd_trigger=9... 9... 9... 9... +rs_trigger=..4. .4.. 4..4 ..4. +sd_trigger=.... 8..6 .... 8... +hh_trigger=74.7 74.7 74.7 74.7 +ho_trigger=..5. ..5. ..5. ..5. + +[drumpattern:pat9fill] +title=Disco - Fill +beats=4 +swing=2 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9... 9... 9... 9... +sd_trigger=.6.. 8.58 .6.5 8688 +hh_trigger=74.7 74.7 74.7 74.7 +ho_trigger=..5. ..5. ..5. ..5. + +[drumpattern:crash] +title=Crash cymbal +beats=4 +track1=cc +cc_note=c#2 +cc_trigger=9... + +[drumpattern:crash2] +title=Kick+crash cymbal +beats=4 +track1=cc +track2=bd +cc_note=a#2 +bd_note=b0 +cc_trigger=5... +bd_trigger=5... + +[drumtrack:trk1] +title=Straight +pos1=pat1,crash +pos2=pat1 +pos3=pat1 +pos4=pat1fill + +[drumtrack:trk2] +title=Enigma +pos1=pat2,crash +pos2=pat2 +pos3=pat2 +pos4=pat2fill + +[drumtrack:trk3] +title=Shuffle +pos1=pat3,crash +pos2=pat3 +pos3=pat3 +pos4=pat3fill + +[drumtrack:trk4] +title=Swing +pos1=pat4,crash2 +pos2=pat4 +pos3=pat4 +pos4=pat4fill + +[drumtrack:trk5] +title=6/8 Blues +pos1=pat5,crash +pos2=pat5 +pos3=pat5 +pos4=pat5fill + +[drumtrack:trk6] +title=Pompopom +pos1=pat6,crash +pos2=pat6 +pos3=pat6 +pos4=pat6fill + +[drumtrack:trk7] +title=Rockish +pos1=pat7,crash +pos2=pat7 +pos3=pat7 +pos4=pat7fill + +[drumtrack:trk8] +title=80s +pos1=pat8,crash +pos2=pat8 +pos3=pat8 +pos4=pat8fill + +[drumtrack:trk9] +title=Disco +pos1=pat9,crash +pos2=pat9 +pos3=pat9 +pos4=pat9fill + +[layer:samplertest] +instrument=samplertest + +[instrument:samplertest] +engine=sampler +program0=prog + +[spgm:prog] +layer1=saw1 +layer2=saw2 + +[slayer:saw1] +sample=*saw +tune=-5 +pan=-30 +volume=-6 + +[slayer:saw2] +sample=*saw +tune=+5 +pan=30 +volume=-6 diff --git a/template/calfbox/chorus.c b/template/calfbox/chorus.c new file mode 100644 index 0000000..06945da --- /dev/null +++ b/template/calfbox/chorus.c @@ -0,0 +1,175 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#define MAX_CHORUS_LENGTH 4096 + +#define MODULE_PARAMS chorus_params + +static float sine_table[2049]; + +struct chorus_params +{ + float lfo_freq; + float min_delay; + float mod_depth; + float wet_dry; + float sphase; +}; + +struct chorus_module +{ + struct cbox_module module; + + float storage[MAX_CHORUS_LENGTH][2]; + struct chorus_params *params; + int pos; + float tp32dsr; + uint32_t phase; +}; + +MODULE_PROCESSCMD_FUNCTION(chorus) +{ + struct chorus_module *m = (struct chorus_module *)ct->user_data; + + EFFECT_PARAM("/min_delay", "f", min_delay, double, , 1, 20) else + EFFECT_PARAM("/mod_depth", "f", mod_depth, double, , 1, 20) else + EFFECT_PARAM("/lfo_freq", "f", lfo_freq, double, , 0, 20) else + EFFECT_PARAM("/stereo_phase", "f", sphase, double, , 0, 360) else + EFFECT_PARAM("/wet_dry", "f", wet_dry, double, , 0, 1) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/min_delay", "f", error, m->params->min_delay) && + cbox_execute_on(fb, NULL, "/mod_depth", "f", error, m->params->mod_depth) && + cbox_execute_on(fb, NULL, "/lfo_freq", "f", error, m->params->lfo_freq) && + cbox_execute_on(fb, NULL, "/stereo_phase", "f", error, m->params->sphase) && + cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry) && + CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void chorus_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct chorus_module *m = (struct chorus_module *)module; +} + +void chorus_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct chorus_module *m = (struct chorus_module *)module; + struct chorus_params *p = m->params; + + float min_delay = p->min_delay; + float mod_depth = p->mod_depth; + float wet_dry = p->wet_dry; + int i, c; + int mask = MAX_CHORUS_LENGTH - 1; + uint32_t sphase = (uint32_t)(p->sphase * 65536.0 * 65536.0 / 360); + uint32_t dphase = (uint32_t)(p->lfo_freq * m->tp32dsr); + const int fracbits = 32 - 11; + const int fracscale = 1 << fracbits; + + for (c = 0; c < 2; c++) + { + int pos = m->pos; + uint32_t phase = m->phase + c * sphase; + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float dry = inputs[c][i]; + float v0 = sine_table[phase >> fracbits]; + float v1 = sine_table[1 + (phase >> fracbits)]; + float lfo = v0 + (v1 - v0) * ((phase & (fracscale - 1)) * (1.0 / fracscale)); + + m->storage[pos & mask][c] = dry; + + float dva = min_delay + mod_depth * lfo; + int dv = (int)dva; + float frac = dva - dv; + float smp0 = m->storage[(pos - dv) & mask][c]; + float smp1 = m->storage[(pos - dv - 1) & mask][c]; + + float smp = smp0 + (smp1 - smp0) * frac; + + outputs[c][i] = sanef(dry + (smp - dry) * wet_dry); + + pos++; + phase += dphase; + } + } + + m->phase += CBOX_BLOCK_SIZE * dphase; + m->pos += CBOX_BLOCK_SIZE; +} + +MODULE_SIMPLE_DESTROY_FUNCTION(chorus) + +MODULE_CREATE_FUNCTION(chorus) +{ + static int inited = 0; + int i; + if (!inited) + { + inited = 1; + for (i = 0; i < 2049; i++) + sine_table[i] = 1 + sin(i * M_PI / 1024); + } + + struct chorus_module *m = malloc(sizeof(struct chorus_module)); + CALL_MODULE_INIT(m, 2, 2, chorus); + m->module.process_event = chorus_process_event; + m->module.process_block = chorus_process_block; + m->pos = 0; + m->phase = 0; + m->tp32dsr = 65536.0 * 65536.0 * m->module.srate_inv; + struct chorus_params *p = malloc(sizeof(struct chorus_params)); + m->params = p; + p->sphase = cbox_config_get_float(cfg_section, "stereo_phase", 90.f); + p->lfo_freq = cbox_config_get_float(cfg_section, "lfo_freq", 1.f); + p->min_delay = cbox_config_get_float(cfg_section, "min_delay", 20.f); + p->mod_depth = cbox_config_get_float(cfg_section, "mod_depth", 15.f); + p->wet_dry = cbox_config_get_float(cfg_section, "wet_dry", 0.5f); + for (i = 0; i < MAX_CHORUS_LENGTH; i++) + m->storage[i][0] = m->storage[i][1] = 0.f; + + return &m->module; +} + + +struct cbox_module_keyrange_metadata chorus_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata chorus_controllers[] = { +}; + +DEFINE_MODULE(chorus, 2, 2) + diff --git a/template/calfbox/cleanpythonbuild.sh b/template/calfbox/cleanpythonbuild.sh new file mode 100755 index 0000000..de408d9 --- /dev/null +++ b/template/calfbox/cleanpythonbuild.sh @@ -0,0 +1,11 @@ +#!/bin/bash +make clean +make distclean +rm build -rf +set -e +sh autogen.sh +./configure --prefix=/usr --without-python +make +python3 setup.py build +sudo python3 setup.py install +sudo make install diff --git a/template/calfbox/cmd.c b/template/calfbox/cmd.c new file mode 100644 index 0000000..775b24e --- /dev/null +++ b/template/calfbox/cmd.c @@ -0,0 +1,208 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "blob.h" +#include "cmd.h" +#include "dom.h" +#include "errors.h" +#include +#include +#include +#include +#include +#include + +void cbox_command_target_init(struct cbox_command_target *ct, cbox_process_cmd cmd, void *user_data) +{ + ct->process_cmd = cmd; + ct->user_data = user_data; +} + +gboolean cbox_execute_on(struct cbox_command_target *ct, struct cbox_command_target *fb, const char *cmd_name, const char *args, GError **error, ...) +{ + va_list av; + + va_start(av, error); + gboolean res = cbox_execute_on_v(ct, fb, cmd_name, args, av, error); + va_end(av); + return res; +} + +gboolean cbox_execute_on_v(struct cbox_command_target *ct, struct cbox_command_target *fb, const char *cmd_name, const char *args, va_list av, GError **error) +{ + int argcount = 0; + struct cbox_osc_command cmd; + uint8_t *extra_data; + // XXXKF might be not good enough for weird platforms + uint32_t unit_size = sizeof(double); + // this must be a power of 2 to guarantee proper alignment + assert(unit_size >= sizeof(int) && (unit_size == 4 || unit_size == 8)); + cmd.command = cmd_name; + cmd.arg_types = args; + for (int i = 0; args[i]; i++) + argcount = i + 1; + // contains pointers to all the values, plus values themselves in case of int/double + // (casting them to pointers is ugly, and va_arg does not return a lvalue) + cmd.arg_values = malloc(sizeof(void *) * argcount + unit_size * argcount); + extra_data = (uint8_t *)&cmd.arg_values[argcount]; + + for (int i = 0; i < argcount; i++) + { + int iv; + double fv; + void *pv = extra_data + unit_size * i; + switch(args[i]) + { + case 's': + cmd.arg_values[i] = va_arg(av, char *); + break; + case 'i': + iv = va_arg(av, int); + memcpy(pv, &iv, sizeof(int)); + cmd.arg_values[i] = pv; + break; + case 'f': // double really + fv = (double)va_arg(av, double); + memcpy(pv, &fv, sizeof(double)); + cmd.arg_values[i] = pv; + break; + case 'b': + cmd.arg_values[i] = va_arg(av, struct cbox_blob *); + break; + case 'o': + cmd.arg_values[i] = va_arg(av, struct cbox_objhdr *); + break; + case 'u': + cmd.arg_values[i] = va_arg(av, struct cbox_uuid *); + break; + default: + g_error("Invalid format character '%c' for command '%s'", args[i], cmd_name); + assert(0); + } + } + gboolean result = ct->process_cmd(ct, fb, &cmd, error); + free(cmd.arg_values); + return result; +} + +gboolean cbox_osc_command_dump(const struct cbox_osc_command *cmd) +{ + g_message("Command = %s, args = %s", cmd->command, cmd->arg_types); + for (int i = 0; cmd->arg_types[i]; i++) + { + switch(cmd->arg_types[i]) + { + case 's': + g_message("Args[%d] = '%s'", i, (const char *)cmd->arg_values[i]); + break; + case 'o': + { + struct cbox_objhdr *oh = cmd->arg_values[i]; + char buf[40]; + uuid_unparse(oh->instance_uuid.uuid, buf); + g_message("Args[%d] = uuid:'%s'", i, buf); + break; + } + case 'i': + g_message("Args[%d] = %d", i, *(int *)cmd->arg_values[i]); + break; + case 'f': + g_message("Args[%d] = %f", i, *(double *)cmd->arg_values[i]); + break; + case 'b': + { + struct cbox_blob *b = cmd->arg_values[i]; + g_message("Args[%d] = (%p, %d)", i, b->data, (int)b->size); + break; + } + default: + g_error("Invalid format character '%c' for command '%s'", cmd->arg_types[i], cmd->command); + assert(0); + return FALSE; + } + } + return TRUE; +} + +gboolean cbox_check_fb_channel(struct cbox_command_target *fb, const char *command, GError **error) +{ + if (fb) + return TRUE; + + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Feedback channel required for command '%s'", command); + return FALSE; +} + + +gboolean cbox_execute_sub(struct cbox_command_target *ct, struct cbox_command_target *fb, const struct cbox_osc_command *cmd, const char *new_command, GError **error) +{ + struct cbox_osc_command subcmd; + subcmd.command = new_command; + subcmd.arg_types = cmd->arg_types; + subcmd.arg_values = cmd->arg_values; + return ct->process_cmd(ct, fb, &subcmd, error); +} + +gboolean cbox_parse_path_part_int(const struct cbox_osc_command *cmd, const char *path, const char **subcommand, int *index, int min_index, int max_index, GError **error) +{ + char *numcopy = NULL; + if (!cbox_parse_path_part_str(cmd, path, subcommand, &numcopy, error)) + return FALSE; + if (!*subcommand) + return TRUE; + char *endptr = NULL; + *index = strtol(numcopy, &endptr, 10); + if (!*numcopy && *endptr) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid index %s for command %s", numcopy, cmd->command); + g_free(numcopy); + *subcommand = NULL; + return TRUE; + } + if (*index < min_index || *index > max_index) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Index %s out of range [%d, %d] for command %s", numcopy, min_index, max_index, cmd->command); + g_free(numcopy); + *subcommand = NULL; + return TRUE; + } + g_free(numcopy); + return TRUE; +} + +gboolean cbox_parse_path_part_str(const struct cbox_osc_command *cmd, const char *path, const char **subcommand, char **path_element, GError **error) +{ + *path_element = NULL; + *subcommand = NULL; + int plen = strlen(path); + if (!strncmp(cmd->command, path, plen)) + { + const char *num = cmd->command + plen; + const char *slash = strchr(num, '/'); + if (!slash) + { + cbox_set_command_error_with_msg(error, cmd, "needs at least one extra path element"); + return TRUE; + } + + *path_element = g_strndup(num, slash-num); + *subcommand = slash; + return TRUE; + } + return FALSE; +} diff --git a/template/calfbox/cmd.h b/template/calfbox/cmd.h new file mode 100644 index 0000000..fa34cdb --- /dev/null +++ b/template/calfbox/cmd.h @@ -0,0 +1,64 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_CMD_H +#define CBOX_CMD_H + +#include +#include +#include + +#define CBOX_ARG_I(cmd, idx) (*(int *)(cmd)->arg_values[(idx)]) +#define CBOX_ARG_S(cmd, idx) ((const char *)(cmd)->arg_values[(idx)]) +#define CBOX_ARG_B(cmd, idx) ((const struct cbox_blob *)(cmd)->arg_values[(idx)]) +#define CBOX_ARG_F(cmd, idx) (*(double *)(cmd)->arg_values[(idx)]) +#define CBOX_ARG_O(cmd, idx, src, class, error) cbox_document_get_object_by_text_uuid(CBOX_GET_DOCUMENT(src), (const char *)(cmd)->arg_values[(idx)], &CBOX_CLASS(class), (error)) +#define CBOX_ARG_S_ISNULL(cmd, idx) (0 == (const char *)(cmd)->arg_values[(idx)]) + +struct cbox_command_target; + +struct cbox_osc_command +{ + const char *command; + const char *arg_types; + void **arg_values; +}; + +typedef gboolean (*cbox_process_cmd)(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +struct cbox_command_target +{ + void *user_data; + cbox_process_cmd process_cmd; +}; + +void cbox_command_target_init(struct cbox_command_target *ct, cbox_process_cmd cmd, void *user_data); + +extern gboolean cbox_check_fb_channel(struct cbox_command_target *fb, const char *command, GError **error); + +extern gboolean cbox_execute_sub(struct cbox_command_target *ct, struct cbox_command_target *fb, const struct cbox_osc_command *cmd, const char *new_command, GError **error); +extern gboolean cbox_execute_on(struct cbox_command_target *ct, struct cbox_command_target *fb, const char *cmd, const char *args, GError **error, ...); +extern gboolean cbox_execute_on_v(struct cbox_command_target *ct, struct cbox_command_target *fb, const char *cmd, const char *args, va_list va, GError **error); + +extern gboolean cbox_osc_command_dump(const struct cbox_osc_command *cmd); + +// Note: this sets *subcommand to NULL on parse error; requires "/path/" as path +extern gboolean cbox_parse_path_part_int(const struct cbox_osc_command *cmd, const char *path, const char **subcommand, int *index, int min_index, int max_index, GError **error); +extern gboolean cbox_parse_path_part_str(const struct cbox_osc_command *cmd, const char *path, const char **subcommand, char **path_element, GError **error); + +#endif diff --git a/template/calfbox/compressor.c b/template/calfbox/compressor.c new file mode 100644 index 0000000..891b27b --- /dev/null +++ b/template/calfbox/compressor.c @@ -0,0 +1,155 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-float.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS compressor_params + +struct compressor_params +{ + float threshold; + float ratio; + float attack; + float release; + float makeup; +}; + +struct compressor_module +{ + struct cbox_module module; + + struct compressor_params *params, *old_params; + struct cbox_onepolef_coeffs attack_lp, release_lp, fast_attack_lp; + struct cbox_onepolef_state tracker; + struct cbox_onepolef_state tracker2; +}; + +MODULE_PROCESSCMD_FUNCTION(compressor) +{ + struct compressor_module *m = (struct compressor_module *)ct->user_data; + + EFFECT_PARAM("/makeup", "f", makeup, double, dB2gain_simple, -100, 100) else + EFFECT_PARAM("/threshold", "f", threshold, double, dB2gain_simple, -100, 100) else + EFFECT_PARAM("/ratio", "f", ratio, double, , 1, 100) else + EFFECT_PARAM("/attack", "f", attack, double, , 1, 1000) else + EFFECT_PARAM("/release", "f", release, double, , 1, 1000) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/makeup", "f", error, gain2dB_simple(m->params->makeup)) + && cbox_execute_on(fb, NULL, "/threshold", "f", error, gain2dB_simple(m->params->threshold)) + && cbox_execute_on(fb, NULL, "/ratio", "f", error, m->params->ratio) + && cbox_execute_on(fb, NULL, "/attack", "f", error, m->params->attack) + && cbox_execute_on(fb, NULL, "/release", "f", error, m->params->release) + && CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error) + ; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void compressor_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct compressor_module *m = module->user_data; +} + +void compressor_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct compressor_module *m = module->user_data; + + if (m->params != m->old_params) + { + float scale = M_PI * 1000 / m->module.srate; + cbox_onepolef_set_lowpass(&m->fast_attack_lp, 2 * scale / m->params->attack); + cbox_onepolef_set_lowpass(&m->attack_lp, scale / m->params->attack); + cbox_onepolef_set_lowpass(&m->release_lp, scale / m->params->release); + m->old_params = m->params; + } + + float threshold = m->params->threshold, invratio = 1.0 / m->params->ratio; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float left = inputs[0][i], right = inputs[1][i]; + float sig = 0.5 * (fabs(left) > fabs(right) ? fabs(left) : fabs(right)); + + int falling = sig < m->tracker.y1 && sig < m->tracker.x1; + int rising_fast = sig > 4 * m->tracker.y1 && sig > 4 * m->tracker.x1; + sig = cbox_onepolef_process_sample(&m->tracker, falling ? &m->release_lp : (rising_fast && m->tracker.y1 ? &m->fast_attack_lp : &m->attack_lp), sig); + sig = cbox_onepolef_process_sample(&m->tracker2, falling ? &m->release_lp : (rising_fast && m->tracker2.y1 ? &m->fast_attack_lp : &m->attack_lp), sig); + float gain = 1.0; + if (sig > threshold) + gain = threshold * powf(sig / threshold, invratio) / sig; + gain *= m->params->makeup; + + outputs[0][i] = left * gain; + outputs[1][i] = right * gain; + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION(compressor) + +MODULE_CREATE_FUNCTION(compressor) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct compressor_module *m = malloc(sizeof(struct compressor_module)); + CALL_MODULE_INIT(m, 2, 2, compressor); + m->module.process_event = compressor_process_event; + m->module.process_block = compressor_process_block; + + struct compressor_params *p = malloc(sizeof(struct compressor_params)); + p->threshold = cbox_config_get_gain_db(cfg_section, "threshold", -12.0); + p->ratio = cbox_config_get_float(cfg_section, "ratio", 2.0); + p->attack = cbox_config_get_float(cfg_section, "attack", 5.0); + p->release = cbox_config_get_float(cfg_section, "release", 100.0); + p->makeup = cbox_config_get_gain_db(cfg_section, "makeup", 6.0); + m->params = p; + m->old_params = NULL; + + cbox_onepolef_reset(&m->tracker); + cbox_onepolef_reset(&m->tracker2); + + return &m->module; +} + + +struct cbox_module_keyrange_metadata compressor_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata compressor_controllers[] = { +}; + +DEFINE_MODULE(compressor, 2, 2) + diff --git a/template/calfbox/config-api.c b/template/calfbox/config-api.c new file mode 100644 index 0000000..885635f --- /dev/null +++ b/template/calfbox/config-api.c @@ -0,0 +1,357 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" + +#include +#include +#include +#include +#include + +static GKeyFile *config_keyfile; +static gchar *keyfile_name; +static GStringChunk *cfg_strings = NULL; +static GHashTable *config_sections_hash = NULL; + +void cbox_config_init(const char *override_file) +{ + const gchar *keyfiledirs[3]; + const gchar *keyfilename = ".cboxrc"; + GKeyFileFlags flags = G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS; + GError *error = NULL; + if (config_keyfile) + return; + + config_sections_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + cfg_strings = g_string_chunk_new(100); + config_keyfile = g_key_file_new(); + + // Allow virtual (in-memory) config by passing empty string + if (override_file && !*override_file) + { + keyfile_name = g_strdup(""); + return; + } + + keyfiledirs[0] = getenv("HOME"); + keyfiledirs[1] = NULL; + // XXXKF add proper error handling + + if (override_file) + { + if (!g_key_file_load_from_file(config_keyfile, override_file, flags, &error)) + { + g_warning("Could not read user config: %s", error->message); + g_error_free(error); + } + else + { + keyfile_name = g_strdup(override_file); + g_message("User config pathname is %s", keyfile_name); + return; + } + } + + if (!g_key_file_load_from_dirs(config_keyfile, keyfilename, keyfiledirs, &keyfile_name, flags, &error)) + { + g_warning("Could not read cboxrc: %s, search dir = %s, filename = %s", error->message, keyfiledirs[0], keyfilename); + g_error_free(error); + } + else + { + g_message("Config pathname is %s", keyfile_name); + } +} + +int cbox_config_has_section(const char *section) +{ + return section && g_key_file_has_group(config_keyfile, section); +} + +char *cbox_config_get_string(const char *section, const char *key) +{ + return cbox_config_get_string_with_default(section, key, NULL); +} + +void cbox_config_set_string(const char *section, const char *key, const char *value) +{ + g_key_file_set_string(config_keyfile, section, key, value); +} + +char *cbox_config_permify(const char *temporary) +{ + return g_string_chunk_insert(cfg_strings, temporary); +} + +char *cbox_config_get_string_with_default(const char *section, const char *key, char *def_value) +{ + if (section && key && g_key_file_has_key(config_keyfile, section, key, NULL)) + { + gchar *tmp = g_key_file_get_string(config_keyfile, section, key, NULL); + gchar *perm = g_string_chunk_insert(cfg_strings, tmp); + g_free(tmp); + return perm; + } + else + { + return def_value ? g_string_chunk_insert(cfg_strings, def_value) : NULL; + } +} + +int cbox_config_get_int(const char *section, const char *key, int def_value) +{ + GError *error = NULL; + int result; + + if (!section || !key) + return def_value; + result = g_key_file_get_integer(config_keyfile, section, key, &error); + if (error) + { + g_error_free(error); + return def_value; + } + return result; +} + +void cbox_config_set_int(const char *section, const char *key, int value) +{ + g_key_file_set_integer(config_keyfile, section, key, value); +} + +float cbox_config_get_float(const char *section, const char *key, float def_value) +{ + GError *error = NULL; + float result; + + if (!section || !key) + return def_value; + result = g_key_file_get_double(config_keyfile, section, key, &error); + if (error) + { + g_error_free(error); + return def_value; + } + return result; +} + +void cbox_config_set_float(const char *section, const char *key, double value) +{ + g_key_file_set_double(config_keyfile, section, key, value); +} + +float cbox_config_get_gain(const char *section, const char *key, float def_value) +{ + GError *error = NULL; + float result; + + if (!section || !key) + return def_value; + result = g_key_file_get_double(config_keyfile, section, key, &error); + if (error) + { + g_error_free(error); + return def_value; + } + return pow(2.0, result / 6.0); +} + +float cbox_config_get_gain_db(const char *section, const char *key, float def_value) +{ + return cbox_config_get_gain(section, key, pow(2.0, def_value / 6.0)); +} + +void cbox_config_foreach_section(void (*process)(void *user_data, const char *section), void *user_data) +{ + gsize i, length = 0; + gchar **groups = g_key_file_get_groups (config_keyfile, &length); + if (!groups) + return; + for (i = 0; i < length; i++) + { + process(user_data, groups[i]); + } + g_strfreev(groups); +} + +void cbox_config_foreach_key(void (*process)(void *user_data, const char *key), const char *section, void *user_data) +{ + gsize i, length = 0; + gchar **keys = g_key_file_get_keys (config_keyfile, section, &length, NULL); + if (!keys) + return; + for (i = 0; i < length; i++) + { + process(user_data, keys[i]); + } + g_strfreev(keys); +} + +int cbox_config_remove_section(const char *section) +{ + return 0 != g_key_file_remove_group(config_keyfile, section, NULL); +} + +int cbox_config_remove_key(const char *section, const char *key) +{ + return 0 != g_key_file_remove_key(config_keyfile, section, key, NULL); +} + +gboolean cbox_config_save(const char *filename, GError **error) +{ + gsize len = 0; + gchar *data = g_key_file_to_data(config_keyfile, &len, error); + if (!data) + return FALSE; + + if (filename == NULL) + filename = keyfile_name; + + gboolean ok = g_file_set_contents(filename, data, len, error); + g_free(data); + return ok; +} + +struct cbox_cfgfile +{ + gchar *libname; + gchar *filename; + GKeyFile *keyfile; +}; + +struct cbox_cfgfile *cbox_cfgfile_get_by_libname(const char *name) +{ + // XXXKF implement + return NULL; +} + +struct cbox_sectref +{ + struct cbox_cfgfile *cfgfile; + gchar *section; +}; + +static struct cbox_sectref *cbox_sectref_lookup(const char *name) +{ + struct cbox_sectref *sr = g_hash_table_lookup(config_sections_hash, name); + return sr; +} + +static void cbox_sectref_keep(struct cbox_sectref *sect) +{ + gchar *tmp = g_strdup_printf("%s@%s", sect->section, sect->cfgfile->libname); + g_hash_table_insert(config_sections_hash, tmp, sect); + g_free(tmp); +} + +static void cbox_sectref_set_from_string(struct cbox_sectref *sr, const gchar *refname, struct cbox_cfgfile *default_lib) +{ + const gchar *p = strchr(refname, '@'); + if (p) + { + sr->section = g_strndup(refname, p - refname); + sr->cfgfile = cbox_cfgfile_get_by_libname(p + 1); + } + else + { + sr->section = g_strndup(refname, p - refname); + sr->cfgfile = default_lib; + } +} + +// find the section 'prefix+refname.section' in the config file - either the one referenced by sect, or refname.file +struct cbox_sectref *cbox_config_sectref(struct cbox_sectref *sect, const char *prefix, const char *refname) +{ + if (!prefix) + prefix = ""; + gchar *tmpsect = NULL; + const char *p = strchr(refname, '@'); + if (p) + tmpsect = g_strdup_printf("%s%s", prefix, refname); + else + tmpsect = g_strdup_printf("%s%s@%s", prefix, refname, sect->cfgfile->libname); + struct cbox_sectref *sr = cbox_sectref_lookup(tmpsect); + if (sr) + { + g_free(tmpsect); + return sr; + } + sr = malloc(sizeof(struct cbox_sectref)); + cbox_sectref_set_from_string(sr, tmpsect, sect ? sect->cfgfile : NULL); + g_free(tmpsect); + cbox_sectref_keep(sr); + return sr; +} + +struct cbox_sectref *cbox_config_get_sectref(struct cbox_sectref *sect, const char *prefix, const char *key) +{ + if (!key || !sect) + return NULL; + //const char *sectname = cbox_config_get_string(sect, key); + const char *sectname = cbox_config_get_string(sect->section, key); + if (!sectname) + return NULL; + return cbox_config_sectref(sect, prefix, sectname); +} + +struct cbox_sectref *cbox_config_get_sectref_n(struct cbox_sectref *sect, const char *prefix, const char *key, int index) +{ + if (!key || !sect) + return NULL; + gchar *tmp = g_strdup_printf("%s%d", key, index); + struct cbox_sectref *sr = cbox_config_get_sectref(sect, prefix, tmp); + g_free(tmp); + return sr; +} + +struct cbox_sectref *cbox_config_get_sectref_suffix(struct cbox_sectref *sect, const char *prefix, const char *key, const char *suffix) +{ + if (!key || !sect) + return NULL; + gchar *tmp = g_strdup_printf("%s%s", key, suffix ? suffix : ""); + struct cbox_sectref *sr = cbox_config_get_sectref(sect, prefix, tmp); + g_free(tmp); + return sr; +} + +void cbox_config_close() +{ + if (config_sections_hash) + { + g_hash_table_destroy(config_sections_hash); + config_sections_hash = NULL; + } + if (config_keyfile) + { + g_key_file_free(config_keyfile); + config_keyfile = NULL; + } + if (cfg_strings) + { + g_string_chunk_free(cfg_strings); + cfg_strings = NULL; + } + if (keyfile_name) + { + g_free(keyfile_name); + keyfile_name = NULL; + } +} + diff --git a/template/calfbox/config-api.h b/template/calfbox/config-api.h new file mode 100644 index 0000000..d021cd5 --- /dev/null +++ b/template/calfbox/config-api.h @@ -0,0 +1,53 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_CONFIG_API_H +#define CBOX_CONFIG_API_H + +#include + +struct cbox_sectref; + +extern void cbox_config_init(const char *override_file); +extern int cbox_config_has_section(const char *section); +extern char *cbox_config_get_string(const char *section, const char *key); +extern char *cbox_config_get_string_with_default(const char *section, const char *key, char *def_value); +extern int cbox_config_get_int(const char *section, const char *key, int def_value); +extern float cbox_config_get_float(const char *section, const char *key, float def_value); +extern float cbox_config_get_gain(const char *section, const char *key, float def_value); +extern float cbox_config_get_gain_db(const char *section, const char *key, float def_value); +extern void cbox_config_foreach_section(void (*process)(void *user_data, const char *section), void *user_data); +extern void cbox_config_foreach_key(void (*process)(void *user_data, const char *key), const char *section, void *user_data); +extern char *cbox_config_permify(const char *temporary); + +extern void cbox_config_set_string(const char *section, const char *key, const char *value); +extern void cbox_config_set_int(const char *section, const char *key, int value); +extern void cbox_config_set_float(const char *section, const char *key, double value); +extern int cbox_config_remove_section(const char *section); +extern int cbox_config_remove_key(const char *section, const char *key); + +extern gboolean cbox_config_save(const char *filename, GError **error); + +extern struct cbox_sectref *cbox_config_sectref(struct cbox_sectref *def_sect, const char *prefix, const char *refname); +extern struct cbox_sectref *cbox_config_get_sectref(struct cbox_sectref *sect, const char *prefix, const char *key); +extern struct cbox_sectref *cbox_config_get_sectref_n(struct cbox_sectref *sect, const char *prefix, const char *key, int index); +extern struct cbox_sectref *cbox_config_get_sectref_suffix(struct cbox_sectref *sect, const char *prefix, const char *key, const char *suffix); + +extern void cbox_config_close(void); + +#endif diff --git a/template/calfbox/configure.ac b/template/calfbox/configure.ac new file mode 100644 index 0000000..d6bdbaa --- /dev/null +++ b/template/calfbox/configure.ac @@ -0,0 +1,160 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.63) +AC_INIT([calfbox],[0.0.3],[wdev@foltman.com]) +AC_CONFIG_HEADER([config.h]) +LT_INIT([]) +LT_LANG([C]) + +AM_INIT_AUTOMAKE(1.8) + +if test "x$prefix" = "xNONE"; then + prefix=$ac_default_prefix +fi + +# Checks for programs. +AC_PROG_CC_C99 +AC_PROG_INSTALL +PKG_PROG_PKG_CONFIG + +# Checks for headers. +AC_HEADER_STDC + +# Set initial parameters +PYTHON_ENABLED="yes" +NCURSES_ENABLED="yes" +JACK_ENABLED="yes" +FLUIDSYNTH_ENABLED="yes" +LIBSMF_ENABLED="yes" +LIBUSB_ENABLED="yes" +SSE_ENABLED="no" +NEON_ENABLED="no" + +# Check options + +AC_MSG_CHECKING([whether to enable Python embedding]) +AC_ARG_WITH(python, +AC_HELP_STRING([--without-python],[disable Python embedding]), + [if test "$withval" = "no"; then PYTHON_ENABLED="no"; fi],[]) +AC_MSG_RESULT($PYTHON_ENABLED) + +AC_MSG_CHECKING([whether to enable ncurses UI]) +AC_ARG_WITH(ncurses, +AC_HELP_STRING([--without-ncurses],[disable ncurses ui]), + [if test "$withval" = "no"; then NCURSES_ENABLED="no"; fi],[]) +AC_MSG_RESULT($NCURSES_ENABLED) + +AC_MSG_CHECKING([whether to enable JACK I/O]) +AC_ARG_WITH(jack, +AC_HELP_STRING([--without-jack],[disable JACK audio and MIDI]), + [if test "$withval" = "no"; then JACK_ENABLED="no"; fi],[]) +AC_MSG_RESULT($JACK_ENABLED) + +AC_MSG_CHECKING([whether to enable Fluidsynth]) +AC_ARG_WITH(fluidsynth, +AC_HELP_STRING([--without-fluidsynth],[disable use of Fluidsynth]), + [if test "$withval" = "no"; then FLUIDSYNTH_ENABLED="no"; fi],[]) +AC_MSG_RESULT($FLUIDSYNTH_ENABLED) + +AC_MSG_CHECKING([whether to enable libsmf]) +AC_ARG_WITH(libsmf, +AC_HELP_STRING([--without-libsmf],[disable use of libsmf]), + [if test "$withval" = "no"; then LIBSMF_ENABLED="no"; fi],[]) +AC_MSG_RESULT($LIBSMF_ENABLED) + +AC_MSG_CHECKING([whether to enable libusb]) +AC_ARG_WITH(libusb, +AC_HELP_STRING([--without-libusb],[disable use of libusb]), + [if test "$withval" = "no"; then LIBUSB_ENABLED="no"; fi],[]) +AC_MSG_RESULT($LIBUSB_ENABLED) + +AC_MSG_CHECKING([whether to enable SSE (x86 family only)]) +AC_ARG_WITH(sse, +AC_HELP_STRING([--with-sse],[enable use of SSE]), + [if test "$withval" = "yes"; then SSE_ENABLED="yes"; fi],[]) +AC_MSG_RESULT($SSE_ENABLED) + +AC_MSG_CHECKING([whether to enable NEON (ARM family only)]) +AC_ARG_WITH(neon, +AC_HELP_STRING([--with-neon],[enable use of NEON]), + [if test "$withval" = "yes"; then NEON_ENABLED="yes"; fi],[]) +AC_MSG_RESULT($NEON_ENABLED) + +# Check dependencies +AC_CHECK_HEADER(uuid/uuid.h, true, AC_MSG_ERROR([libuuid header (uuid/uuid.h) is required])) +AC_CHECK_LIB(uuid, uuid_unparse, true, AC_MSG_ERROR([libuuid is required])) +PKG_CHECK_MODULES(GLIB_DEPS, glib-2.0 >= 2.6, true, AC_MSG_ERROR([libglib-2.0 is required])) +if test "$LIBUSB_ENABLED" = "yes"; then + PKG_CHECK_MODULES(LIBUSB_DEPS, libusb-1.0 >= 1.0, true, AC_MSG_ERROR([libusb-1.0 is required])) +fi +PKG_CHECK_MODULES(LIBSNDFILE_DEPS, sndfile, true, AC_MSG_ERROR([libsndfile is required])) +if test "$NCURSES_ENABLED" = "yes"; then + PKG_CHECK_MODULES(NCURSES_DEPS, ncurses, true, AC_MSG_ERROR([libncurses is required])) +fi + +if test "$FLUIDSYNTH_ENABLED" = "yes"; then + PKG_CHECK_MODULES(FLUIDSYNTH_DEPS, fluidsynth >= 1.0.8, true, AC_MSG_ERROR([fluidsynth 1.0.8 is required])) +fi + +if test "$LIBSMF_ENABLED" = "yes"; then + PKG_CHECK_MODULES(LIBSMF_DEPS, smf >= 1.3, true, AC_MSG_ERROR([libsmf 1.3 is required (libsmf.sourceforge.net)])) +fi + +if test "$JACK_ENABLED" = "yes"; then + PKG_CHECK_MODULES(JACK_DEPS, jack >= 0.116.0, true, AC_MSG_ERROR([JACK is required (or use --without-jack)])) + AC_CHECK_HEADER(jack/jack.h, true, AC_MSG_ERROR([JACK is required (or use --without-jack)])) + PKG_CHECK_MODULES(JACK_RENAME_PORT, jack >= 0.124.2 jack < 1.9.0, JACK_HAS_RENAME="yes", JACK_HAS_RENAME="no") + PKG_CHECK_MODULES(JACK2_RENAME_PORT, jack >= 1.9.11, JACK_HAS_RENAME="yes", JACK_HAS_RENAME_DUMMY="no") +fi + +if test "$PYTHON_ENABLED" = "yes"; then + PKG_CHECK_MODULES(PYTHON_DEPS, python3 >= 3.0, true, AC_MSG_ERROR([python 3.0 or newer is required (or use --without-python)])) +fi + +# Generate Automake conditionals +AM_CONDITIONAL(USE_PYTHON, test "$PYTHON_ENABLED" = "yes") +AM_CONDITIONAL(USE_NCURSES, test "$NCURSES_ENABLED" = "yes") +AM_CONDITIONAL(USE_JACK, test "$JACK_ENABLED" = "yes") +AM_CONDITIONAL(USE_FLUIDSYNTH, test "$FLUIDSYNTH_ENABLED" = "yes") +AM_CONDITIONAL(USE_LIBSMF, test "$LIBSMF_ENABLED" = "yes") +AM_CONDITIONAL(USE_LIBUSB, test "$LIBUSB_ENABLED" = "yes") +AM_CONDITIONAL(USE_SSE, test "$SSE_ENABLED" = "yes") +AM_CONDITIONAL(USE_NEON, test "$NEON_ENABLED" = "yes") + +# Generate config.h conditionals +if test "$PYTHON_ENABLED" = "yes"; then + AC_DEFINE(USE_PYTHON, 1, [Python will be included]) +fi +if test "$NCURSES_ENABLED" = "yes"; then + AC_DEFINE(USE_NCURSES, 1, [ncurses will be included]) +fi +if test "$JACK_ENABLED" = "yes"; then + AC_DEFINE(USE_JACK, 1, [JACK I/O will be included]) +fi +if test "$JACK_HAS_RENAME" = "yes"; then + AC_DEFINE(JACK_HAS_RENAME, 1, [JACK function jack_port_rename should be used instead of jack_port_set_name]) +fi +if test "$FLUIDSYNTH_ENABLED" = "yes"; then + AC_DEFINE(USE_FLUIDSYNTH, 1, [Fluidsynth will be included]) +fi +if test "$LIBSMF_ENABLED" = "yes"; then + AC_DEFINE(USE_LIBSMF, 1, [libsmf will be used]) +fi +if test "$LIBUSB_ENABLED" = "yes"; then + AC_DEFINE(USE_LIBUSB, 1, [libusb will be used]) +fi +if test "$LIBUSB_ENABLED" = "no" && test "$JACK_ENABLED" = "no"; then + AC_MSG_ERROR([Neither JACK nor libusb are enabled, at least one is required]) +fi +if test "$SSE_ENABLED" = "yes"; then + AC_DEFINE(USE_SSE, 1, [x86 Streaming SIMD Extensions will be used]) +fi +if test "$NEON_ENABLED" = "yes"; then + AC_DEFINE(USE_NEON, 1, [ARM NEON SIMD Extensions will be used]) +fi + +# Generate files +AC_CONFIG_FILES([Makefile]) + +AC_OUTPUT diff --git a/template/calfbox/delay.c b/template/calfbox/delay.c new file mode 100644 index 0000000..c48ad7f --- /dev/null +++ b/template/calfbox/delay.c @@ -0,0 +1,139 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#define MAX_DELAY_LENGTH 65536 + +struct delay_params +{ + float time; + float wet_dry, fb_amt; +}; + +struct delay_module +{ + struct cbox_module module; + + float storage[MAX_DELAY_LENGTH][2]; + struct delay_params *params; + int pos; +}; + +#define MODULE_PARAMS delay_params + +MODULE_PROCESSCMD_FUNCTION(delay) +{ + struct delay_module *m = (struct delay_module *)ct->user_data; + + EFFECT_PARAM("/time", "f", time, double, , 1, 1000) else + EFFECT_PARAM("/fb_amt", "f", fb_amt, double, , 0, 1) else + EFFECT_PARAM("/wet_dry", "f", wet_dry, double, , 0, 1) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/time", "f", error, m->params->time) + && cbox_execute_on(fb, NULL, "/fb_amt", "f", error, m->params->fb_amt) + && cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry) + && CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error) + ; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void delay_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct delay_module *m = (struct delay_module *)module; +} + +void delay_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct delay_module *m = (struct delay_module *)module; + + int pos = m->pos; + int dv = m->params->time * m->module.srate / 1000.0; + float dryamt = 1 - m->params->wet_dry; + float wetamt = m->params->wet_dry; + float fbamt = m->params->fb_amt; + + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float dry[2] = { inputs[0][i], inputs[1][i] }; + float *delayed = &m->storage[pos & (MAX_DELAY_LENGTH - 1)][0]; + + float wet[2] = { dryamt * dry[0] + wetamt * delayed[0], dryamt * dry[1] + wetamt * delayed[1] }; + float fb[2] = { dry[0] + fbamt * delayed[0], dry[1] + fbamt * delayed[1] }; + outputs[0][i] = sanef(wet[0]); + outputs[1][i] = sanef(wet[1]); + + float *wcell = &m->storage[(pos + dv) & (MAX_DELAY_LENGTH - 1)][0]; + wcell[0] = sanef(fb[0]); + wcell[1] = sanef(fb[1]); + pos++; + } + m->pos = pos; +} + +MODULE_SIMPLE_DESTROY_FUNCTION(delay) + +MODULE_CREATE_FUNCTION(delay) +{ + static int inited = 0; + int i; + if (!inited) + { + inited = 1; + } + + struct delay_module *m = malloc(sizeof(struct delay_module)); + CALL_MODULE_INIT(m, 2, 2, delay); + struct delay_params *p = malloc(sizeof(struct delay_params)); + m->params = p; + m->module.process_event = delay_process_event; + m->module.process_block = delay_process_block; + m->pos = 0; + p->time = cbox_config_get_float(cfg_section, "delay", 250); + p->wet_dry = cbox_config_get_float(cfg_section, "wet_dry", 0.3); + p->fb_amt = cbox_config_get_gain_db(cfg_section, "feedback_gain", -12.f); + for (i = 0; i < MAX_DELAY_LENGTH; i++) + m->storage[i][0] = m->storage[i][1] = 0.f; + + return &m->module; +} + +struct cbox_module_keyrange_metadata delay_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata delay_controllers[] = { +}; + +DEFINE_MODULE(delay, 2, 2) + diff --git a/template/calfbox/distortion.c b/template/calfbox/distortion.c new file mode 100644 index 0000000..8251e89 --- /dev/null +++ b/template/calfbox/distortion.c @@ -0,0 +1,138 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS distortion_params + +struct distortion_params +{ + float drive; + float shape; +}; + +struct distortion_module +{ + struct cbox_module module; + + struct distortion_params *params, *old_params; +}; + +gboolean distortion_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct distortion_module *m = (struct distortion_module *)ct->user_data; + + EFFECT_PARAM("/drive", "f", drive, double, dB2gain_simple, -36, 36) else + EFFECT_PARAM("/shape", "f", shape, double, , -1, 2) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/drive", "f", error, gain2dB_simple(m->params->drive)) + && cbox_execute_on(fb, NULL, "/shape", "f", error, m->params->shape) + && CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error) + ; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void distortion_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct distortion_module *m = module->user_data; +} + +void distortion_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct distortion_module *m = module->user_data; + + if (m->params != m->old_params) + { + // update calculated values + } + + float drive = m->params->drive; + float shape = m->params->shape; + + float a0 = shape; + float a1 = -2 * shape - 0.5; + float a2 = 1.5 + shape; + + float post = pow(drive, -0.7); + + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + for (int c = 0; c < 2; c++) + { + float val = inputs[c][i]; + + val *= drive; + + if (fabs(val) > 1.0) + val = (val > 0) ? 1 : -1; + else + val = a0 * val * val * val * val * val + a1 * val * val * val + a2 * val; + + outputs[c][i] = val * post; + } + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION(distortion) + +MODULE_CREATE_FUNCTION(distortion) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct distortion_module *m = malloc(sizeof(struct distortion_module)); + CALL_MODULE_INIT(m, 2, 2, distortion); + m->module.process_event = distortion_process_event; + m->module.process_block = distortion_process_block; + struct distortion_params *p = malloc(sizeof(struct distortion_params)); + p->drive = cbox_config_get_gain_db(cfg_section, "drive", 0.f); + p->shape = cbox_config_get_gain_db(cfg_section, "shape", 0.f); + m->params = p; + m->old_params = NULL; + + return &m->module; +} + + +struct cbox_module_keyrange_metadata distortion_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata distortion_controllers[] = { +}; + +DEFINE_MODULE(distortion, 2, 2) + diff --git a/template/calfbox/dom.c b/template/calfbox/dom.c new file mode 100644 index 0000000..b512623 --- /dev/null +++ b/template/calfbox/dom.c @@ -0,0 +1,372 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2012 Krzysztof Foltman + +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 . +*/ + +#include "cmd.h" +#include "errors.h" +#include "dom.h" + +#include +#include +#include +#include + +static GHashTable *class_name_hash = NULL; + +struct cbox_class_per_document +{ + GList *instances; +}; + +struct cbox_document +{ + GHashTable *classes_per_document; + GHashTable *services_per_document; + GHashTable *uuids_per_document; + struct cbox_command_target cmd_target; + int item_ctr; + uint64_t generation_ctr; +}; + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_dom_init() +{ + class_name_hash = g_hash_table_new(g_str_hash, g_str_equal); +} + +void cbox_dom_close() +{ + g_hash_table_destroy(class_name_hash); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_class *cbox_class_find_by_name(const char *name) +{ + assert(class_name_hash != NULL); + return g_hash_table_lookup(class_name_hash, name); +} + +void cbox_class_register(struct cbox_class *class_ptr) +{ + assert(class_name_hash != NULL); + g_hash_table_insert(class_name_hash, (gpointer)class_ptr->name, class_ptr); +} + +static struct cbox_class_per_document *get_cpd_for_class(struct cbox_document *doc, struct cbox_class *class_ptr) +{ + struct cbox_class_per_document *p = g_hash_table_lookup(doc->classes_per_document, class_ptr); + if (p != NULL) + return p; + p = malloc(sizeof(struct cbox_class_per_document)); + p->instances = NULL; + g_hash_table_insert(doc->classes_per_document, class_ptr, p); + return p; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_uuid_clear(struct cbox_uuid *uuid) +{ + uuid_clear(uuid->uuid); +} + +guint cbox_uuid_hash(gconstpointer v) +{ + char buf[40]; + uuid_unparse_lower(((struct cbox_uuid *)v)->uuid, buf); + return g_str_hash(buf); +} + +gboolean cbox_uuid_equal(gconstpointer v1, gconstpointer v2) +{ + const struct cbox_uuid *p1 = v1; + const struct cbox_uuid *p2 = v2; + + return !uuid_compare(p1->uuid, p2->uuid); +} + +void cbox_uuid_copy(struct cbox_uuid *vto, const struct cbox_uuid *vfrom) +{ + uuid_copy(vto->uuid, vfrom->uuid); +} + +gboolean cbox_uuid_report_as(struct cbox_uuid *uuid, const char *cmd, struct cbox_command_target *fb, GError **error) +{ + if (!fb) + return TRUE; + return cbox_execute_on(fb, NULL, cmd, "u", error, uuid->uuid); +} + +gboolean cbox_uuid_report(struct cbox_uuid *uuid, struct cbox_command_target *fb, GError **error) +{ + return cbox_uuid_report_as(uuid, "/uuid", fb, error); +} + +gboolean cbox_uuid_fromstring(struct cbox_uuid *uuid, const char *str, GError **error) +{ + if (uuid_parse(str, uuid->uuid)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Malformed UUID: '%s'", str); + return FALSE; + } + return TRUE; +} + +void cbox_uuid_tostring(struct cbox_uuid *uuid, char str[40]) +{ + uuid_unparse(uuid->uuid, str); +} + +void cbox_uuid_generate(struct cbox_uuid *uuid) +{ + uuid_generate(uuid->uuid); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_object_register_instance(struct cbox_document *doc, struct cbox_objhdr *obj) +{ + assert(obj != NULL); + + struct cbox_class_per_document *cpd = get_cpd_for_class(doc, obj->class_ptr); + cpd->instances = g_list_prepend(cpd->instances, obj); + obj->owner = doc; + obj->link_in_document = cpd->instances; + g_hash_table_insert(obj->owner->uuids_per_document, &obj->instance_uuid, obj); +} + +struct cbox_command_target *cbox_object_get_cmd_target(struct cbox_objhdr *hdr_ptr) +{ + if (!hdr_ptr->class_ptr->getcmdtargetfunc) + return NULL; + return hdr_ptr->class_ptr->getcmdtargetfunc(hdr_ptr); +} + +gboolean cbox_object_try_default_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, gboolean *result, GError **error) +{ + // XXXKF this assumes objhdr ptr == object ptr - needs to add the header offset in cmd target? + struct cbox_objhdr *obj = ct->user_data; + if (!strcmp(subcmd, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_object_default_status(obj, fb, error)) + { + *result = FALSE; + return TRUE; + } + return FALSE; + } + if (!strcmp(subcmd, "/delete") && !strcmp(cmd->arg_types, "")) + { + cbox_object_destroy(obj); + *result = TRUE; + return TRUE; + } + if (!strcmp(subcmd, "/get_uuid") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + { + *result = FALSE; + return TRUE; + } + + *result = cbox_uuid_report(&obj->instance_uuid, fb, error); + return TRUE; + } + if (!strcmp(subcmd, "/get_class_name") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + { + *result = FALSE; + return TRUE; + } + *result = cbox_execute_on(fb, NULL, "/class_name", "s", error, obj->class_ptr->name); + return TRUE; + } + return FALSE; +} + +gboolean cbox_object_default_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + gboolean result = FALSE; + if (cbox_object_try_default_process_cmd(ct, fb, cmd, cmd->command, &result, error)) + return result; + struct cbox_objhdr *obj = ct->user_data; + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s' for object class '%s'", cmd->command, cmd->arg_types, obj->class_ptr->name); + return FALSE; +} + +gboolean cbox_object_default_status(struct cbox_objhdr *objhdr, struct cbox_command_target *fb, GError **error) +{ + char buf[40]; + uuid_unparse(objhdr->instance_uuid.uuid, buf); + return cbox_execute_on(fb, NULL, "/uuid", "s", error, buf); +} + +void cbox_object_destroy(struct cbox_objhdr *hdr_ptr) +{ + struct cbox_class_per_document *cpd = get_cpd_for_class(hdr_ptr->owner, hdr_ptr->class_ptr); + cpd->instances = g_list_delete_link(cpd->instances, hdr_ptr->link_in_document); + hdr_ptr->link_in_document = NULL; + g_hash_table_remove(hdr_ptr->owner->uuids_per_document, &hdr_ptr->instance_uuid); + + hdr_ptr->class_ptr->destroyfunc(hdr_ptr); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +static gboolean document_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + char *uuid; + const char *subcommand; + if (!strcmp(cmd->command, "/dump") && !strcmp(cmd->arg_types, "")) + { + struct cbox_document *doc = ct->user_data; + cbox_document_dump(doc); + return TRUE; + } + if (cbox_parse_path_part_str(cmd, "/uuid/", &subcommand, &uuid, error)) + { + struct cbox_document *doc = ct->user_data; + if (!subcommand) + return FALSE; + struct cbox_objhdr *obj = cbox_document_get_object_by_text_uuid(doc, uuid, NULL, error); + g_free(uuid); + if (!obj) + return FALSE; + struct cbox_command_target *ct2 = cbox_object_get_cmd_target(obj); + return cbox_execute_sub(ct2, fb, cmd, subcommand, error); + } + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return FALSE; +} + +struct cbox_document *cbox_document_new() +{ + struct cbox_document *res = malloc(sizeof(struct cbox_document)); + res->classes_per_document = g_hash_table_new_full(NULL, NULL, NULL, g_free); + res->services_per_document = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + res->uuids_per_document = g_hash_table_new(cbox_uuid_hash, cbox_uuid_equal); + res->cmd_target.process_cmd = document_process_cmd; + res->cmd_target.user_data = res; + res->item_ctr = 0; + res->generation_ctr = 1000; // start with non-zero value just to spot invalid values more easily + + return res; +} + +struct cbox_command_target *cbox_document_get_cmd_target(struct cbox_document *doc) +{ + return &doc->cmd_target; +} + +struct cbox_objhdr *cbox_document_get_service(struct cbox_document *document, const char *name) +{ + return g_hash_table_lookup(document->services_per_document, name); +} + +void cbox_document_set_service(struct cbox_document *document, const char *name, struct cbox_objhdr *obj) +{ + g_hash_table_insert(document->services_per_document, g_strdup(name), obj); +} + +struct cbox_objhdr *cbox_document_get_object_by_uuid(struct cbox_document *doc, const struct cbox_uuid *uuid) +{ + return g_hash_table_lookup(doc->uuids_per_document, uuid); +} + +struct cbox_objhdr *cbox_document_get_object_by_text_uuid(struct cbox_document *doc, const char *uuid, const struct cbox_class *class_ptr, GError **error) +{ + struct cbox_uuid uuidv; + if (!cbox_uuid_fromstring(&uuidv, uuid, error)) + return NULL; + struct cbox_objhdr *obj = cbox_document_get_object_by_uuid(doc, &uuidv); + if (!obj) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "UUID not found: '%s'", uuid); + return NULL; + } + if (class_ptr && !cbox_class_is_a(obj->class_ptr, class_ptr)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unexpected object type '%s' for UUID '%s' (expected '%s')", obj->class_ptr->name, uuid, class_ptr->name); + return NULL; + } + return obj; +} + +static void iter_func(gpointer key, gpointer value, gpointer doc_) +{ +#ifndef NDEBUG + struct cbox_document *doc = (struct cbox_document *)doc_; +#endif + struct cbox_class *class_ptr = key; + struct cbox_class_per_document *cpd = value; + int first = 1; + printf("Class %s: ", class_ptr->name); + GList *l = cpd->instances; + while(l) { + if (!first) + printf(", "); + printf("%p", l->data); + fflush(stdout); + struct cbox_objhdr *hdr = (struct cbox_objhdr *)l->data; + char buf[40]; + uuid_unparse(hdr->instance_uuid.uuid, buf); + printf("[%s]", buf); + fflush(stdout); + assert(cbox_document_get_object_by_uuid(doc, &hdr->instance_uuid)); + l = l->next; + first = 0; + } + if (first) + printf(""); + printf("\n"); +} + +static void iter_func2(gpointer key, gpointer value, gpointer document) +{ + struct cbox_objhdr *oh = value; + char buf[40]; + uuid_unparse(oh->instance_uuid.uuid, buf); + printf("Service %s: %p", (const char *)key, value); + fflush(stdout); + printf("[%s]", buf); + fflush(stdout); + printf(" (%s)\n", oh->class_ptr->name); +} + +void cbox_document_dump(struct cbox_document *document) +{ + g_hash_table_foreach(document->classes_per_document, iter_func, document); + g_hash_table_foreach(document->services_per_document, iter_func2, document); +} + +uint64_t cbox_document_get_next_stamp(struct cbox_document *document) +{ + return document->generation_ctr; +} + +void cbox_document_destroy(struct cbox_document *document) +{ + g_hash_table_destroy(document->classes_per_document); + g_hash_table_destroy(document->services_per_document); + g_hash_table_destroy(document->uuids_per_document); + free(document); +} + + diff --git a/template/calfbox/dom.h b/template/calfbox/dom.h new file mode 100644 index 0000000..4f270bf --- /dev/null +++ b/template/calfbox/dom.h @@ -0,0 +1,155 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2012 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_DOM_H +#define CBOX_DOM_H + +#include +#include +#include + +struct cbox_command_target; +struct cbox_osc_command; +struct cbox_objhdr; +struct cbox_document; +struct GList; + +struct cbox_uuid +{ + uuid_t uuid; +}; + +extern void cbox_uuid_clear(struct cbox_uuid *uuid); +extern guint cbox_uuid_hash(gconstpointer v); +extern void cbox_uuid_copy(struct cbox_uuid *vto, const struct cbox_uuid *vfrom); +extern gboolean cbox_uuid_equal(gconstpointer v1, gconstpointer v2); +extern gboolean cbox_uuid_report(struct cbox_uuid *uuid, struct cbox_command_target *fb, GError **error); +extern gboolean cbox_uuid_report_as(struct cbox_uuid *uuid, const char *cmd, struct cbox_command_target *fb, GError **error); +extern gboolean cbox_uuid_fromstring(struct cbox_uuid *uuid, const char *str, GError **error); +extern void cbox_uuid_tostring(struct cbox_uuid *uuid, char str[40]); +extern void cbox_uuid_generate(struct cbox_uuid *uuid); + +struct cbox_class +{ + struct cbox_class *parent; + const char *name; + int hdr_offset; + void (*destroyfunc)(struct cbox_objhdr *objhdr); + struct cbox_command_target *(*getcmdtargetfunc)(struct cbox_objhdr *objhdr); +}; + +extern struct cbox_class *cbox_class_find_by_name(const char *name); +extern void cbox_class_register(struct cbox_class *class_ptr); + +struct cbox_objhdr +{ + struct cbox_class *class_ptr; + struct cbox_document *owner; + void *link_in_document; + struct cbox_uuid instance_uuid; + uint64_t stamp; +}; + +static inline int cbox_class_is_a(const struct cbox_class *c1, const struct cbox_class *c2) +{ + while(c1 != c2 && c1->parent) + c1 = c1->parent; + return c1 == c2; +} + +extern void cbox_object_register_instance(struct cbox_document *doc, struct cbox_objhdr *obj); +extern struct cbox_command_target *cbox_object_get_cmd_target(struct cbox_objhdr *hdr_ptr); +extern gboolean cbox_object_try_default_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, gboolean *result, GError **error); +extern gboolean cbox_object_default_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); +extern gboolean cbox_object_default_status(struct cbox_objhdr *objhdr, struct cbox_command_target *fb, GError **error); + +extern void cbox_object_destroy(struct cbox_objhdr *hdr_ptr); + +extern struct cbox_document *cbox_document_new(void); +extern void cbox_document_dump(struct cbox_document *); +extern struct cbox_command_target *cbox_document_get_cmd_target(struct cbox_document *); +extern struct cbox_objhdr *cbox_document_get_service(struct cbox_document *doc, const char *name); +extern void cbox_document_set_service(struct cbox_document *doc, const char *name, struct cbox_objhdr *hdr_ptr); +extern struct cbox_objhdr *cbox_document_get_object_by_uuid(struct cbox_document *doc, const struct cbox_uuid *uuid); +extern struct cbox_objhdr *cbox_document_get_object_by_text_uuid(struct cbox_document *doc, const char *uuid, const struct cbox_class *class_ptr, GError **error); +extern uint64_t cbox_document_get_next_stamp(struct cbox_document *doc); +extern void cbox_document_destroy(struct cbox_document *); + +extern void cbox_dom_init(void); +extern void cbox_dom_close(void); + +// must be the first field in the object-compatible struct +#define CBOX_OBJECT_HEADER() \ + struct cbox_objhdr _obj_hdr; + +#define CBOX_CLASS(class) CBOX_CLASS_##class + +#define CBOX_EXTERN_CLASS(class) \ + extern struct cbox_class CBOX_CLASS(class); + +#define CBOX_GET_DOCUMENT(obj) \ + ((obj)->_obj_hdr.owner) + +#define CBOX_STAMP(obj) \ + ((obj)->_obj_hdr.stamp = cbox_document_get_next_stamp(CBOX_GET_DOCUMENT(obj))) + +#define CBOX_DELETE(obj) \ + ((obj) && (cbox_object_destroy(&(obj)->_obj_hdr), 1)) + +#define CBOX_IS_A(obj, class) \ + ((obj) && cbox_class_is_a((obj)->_obj_hdr.class_ptr, &CBOX_CLASS(class))) + +#define CBOX_OBJECT_HEADER_INIT(self, class, document) \ + do { \ + (self)->_obj_hdr.class_ptr = &CBOX_CLASS_##class; \ + (self)->_obj_hdr.owner = (document); \ + (self)->_obj_hdr.link_in_document = NULL; \ + (self)->_obj_hdr.stamp = cbox_document_get_next_stamp(document); \ + uuid_generate((self)->_obj_hdr.instance_uuid.uuid); \ + } while(0) + +#define CBOX_OBJECT_REGISTER(self) \ + (cbox_object_register_instance((self)->_obj_hdr.owner, &(self)->_obj_hdr)) + +#define CBOX_OBJECT_DEFAULT_STATUS(self, fb, error) \ + (cbox_object_default_status(&(self)->_obj_hdr, (fb), (error))) + +#define CBOX_CLASS_DEFINITION_ROOT(class) \ + static void class##_destroyfunc(struct cbox_objhdr *hdr_ptr); \ + static struct cbox_command_target *class##_getcmdtarget(struct cbox_objhdr *hdr) { \ + return &(((struct class *)hdr)->cmd_target);\ + }; \ + struct cbox_class CBOX_CLASS_##class = { \ + .parent = NULL, \ + .name = #class, \ + .hdr_offset = offsetof(struct class, _obj_hdr), \ + .destroyfunc = class##_destroyfunc, \ + .getcmdtargetfunc = class##_getcmdtarget \ + }; \ + +#define CBOX_RETURN_OBJECT(result) \ + return &(result)->_obj_hdr + +// Convert header to object, regardless of the relative position of the header. +#define CBOX_H2O(hdr) \ + (void *)(((char *)(hdr)) - (hdr)->class_ptr->hdr_offset) + +#define CBOX_O2H(obj) \ + (&(*(obj))._obj_hdr) + +#endif diff --git a/template/calfbox/drvjunk/miditest.py b/template/calfbox/drvjunk/miditest.py new file mode 100644 index 0000000..a670167 --- /dev/null +++ b/template/calfbox/drvjunk/miditest.py @@ -0,0 +1,152 @@ +import array +import binascii +import usb.core +import usb.util +import time + +class USBMIDIConfiguration: + def __init__(self, cfg, ifno, ifalt): + self.cfg = cfg + self.ifno = ifno + self.ifalt = ifalt + def __str__(self): + return "cfg=%d ifno=%d ifalt=%d" % (self.cfg, self.ifno, self.ifalt) + def __repr__(self): + return "USBMIDIConfiguration(%d,%d,%d)" % (self.cfg, self.ifno, self.ifalt) + +class USBMIDIDeviceDescriptor: + def __init__(self, vendorID, productID, interfaces = None): + self.vendorID = vendorID + self.productID = productID + if interfaces is None: + self.interfaces = [] + else: + self.interfaces = interfaces + def add_interface(self, config, ifno, ifalt): + self.interfaces.append(USBMIDIConfiguration(config, ifno, ifalt)) + def has_interfaces(self): + return len(self.interfaces) + def __str__(self): + return "vid=%04x pid=%04x" % (self.vendorID, self.productID) + def __repr__(self): + return "USBMIDIDeviceDescriptor(0x%04x, 0x%04x, %s)" % (self.vendorID, self.productID, self.interfaces) + +class USBMIDI: + cin_sizes = [None, None, 2, 3, 3, 1, 2, 3, 3, 3, 3, 3, 2, 2, 3, 1] + def __init__(self, mididev, midicfg, debug = False): + dev = usb.core.find(idVendor = mididev.vendorID, idProduct = mididev.productID) + self.dev = dev + intf = None + for cfgo in dev: + if cfgo.bConfigurationValue == midicfg.cfg: + cfgo.set() + intf = cfgo[(midicfg.ifno, midicfg.ifalt)] + if not intf: + raise ValueError, "Configuration %d not found" % midicfg.cfg + print intf.bNumEndpoints + self.epIn = None + self.epOut = None + for ep in intf: + if debug: + print "endpoint %x" % ep.bEndpointAddress + if ep.bEndpointAddress > 0x80: + if self.epIn is None: + self.epIn = ep + else: + if self.epOut is None: + self.epOut = ep + + def read(self): + try: + data = self.epIn.read(self.epIn.wMaxPacketSize) + if data is None: + return None + return array.array('B', data) + except usb.core.USBError, e: + return None + + def encode(self, port, msg): + a = array.array('B') + a.append(16 * port + (msg[0] >> 4)) + a.fromlist(msg) + return a + + def write(self, data): + self.epOut.write(data) + + def parse(self, data): + i = 0 + msgs = [] + while i < len(data): + if data[i] == 0: + break + cin, cable_id = data[i] & 15, data[i] >> 4 + msgs.append(data[i + 1 : i + 1 + KeyRig25.cin_sizes[cin]]) + i += 4 + return msgs + + @staticmethod + def findall(vendorID = None, productID = None, debug = False): + dev_list = [] + devices = usb.core.find(find_all = True) + for dev in devices: + if vendorID is not None and dev.idVendor != vendorID: + continue + if productID is not None and dev.idProduct != productID: + continue + thisdev = USBMIDIDeviceDescriptor(dev.idVendor, dev.idProduct) + if debug: + print "Device %04x:%04x, class %d" % (dev.idVendor, dev.idProduct, dev.bDeviceClass) + if dev.bDeviceClass == 0: # device defined at interface level + for cfg in dev: + if debug: + print "Configuration ", cfg.bConfigurationValue + for intf in cfg: + if debug: + print "Interface %d alternate-setting %d" % (intf.bInterfaceNumber, intf.bAlternateSetting) + print "Class %d subclass %d" % (intf.bInterfaceClass, intf.bInterfaceSubClass) + if intf.bInterfaceClass == 1 and intf.bInterfaceSubClass == 3: + if debug: + print "(%d,%d,%d): This is USB MIDI" % (cfg.bConfigurationValue, intf.bInterfaceNumber, intf.bAlternateSetting) + thisdev.add_interface(cfg.bConfigurationValue, intf.bInterfaceNumber, intf.bAlternateSetting) + if thisdev.has_interfaces(): + dev_list.append(thisdev) + return dev_list + + #print devices + +class KnownUSBMIDI(USBMIDI): + def __init__(self, vendorID, productID): + devlist = USBMIDI.findall(vendorID, productID, debug = False) + if not devlist: + raise ValueError + USBMIDI.__init__(self, devlist[0], devlist[0].interfaces[0]) + +class KeyRig25(KnownUSBMIDI): + def __init__(self): + KnownUSBMIDI.__init__(self, vendorID = 0x763, productID = 0x115) + +class XMidi2x2(KnownUSBMIDI): + def __init__(self): + KnownUSBMIDI.__init__(self, vendorID = 0x41e, productID = 0x3f08) + +class LexiconOmega(KnownUSBMIDI): + def __init__(self): + KnownUSBMIDI.__init__(self, vendorID = 0x1210, productID = 2) + +print USBMIDI.findall() +xmidi = XMidi2x2() +xmidi.write(xmidi.encode(1, [0x90, 36, 100])) +xmidi.write(xmidi.encode(1, [0x80, 36, 100])) + +#krig = KeyRig25() +krig = LexiconOmega() +while True: + data = krig.read() + if data is not None: + decoded = krig.parse(data) + reencoded = array.array('B') + for msg in decoded: + reencoded.extend(xmidi.encode(1, list(msg))) + xmidi.write(reencoded) + print decoded diff --git a/template/calfbox/drvjunk/omega.c b/template/calfbox/drvjunk/omega.c new file mode 100644 index 0000000..3611bb1 --- /dev/null +++ b/template/calfbox/drvjunk/omega.c @@ -0,0 +1,255 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// interface 1 altsetting 1 endpoint 01 (out) bits 16 channels 2 mps 192 +// interface 2 altsetting 1 endpoint 83 (in) bits 16 channels 2 mps 192 + +int samples_captured = 0; +int samples_played = 0; + +static struct libusb_context *usbctx; +static int omega_timeout = 1000; + +int epOUT = 0x01, epIN = 0x83; + +void init_usb() +{ + libusb_init(&usbctx); + libusb_set_debug(usbctx, 3); +} + +struct libusb_device_handle *open_omega() +{ + struct libusb_device_handle *handle; + handle = libusb_open_device_with_vid_pid(usbctx, 0x1210, 0x0002); + if (!handle) + { + printf("Lexicon Omega not found after reset.\n"); + return NULL; + } + if (libusb_set_configuration(handle, 1)) + { + libusb_close(handle); + return NULL; + } + + if (libusb_claim_interface(handle, 1)) + goto error; + if (libusb_claim_interface(handle, 2)) + goto error; + if (libusb_claim_interface(handle, 7)) + goto error; + if (libusb_set_interface_alt_setting(handle, 1, 1)) + goto error; + if (libusb_set_interface_alt_setting(handle, 2, 1)) + goto error; + + return handle; + +error: + libusb_close(handle); + return NULL; +} + +#define EP_CONTROL_UNDEFINED 0 +#define SAMPLING_FREQ_CONTROL 1 +#define PITCH_CONTROL 2 + +#define SET_CUR 0x01 +#define GET_CUR 0x81 +#define SET_MIN 0x02 +#define GET_MIN 0x82 +#define SET_MAX 0x03 +#define GET_MAX 0x83 +#define SET_RES 0x04 +#define GET_RES 0x84 +#define SET_MEM 0x05 +#define GET_MEM 0x85 +#define GET_STAT 0xFF + +int configure_omega(struct libusb_device_handle *h, int sample_rate) +{ + uint8_t freq_data[3]; + freq_data[0] = sample_rate & 0xFF; + freq_data[1] = (sample_rate & 0xFF00) >> 8; + freq_data[2] = (sample_rate & 0xFF0000) >> 16; + if (libusb_control_transfer(h, 0x22, 0x01, 256, epOUT, freq_data, 3, omega_timeout) != 3) + return -1; + if (libusb_control_transfer(h, 0x22, 0x01, 256, epIN, freq_data, 3, omega_timeout) != 3) + return -1; +// libusb_control_transfer(dev, 0x22, 0x01, + return 0; +} + +// 192 bytes = 1ms@48000 (48 samples) + +#define NUM_PLAY_BUFS 2 +#define PLAY_PACKET_COUNT 2 +#define PLAY_PACKET_SIZE 192 + +static float phase = 0; +static int phase2 = 0; + +static int desync = 0; +static int samples_sent = 0; + +static int srate = 48000; + +void play_callback(struct libusb_transfer *transfer) +{ + int i; + //printf("Play Callback! %d %p status %d\n", (int)transfer->length, transfer->buffer, (int)transfer->status); + + samples_played += transfer->length / 4; + int nsamps = srate / 1000; + if (desync >= 1000 * transfer->num_iso_packets && nsamps < PLAY_PACKET_SIZE/4) + nsamps++; + // printf("desync = %d nsamps = %d!\n", desync, nsamps); + int tlen = 0; + for (i = 0; i < transfer->num_iso_packets; i++) + { + tlen += transfer->iso_packet_desc[i].actual_length; + if (transfer->iso_packet_desc[i].status) + printf("ISO error: index = %d i = %d status = %d\n", (int)transfer->user_data, i, transfer->iso_packet_desc[i].status); + } + // printf("actual length = %d!\n", tlen); + + transfer->length = nsamps * transfer->num_iso_packets * 4; + libusb_set_iso_packet_lengths(transfer, nsamps * 4); + + desync += transfer->num_iso_packets * srate; + desync -= tlen / 4 * 1000; + + int16_t *data = (int16_t*)transfer->buffer; + for (i = 0; i < transfer->length / 4; i ++) + { + float v = 16000 * sin(phase); + //phase += (phase2 & 4096) ? 0.02 : 0.04; + phase += (phase2 & 16384) ? 0.04: 0.02; + //phase += 0.2 * cos(phase2 / 16384.0); + phase2++; + if (phase > 2 * M_PI) + phase -= 2 * M_PI; + int vi = (int)v; + data[i * 2] = vi; + data[i * 2 + 1] = vi; + } + libusb_submit_transfer(transfer); +} + +void play_stuff(struct libusb_device_handle *h, int index) +{ + struct libusb_transfer *t; + int i; + int packets = PLAY_PACKET_COUNT; + t = libusb_alloc_transfer(packets); + int tsize = srate * 4 / 1000; + uint8_t *buf = (uint8_t *)malloc(PLAY_PACKET_SIZE*packets); + //int ssf = 0; + + for (i = 0; i < tsize * packets; i++) + buf[i] = 0; + + libusb_fill_iso_transfer(t, h, epOUT, buf, tsize * packets, packets, play_callback, (void *)index, 20000); + libusb_set_iso_packet_lengths(t, tsize); + libusb_submit_transfer(t); +} + +#define NUM_RECORD_BUFS 2 +#define NUM_REC_PACKETS 2 +#define REC_PACKET_SIZE 192 + +static uint8_t *record_buffers[NUM_RECORD_BUFS]; +// struct libusb_transfer *record_transfers[NUM_RECORD_BUFS]; + +float filt = 0; + +void record_callback(struct libusb_transfer *transfer) +{ + int i; + // printf("Record callback! %p index %d len %d\n", transfer, (int)transfer->user_data, transfer->length); + samples_captured += transfer->length / 4; + + float avg = 0; + int16_t *bufz = (int16_t*)transfer->buffer; + int items = transfer->length / 4; + for (i = 0; i < items; i ++) + { + int16_t *buf = &bufz[i * 2]; + if (fabs(buf[0]) > avg) + avg = fabs(buf[0]); + if (fabs(buf[1]) > avg) + avg = fabs(buf[1]); + } + if (avg) + printf("%12.6f dBFS\r", 6 * log(avg / 32767 / items) / log(2.0)); + libusb_submit_transfer(transfer); +} + +void record_stuff(struct libusb_device_handle *h, int index) +{ + // 0x02 + struct libusb_transfer *t; + + record_buffers[index] = (uint8_t*)malloc(NUM_REC_PACKETS * REC_PACKET_SIZE); + memset(record_buffers[index], 0, NUM_REC_PACKETS * REC_PACKET_SIZE); + + t = libusb_alloc_transfer(NUM_REC_PACKETS); + libusb_fill_iso_transfer(t, h, epIN, record_buffers[index], NUM_REC_PACKETS * REC_PACKET_SIZE, NUM_REC_PACKETS, record_callback, (void *)index, 1000); + libusb_set_iso_packet_lengths(t, REC_PACKET_SIZE); + if (libusb_submit_transfer(t)) + goto error; + return; +error: + printf("Record setup failed for index=%d\n", index); +} + +void usb_main_loop() +{ + struct sched_param p; + p.sched_priority = 10; + sched_setscheduler(0, SCHED_FIFO, &p); + while(1) { + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 100 + }; + libusb_handle_events_timeout(usbctx, &tv); + } +} + +int main(int argc, char *argv[]) +{ + struct libusb_device_handle *h; + int i; + + init_usb(); + h = open_omega(); + if (!h) + { + printf("Lexicon Omega could not be opened.\n"); + return 2; + } + + // 10: 4 3 3 - 16 bit + // 30: 2 2 1 2 2 2 1 - 24 bit + // 50: 4 3 3 + // 70: 2 2 1 2 2 2 1 + printf("Error = %d\n", configure_omega(h, srate)); + usleep(1); + for (i = 0; i < NUM_PLAY_BUFS; i++) + play_stuff(h, i); + for (i = 0; i < NUM_RECORD_BUFS; i++) + record_stuff(h, i); + usb_main_loop(); + return 0; +} + diff --git a/template/calfbox/dspmath.h b/template/calfbox/dspmath.h new file mode 100644 index 0000000..81c7ed5 --- /dev/null +++ b/template/calfbox/dspmath.h @@ -0,0 +1,238 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ +#ifndef CBOX_DSPMATH_H +#define CBOX_DSPMATH_H + +#define CBOX_BLOCK_SIZE 16 + +#include +#include +#include +#include +#include + +#ifndef M_PI +#include +#define M_PI G_PI +#endif + +typedef float cbox_sample_t; + +struct cbox_sincos +{ + float sine; + float cosine; + float prewarp; + float prewarp2; +}; + +static inline float hz2w(float hz, float sr) +{ + return M_PI * hz / (2 * sr); +} + +static inline float cerp_naive(float v0, float v1, float v2, float v3, float f) +{ + float x0 = -1; + float x1 = 0; + float x2 = 1; + float x3 = 2; + + float l0 = ((f - x1) * (f - x2) * (f - x3)) / ( (x0 - x1) * (x0 - x2) * (x0 - x3)); + float l1 = ((f - x0) * (f - x2) * (f - x3)) / ((x1 - x0) * (x1 - x2) * (x1 - x3)); + float l2 = ((f - x0) * (f - x1) * (f - x3)) / ((x2 - x0) * (x2 - x1) * (x2 - x3)); + float l3 = ((f - x0) * (f - x1) * (f - x2)) / ((x3 - x0) * (x3 - x1) * (x3 - x2) ); + + return v0 * l0 + v1 * l1 + v2 * l2 + v3 * l3; +} + +static inline float cerp(float v0, float v1, float v2, float v3, float f) +{ + f += 1; + + float d0 = (f - 0); + float d1 = (f - 1); + float d2 = (f - 2); + float d3 = (f - 3); + + float d03 = (d0 * d3) * (1.0 / 2.0); + float d12 = (d03 + 1) * (1.0 / 3.0); + + float l0 = -d12 * d3; + float l1 = d03 * d2; + float l2 = -d03 * d1; + float l3 = d12 * d0; + + float y = v0 * l0 + v1 * l1 + v2 * l2 + v3 * l3; + // printf("%f\n", y - cerp_naive(v0, v1, v2, v3, f - 1)); + return y; +} + +static inline float sanef(float v) +{ + if (fabs(v) < (1.0 / (65536.0 * 65536.0))) + return 0; + return v; +} + +static inline void sanebf(float *buf) +{ + int i; + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + buf[i] = sanef(buf[i]); +} + +static inline void copybf(float *to, float *from) +{ + memcpy(to, from, sizeof(float) * CBOX_BLOCK_SIZE); +} + +static inline float cent2factor(float cent) +{ + return powf(2.0, cent * (1.f / 1200.f)); // I think this may be optimised using exp() +} + +static inline float dB2gain(float dB) +{ + return powf(2.f, dB * (1.f / 6.f)); +} + +static inline float dB2gain_simple(float dB) +{ + if (dB <= -96) + return 0; + return powf(2.f, dB * (1.f / 6.f)); +} + +static inline float gain2dB_simple(float gain) +{ + static const float sixoverlog2 = 8.656170245333781; // 6.0 / logf(2.f); + if (gain < (1.f / 65536.f)) + return -96.f; + return sixoverlog2 * logf(gain); +} + +static inline float deg2rad(float deg) +{ + return deg * (float)(M_PI / 180.f); +} + +static inline float rad2deg(float rad) +{ + return rad * (float)(180.f / M_PI); +} + +// Do a butterfly operation: +// dst1 = src1 + e^iw_1*src2 +// dst2 = src1 + e^iw_2*src2 (w = phase * 2pi / ANALYSIS_BUFFER_SIZE) +static inline void butterfly(complex float *dst1, complex float *dst2, complex float src1, complex float src2, complex float eiw1, complex float eiw2) +{ + *dst1 = src1 + eiw1 * src2; + *dst2 = src1 + eiw2 * src2; +} + +struct cbox_gain +{ + float db_gain; + float lin_gain; + float old_lin_gain; + float pos; + float delta; +}; + +static inline void cbox_gain_init(struct cbox_gain *gain) +{ + gain->db_gain = 0; + gain->lin_gain = 1; + gain->old_lin_gain = 1; + gain->pos = 1; + gain->delta = 1 / (44100 * 0.1); // XXXKF ballpark +} + +static inline void cbox_gain_set_db(struct cbox_gain *gain, float db) +{ + if (gain->db_gain == db) + return; + gain->db_gain = db; + gain->old_lin_gain = gain->old_lin_gain + (gain->lin_gain - gain->old_lin_gain) * gain->pos; + gain->lin_gain = dB2gain(db); + gain->pos = 0; +} + +#define CBOX_GAIN_APPLY_LOOP(gain, nsamples, code) \ +{ \ + double pos = (gain)->pos; \ + double span = (gain)->lin_gain - (gain)->old_lin_gain; \ + double start = (gain)->old_lin_gain; \ + double step = (gain)->delta; \ + if (pos >= 1) { \ + double tgain = gain->lin_gain; \ + for (uint32_t i = 0; i < (nsamples); ++i) { \ + code(i, tgain) \ + } \ + } else { \ + if (pos + (nsamples) * step < 1.0) { \ + for (uint32_t i = 0; i < (nsamples); ++i) { \ + double tgain = start + (pos + i * step) * span; \ + code(i, tgain) \ + } \ + gain->pos += (nsamples) * step; \ + } \ + else { \ + for (uint32_t i = 0; i < (nsamples); ++i) { \ + code(i, (start + pos * span)) \ + pos = (pos + step < 1.0 ? pos + step : 1.0); \ + } \ + gain->pos = 1.0; \ + } \ + } \ +} + +#define CBOX_GAIN_ADD_MONO(i, gain) \ + dest1[i] += src1[i] * gain; + +static inline void cbox_gain_add_mono(struct cbox_gain *gain, float *dest1, const float *src1, uint32_t nsamples) +{ + CBOX_GAIN_APPLY_LOOP(gain, nsamples, CBOX_GAIN_ADD_MONO); +} + +#define CBOX_GAIN_ADD_STEREO(i, gain) \ + dest1[i] += src1[i] * gain, dest2[i] += src2[i] * gain; + +static inline void cbox_gain_add_stereo(struct cbox_gain *gain, float *dest1, const float *src1, float *dest2, const float *src2, uint32_t nsamples) +{ + CBOX_GAIN_APPLY_LOOP(gain, nsamples, CBOX_GAIN_ADD_STEREO); +} + +#define CBOX_GAIN_COPY_MONO(i, gain) \ + dest1[i] = src1[i] * gain; + +static inline void cbox_gain_copy_mono(struct cbox_gain *gain, float *dest1, const float *src1, uint32_t nsamples) +{ + CBOX_GAIN_APPLY_LOOP(gain, nsamples, CBOX_GAIN_COPY_MONO); +} + +#define CBOX_GAIN_COPY_STEREO(i, gain) \ + dest1[i] = src1[i] * gain, dest2[i] = src2[i] * gain; + +static inline void cbox_gain_copy_stereo(struct cbox_gain *gain, float *dest1, const float *src1, float *dest2, const float *src2, uint32_t nsamples) +{ + CBOX_GAIN_APPLY_LOOP(gain, nsamples, CBOX_GAIN_COPY_STEREO); +} + +#endif \ No newline at end of file diff --git a/template/calfbox/engine.c b/template/calfbox/engine.c new file mode 100644 index 0000000..7d969d6 --- /dev/null +++ b/template/calfbox/engine.c @@ -0,0 +1,445 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "blob.h" +#include "dom.h" +#include "engine.h" +#include "instr.h" +#include "io.h" +#include "layer.h" +#include "midi.h" +#include "mididest.h" +#include "module.h" +#include "rt.h" +#include "scene.h" +#include "seq.h" +#include "song.h" +#include "stm.h" +#include "track.h" +#include +#include +#include + +CBOX_CLASS_DEFINITION_ROOT(cbox_engine) + +static gboolean cbox_engine_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +struct cbox_engine *cbox_engine_new(struct cbox_document *doc, struct cbox_rt *rt) +{ + struct cbox_engine *engine = malloc(sizeof(struct cbox_engine)); + CBOX_OBJECT_HEADER_INIT(engine, cbox_engine, doc); + + engine->rt = rt; + engine->scenes = NULL; + engine->scene_count = 0; + engine->effect = NULL; + engine->master = cbox_master_new(engine); + engine->master->song = cbox_song_new(doc); + engine->spb = NULL; + engine->spb_lock = 0; + engine->spb_retry = 0; + + if (rt) + cbox_io_env_copy(&engine->io_env, &rt->io_env); + else + { + engine->io_env.srate = 0; // must be overridden + engine->io_env.buffer_size = 256; + engine->io_env.input_count = 0; + engine->io_env.output_count = 2; + } + + cbox_midi_buffer_init(&engine->midibuf_aux); + cbox_midi_buffer_init(&engine->midibuf_jack); + cbox_midi_buffer_init(&engine->midibuf_song); + engine->stmap = malloc(sizeof(struct cbox_song_time_mapper)); + cbox_song_time_mapper_init(engine->stmap, engine); + cbox_midi_appsink_init(&engine->appsink, rt, &engine->stmap->tmap); + + cbox_command_target_init(&engine->cmd_target, cbox_engine_process_cmd, engine); + CBOX_OBJECT_REGISTER(engine); + + return engine; +} + +struct cbox_objhdr *cbox_engine_newfunc(struct cbox_class *class_ptr, struct cbox_document *doc) +{ + return NULL; +} + +void cbox_engine_destroyfunc(struct cbox_objhdr *obj_ptr) +{ + struct cbox_engine *engine = (struct cbox_engine *)obj_ptr; + while(engine->scene_count) + CBOX_DELETE(engine->scenes[0]); + if (engine->master->song) + { + CBOX_DELETE(engine->master->song); + engine->master->song = NULL; + } + cbox_master_destroy(engine->master); + engine->master = NULL; + free(engine->stmap); + engine->stmap = NULL; + + free(engine); +} + +static gboolean cbox_engine_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_engine *engine = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + for (uint32_t i = 0; i < engine->scene_count; i++) + { + if (!cbox_execute_on(fb, NULL, "/scene", "o", error, engine->scenes[i])) + return FALSE; + } + return CBOX_OBJECT_DEFAULT_STATUS(engine, fb, error); + } + else if (!strcmp(cmd->command, "/render_stereo") && !strcmp(cmd->arg_types, "i")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (engine->rt && engine->rt->io) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot use render function in real-time mode."); + return FALSE; + } + struct cbox_midi_buffer midibuf_song; + cbox_midi_buffer_init(&midibuf_song); + int nframes = CBOX_ARG_I(cmd, 0); + float *data = malloc(2 * nframes * sizeof(float)); + float *data_i = malloc(2 * nframes * sizeof(float)); + float *buffers[2] = { data, data + nframes }; + for (int i = 0; i < nframes; i++) + { + buffers[0][i] = 0.f; + buffers[1][i] = 0.f; + } + cbox_engine_process(engine, NULL, nframes, buffers, 2); + for (int i = 0; i < nframes; i++) + { + data_i[i * 2] = buffers[0][i]; + data_i[i * 2 + 1] = buffers[1][i]; + } + free(data); + + if (!cbox_execute_on(fb, NULL, "/data", "b", error, cbox_blob_new_acquire_data(data_i, nframes * 2 * sizeof(float)))) + return FALSE; + return TRUE; + } + else if (!strncmp(cmd->command, "/master_effect/",15)) + { + return cbox_module_slot_process_cmd(&engine->effect, fb, cmd, cmd->command + 14, CBOX_GET_DOCUMENT(engine), engine->rt, engine, error); + } + else if (!strcmp(cmd->command, "/new_scene") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_scene *s = cbox_scene_new(CBOX_GET_DOCUMENT(engine), engine); + + return s ? cbox_execute_on(fb, NULL, "/uuid", "o", error, s) : FALSE; + } + else if (!strcmp(cmd->command, "/new_recorder") && !strcmp(cmd->arg_types, "s")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_recorder *rec = cbox_recorder_new_stream(engine, engine->rt, CBOX_ARG_S(cmd, 0)); + + return rec ? cbox_execute_on(fb, NULL, "/uuid", "o", error, rec) : FALSE; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +void cbox_engine_process(struct cbox_engine *engine, struct cbox_io *io, uint32_t nframes, float **output_buffers, uint32_t output_channels) +{ + struct cbox_module *effect = engine->effect; + uint32_t i, j; + + cbox_midi_buffer_clear(&engine->midibuf_aux); + cbox_midi_buffer_clear(&engine->midibuf_song); + if (io) + cbox_io_get_midi_data(io, &engine->midibuf_jack); + else + cbox_midi_buffer_clear(&engine->midibuf_jack); + + // Copy MIDI input to the app-sink + cbox_midi_appsink_supply(&engine->appsink, &engine->midibuf_jack, io->free_running_frame_counter); + + // Clear external track outputs + if (engine->spb) + cbox_song_playback_prepare_render(engine->spb); + + if (engine->rt) + cbox_rt_handle_rt_commands(engine->rt); + + // Combine various sources of events (song, non-RT thread, JACK input) + if (engine->spb) + { + engine->frame_start_song_pos = engine->spb->song_pos_samples; + cbox_song_playback_render(engine->spb, &engine->midibuf_song, nframes); + } + + for (uint32_t i = 0; i < engine->scene_count; i++) + cbox_scene_render(engine->scenes[i], nframes, output_buffers, output_channels); + + // Process "master" effect + if (effect) + { + for (i = 0; i < nframes; i += CBOX_BLOCK_SIZE) + { + cbox_sample_t left[CBOX_BLOCK_SIZE], right[CBOX_BLOCK_SIZE]; + cbox_sample_t *in_bufs[2] = {output_buffers[0] + i, output_buffers[1] + i}; + cbox_sample_t *out_bufs[2] = {left, right}; + (*effect->process_block)(effect, in_bufs, out_bufs); + for (j = 0; j < CBOX_BLOCK_SIZE; j++) + { + output_buffers[0][i + j] = left[j]; + output_buffers[1][i + j] = right[j]; + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_engine_add_scene(struct cbox_engine *engine, struct cbox_scene *scene) +{ + assert(scene->engine == engine); + + cbox_rt_array_insert(engine->rt, (void ***)&engine->scenes, &engine->scene_count, -1, scene); +} + +void cbox_engine_remove_scene(struct cbox_engine *engine, struct cbox_scene *scene) +{ + assert(scene->engine == engine); + cbox_rt_array_remove_by_value(engine->rt, (void ***)&engine->scenes, &engine->scene_count, scene); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +#define cbox_engine_set_song_playback_args(ARG) ARG(struct cbox_song_playback *, old_song) ARG(struct cbox_song_playback *, new_song) ARG(uint32_t, new_time_ppqn) + +DEFINE_ASYNC_RT_FUNC(cbox_engine, engine, cbox_engine_set_song_playback) +{ + // If there's no new song, silence all ongoing notes. Otherwise, copy the + // ongoing notes to the new playback structure so that the notes get released + // when playback is stopped (or possibly earlier). + if (engine->spb) + { + if (new_song) + cbox_song_playback_apply_old_state(new_song); + + if (cbox_song_playback_active_notes_release(engine->spb, new_song, new_time_ppqn == (uint32_t)-1 ? old_song->song_pos_ppqn : new_time_ppqn, &engine->midibuf_aux) < 0) + { + RT_CALL_AGAIN_LATER(); + return; + } + } + engine->spb = new_song; + engine->master->spb = new_song; + if (new_song) + { + if (new_time_ppqn == (uint32_t)-1) + { + int old_time_ppqn = old_song ? old_song->song_pos_ppqn : 0; + cbox_song_playback_seek_samples(engine->master->spb, old_song ? old_song->song_pos_samples : 0); + // If tempo change occurred anywhere before playback point, then + // sample-based position corresponds to a completely different location. + // So, if new sample-based position corresponds to different PPQN + // position, seek again using PPQN position. + if (old_song && abs(new_song->song_pos_ppqn - old_time_ppqn) > 1) + cbox_song_playback_seek_ppqn(engine->master->spb, old_time_ppqn, FALSE); + } + else + cbox_song_playback_seek_ppqn(engine->master->spb, new_time_ppqn, FALSE); + } +} + +ASYNC_PREPARE_FUNC(cbox_engine, engine, cbox_engine_set_song_playback) +{ + // If update is already in progress, reschedule another at the end of it + if (engine->spb_lock) + { + engine->spb_retry = 1; + return 1; + } + ++engine->spb_lock; + args->old_song = engine->spb; + args->new_song = cbox_song_playback_new(engine->master->song, engine->master, engine, args->old_song); + + return 0; +} + +ASYNC_CLEANUP_FUNC(cbox_engine, engine, cbox_engine_set_song_playback) +{ + --engine->spb_lock; + assert(!engine->spb_lock); + if (args->old_song) + cbox_song_playback_destroy(args->old_song); + // If another update was requested while this one was in progress, repeat + // the operation + if (engine->spb_retry) { + engine->spb_retry = 0; + cbox_engine_set_song_playback(engine, NULL, NULL, args->new_time_ppqn); + } +} + +void cbox_engine_update_song(struct cbox_engine *engine, int new_pos_ppqn) +{ + cbox_engine_set_song_playback(engine, NULL, NULL, new_pos_ppqn); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_engine_update_song_playback(struct cbox_engine *engine) +{ + cbox_engine_update_song(engine, -1); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +uint32_t cbox_engine_current_pos_samples(struct cbox_engine *engine) +{ + uint32_t pos = engine->frame_start_song_pos + engine->song_pos_offset; + if (engine->spb && engine->spb->loop_start_ppqn < engine->spb->loop_end_ppqn) + pos = cbox_song_playback_correct_for_looping(engine->spb, pos); + return pos; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_engine_update_input_connections(struct cbox_engine *engine) +{ + for (uint32_t i = 0; i < engine->scene_count; i++) + cbox_scene_update_connected_inputs(engine->scenes[i]); +} + +void cbox_engine_update_output_connections(struct cbox_engine *engine) +{ + for (uint32_t i = 0; i < engine->scene_count; i++) + cbox_scene_update_connected_outputs(engine->scenes[i]); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_engine_send_events_to(struct cbox_engine *engine, struct cbox_midi_merger *merger, struct cbox_midi_buffer *buffer) +{ + if (!engine || !buffer) + return; + if (merger) + cbox_midi_merger_push(merger, buffer, engine->rt); + else + { + for (uint32_t i = 0; i < engine->scene_count; i++) + cbox_midi_merger_push(&engine->scenes[i]->scene_input_merger, buffer, engine->rt); + if (!engine->rt || !engine->rt->io) + return; + for (GSList *p = engine->rt->io->midi_outputs; p; p = p->next) + { + struct cbox_midi_output *midiout = p->data; + cbox_midi_merger_push(&midiout->merger, buffer, engine->rt); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_engine_on_tempo_sync(struct cbox_engine *engine, double beats_per_minute) +{ + if (!engine->master) + return; + if (beats_per_minute && beats_per_minute != engine->master->tempo && beats_per_minute != engine->master->new_tempo) { + engine->master->new_tempo = beats_per_minute; + } +} + +//////////////////////////////////////////////////////////////////////////////////////// + +gboolean cbox_engine_on_transport_sync(struct cbox_engine *engine, enum cbox_transport_state state, uint32_t frame) +{ + if (state == ts_stopping) + { + if (engine->master->state == CMTS_ROLLING) + engine->master->state = engine->spb ? CMTS_STOPPING : CMTS_STOP; + + return engine->master->state == CMTS_STOP; + } + if (state == ts_starting) + { + if (engine->master->state == CMTS_STOPPING) + return FALSE; + if (engine->master->state == CMTS_ROLLING) + { + if (engine->spb->song_pos_samples == frame) + return TRUE; + engine->master->state = CMTS_STOPPING; + return FALSE; + } + if (engine->spb && engine->spb->song_pos_samples != frame) + { + cbox_song_playback_seek_samples(engine->spb, frame); + } + engine->frame_start_song_pos = frame; + return TRUE; + } + if (state == ts_rolling) + { + // When starting with JACK transport rolling, there is no + // ts_starting message in first place (because there can't be without + // interfering with other applications). Seek immediately. + if (engine->spb && engine->spb->song_pos_samples != frame) + { + cbox_song_playback_seek_samples(engine->spb, frame); + } + else + engine->frame_start_song_pos = frame; + engine->master->state = CMTS_ROLLING; + return TRUE; + } + if (state == ts_stopped) + { + if (engine->master->state == CMTS_ROLLING) + engine->master->state = CMTS_STOPPING; + + if (engine->master->state == CMTS_STOP && engine->spb && engine->spb->song_pos_samples != frame) + { + cbox_song_playback_seek_samples(engine->spb, frame); + } + + return engine->master->state == CMTS_STOP; + } + return TRUE; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_merger *cbox_engine_get_midi_output(struct cbox_engine *engine, struct cbox_uuid *uuid) +{ + struct cbox_objhdr *objhdr = cbox_document_get_object_by_uuid(CBOX_GET_DOCUMENT(engine), uuid); + if (!objhdr) + return NULL; + struct cbox_scene *scene = (struct cbox_scene *)objhdr; + if (!CBOX_IS_A(scene, cbox_scene)) + return NULL; + return &scene->scene_input_merger; +} diff --git a/template/calfbox/engine.h b/template/calfbox/engine.h new file mode 100644 index 0000000..d9b560c --- /dev/null +++ b/template/calfbox/engine.h @@ -0,0 +1,72 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_ENGINE_H +#define CBOX_ENGINE_H + +#include "cmd.h" +#include "dom.h" +#include "io.h" +#include "midi.h" +#include "rt.h" + +CBOX_EXTERN_CLASS(cbox_engine) + +#define GET_RT_FROM_cbox_engine(ptr) ((ptr)->rt) + +struct cbox_engine +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + struct cbox_io_env io_env; + struct cbox_rt *rt; + struct cbox_scene **scenes; + uint32_t scene_count; + struct cbox_song_playback *spb; + struct cbox_module *effect; + struct cbox_master *master; + struct cbox_midi_buffer midibuf_aux, midibuf_jack, midibuf_song; + struct cbox_song_time_mapper *stmap; + struct cbox_midi_appsink appsink; + + int spb_lock, spb_retry; + + uint32_t frame_start_song_pos, song_pos_offset; // samples +}; + +// These use an RT command internally +extern struct cbox_engine *cbox_engine_new(struct cbox_document *doc, struct cbox_rt *rt); +extern void cbox_engine_update_song_playback(struct cbox_engine *engine); +extern void cbox_engine_update_input_connections(struct cbox_engine *engine); +extern void cbox_engine_update_output_connections(struct cbox_engine *engine); +extern void cbox_engine_add_scene(struct cbox_engine *engine, struct cbox_scene *scene); +void cbox_engine_remove_scene(struct cbox_engine *engine, struct cbox_scene *scene); +extern struct cbox_song *cbox_engine_set_song(struct cbox_engine *engine, struct cbox_song *song, int new_pos); +extern struct cbox_song *cbox_engine_set_pattern(struct cbox_engine *engine, struct cbox_midi_pattern *pattern, int new_pos); +extern void cbox_engine_set_pattern_and_destroy(struct cbox_engine *engine, struct cbox_midi_pattern *pattern); +extern void cbox_engine_send_events_to(struct cbox_engine *engine, struct cbox_midi_merger *merger, struct cbox_midi_buffer *buffer); +extern void cbox_engine_process(struct cbox_engine *engine, struct cbox_io *io, uint32_t nframes, float **output_buffers, uint32_t output_channels); +extern gboolean cbox_engine_on_transport_sync(struct cbox_engine *engine, enum cbox_transport_state state, uint32_t frame); +extern void cbox_engine_on_tempo_sync(struct cbox_engine *engine, double beats_per_minute); +extern struct cbox_midi_merger *cbox_engine_get_midi_output(struct cbox_engine *engine, struct cbox_uuid *uuid); +extern uint32_t cbox_engine_current_pos_samples(struct cbox_engine *engine); + +extern int cbox_engine_get_sample_rate(struct cbox_engine *engine); +extern int cbox_engine_get_buffer_size(struct cbox_engine *engine); + +#endif diff --git a/template/calfbox/envelope.h b/template/calfbox/envelope.h new file mode 100644 index 0000000..af26dae --- /dev/null +++ b/template/calfbox/envelope.h @@ -0,0 +1,324 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_ENVELOPE_H +#define CBOX_ENVELOPE_H + +#include +#include + +struct cbox_envstage +{ + double end_value; + int time; + int next_if_pressed, next_if_released, keep_last_value, break_on_release, is_exp; +}; + +#define MAX_ENV_STAGES 16 +#define EXP_NOISE_FLOOR (100.0 / 16384.0) + +struct cbox_envelope_shape +{ + double start_value; + struct cbox_envstage stages[MAX_ENV_STAGES]; +}; + +struct cbox_envelope +{ + struct cbox_envelope_shape *shape; + double stage_start_value, cur_value, exp_factor, inv_time, cur_time, orig_time, orig_target; + int cur_stage; +}; + +static inline void cbox_envelope_init_stage(struct cbox_envelope *env) +{ + struct cbox_envstage *es = &env->shape->stages[env->cur_stage]; + env->orig_time = es->time; + env->orig_target = es->end_value; + env->inv_time = es->time > 0 ? 1.0 / es->time : 1e6; + if (es->is_exp) + { + if (env->stage_start_value < EXP_NOISE_FLOOR) + env->stage_start_value = EXP_NOISE_FLOOR; + double ev = es->end_value; + if (ev < EXP_NOISE_FLOOR) + ev = EXP_NOISE_FLOOR; + env->exp_factor = log(ev / env->stage_start_value); + } +} + +static inline void cbox_envelope_go_to(struct cbox_envelope *env, int stage) +{ + env->stage_start_value = env->cur_value; + env->cur_stage = stage; + env->cur_time = 0; + cbox_envelope_init_stage(env); +} + +static inline void cbox_envelope_reset(struct cbox_envelope *env) +{ + env->cur_value = 0; + env->cur_stage = 0; + env->cur_time = 0; + cbox_envelope_init_stage(env); +} + +static inline void cbox_envelope_update_shape(struct cbox_envelope *env, struct cbox_envelope_shape *shape) +{ + struct cbox_envelope_shape *old_shape = env->shape; + env->shape = shape; + if (env->cur_stage < 0) + return; + struct cbox_envstage *ns = &env->shape->stages[env->cur_stage]; + struct cbox_envstage *os = &old_shape->stages[env->cur_stage]; + if (os->time > 0) + env->cur_time = env->cur_time * ns->time / os->time; + if (env->cur_time > ns->time) + env->cur_time = ns->time; +} + +static inline float cbox_envelope_get_value(struct cbox_envelope *env, const struct cbox_envelope_shape *shape) +{ + if (env->cur_stage < 0) + return env->cur_value; + const struct cbox_envstage *es = &shape->stages[env->cur_stage]; + double pos = es->time > 0 ? env->cur_time * env->inv_time : 0; + if (pos > 1) + pos = 1; + if (es->is_exp) + { + // instead of exp, may use 2**x which can be factored + // into a shift and a table lookup + env->cur_value = env->stage_start_value * expf(pos * env->exp_factor); + if (env->cur_value <= EXP_NOISE_FLOOR) + env->cur_value = 0; + } + else + env->cur_value = env->stage_start_value + (es->end_value - env->stage_start_value) * pos; + return env->cur_value; +} + +#define DEBUG_UPDATE_SHAPE(...) + +static inline void cbox_envelope_update_shape_after_modify(struct cbox_envelope *env, struct cbox_envelope_shape *shape, double sr) +{ + if (env->cur_stage < 0) + return; + struct cbox_envstage *es = &shape->stages[env->cur_stage]; + if (es->time != env->orig_time) + { + // Scale cur_time to reflect the same relative position within the stage + env->cur_time = env->cur_time * es->time / (env->orig_time > 0 ? env->orig_time : 1); + env->orig_time = es->time; + env->inv_time = es->time > 0 ? 1.0 / es->time : 1e6; + } + if (es->end_value != env->orig_target) + { + // Adjust the start value to keep the current value intact given the change in the slope + double pos = es->time > 0 ? env->cur_time * env->inv_time : 1; + if (pos < 1) + { + if (es->is_exp) + env->stage_start_value /= pow(es->end_value / (env->orig_target >= EXP_NOISE_FLOOR ? env->orig_target : EXP_NOISE_FLOOR), pos / (1 - pos)); // untested, likely never used + else + env->stage_start_value -= (es->end_value - env->orig_target) * pos / (1 - pos); + } + env->orig_target = es->end_value; + } +} + +static inline void cbox_envelope_advance(struct cbox_envelope *env, int released, const struct cbox_envelope_shape *shape) +{ + if (env->cur_stage < 0) + return; + const struct cbox_envstage *es = &shape->stages[env->cur_stage]; + double pos = es->time > 0 ? env->cur_time * env->inv_time : 1; + env->cur_time++; + if (pos >= 1 || (es->break_on_release && released)) + { + int next_stage = released ? es->next_if_released : es->next_if_pressed; + if (!es->keep_last_value || pos >= 1 || (es->keep_last_value == 2 && !released) || next_stage == env->cur_stage) + env->stage_start_value = es->end_value; + else + env->stage_start_value = env->cur_value; + env->cur_stage = next_stage; + env->cur_time = 0; + cbox_envelope_init_stage(env); + } +} + +struct cbox_adsr +{ + float attack; + float decay; + float sustain; + float release; +}; + +static inline void cbox_envelope_init_adsr(struct cbox_envelope_shape *env, const struct cbox_adsr *adsr, int sr) +{ + env->start_value = 0; + env->stages[0].end_value = 1; + env->stages[0].time = adsr->attack * sr; + env->stages[0].next_if_pressed = 1; + env->stages[0].next_if_released = 3; + env->stages[0].keep_last_value = 1; + env->stages[0].break_on_release = 0; + env->stages[0].is_exp = 0; + + env->stages[1].end_value = adsr->sustain; + env->stages[1].time = adsr->decay * sr; + env->stages[1].next_if_pressed = 2; + env->stages[1].next_if_released = 3; + env->stages[1].keep_last_value = 1; + env->stages[1].break_on_release = 0; + env->stages[1].is_exp = 0; + + env->stages[2].end_value = adsr->sustain; + env->stages[2].time = 1 * sr; + env->stages[2].next_if_pressed = 2; + env->stages[2].next_if_released = 3; + env->stages[2].keep_last_value = 0; + env->stages[2].break_on_release = 1; + env->stages[2].is_exp = 0; + + env->stages[3].end_value = 0; + env->stages[3].time = adsr->release * sr; + env->stages[3].next_if_pressed = -1; + env->stages[3].next_if_released = -1; + env->stages[3].keep_last_value = 0; + env->stages[3].break_on_release = 0; + env->stages[3].is_exp = 1; + + env->stages[15].end_value = 0; + env->stages[15].time = 0.01 * sr; + env->stages[15].next_if_pressed = -1; + env->stages[15].next_if_released = -1; + env->stages[15].keep_last_value = 0; + env->stages[15].break_on_release = 0; + env->stages[15].is_exp = 0; +} + +struct cbox_dahdsr +{ + float start; + float delay; + float attack; + float hold; + float decay; + float sustain; + float release; +}; + +static inline void cbox_dahdsr_init(struct cbox_dahdsr *dahdsr, float top_value) +{ + dahdsr->start = 0.f; + dahdsr->delay = 0.f; + dahdsr->attack = 0.f; + dahdsr->hold = 0.f; + dahdsr->decay = 0.f; + dahdsr->sustain = top_value; + dahdsr->release = 0.05f; +} + +static inline void cbox_envelope_init_dahdsr(struct cbox_envelope_shape *env, const struct cbox_dahdsr *dahdsr, int sr, float top_value, gboolean is_release_exp) +{ + env->start_value = dahdsr->start; + env->stages[0].end_value = dahdsr->start; + env->stages[0].time = dahdsr->delay * sr; + env->stages[0].next_if_pressed = 1; + env->stages[0].next_if_released = 5; + env->stages[0].keep_last_value = 1; + env->stages[0].break_on_release = 0; + env->stages[0].is_exp = 0; + + env->stages[1].end_value = top_value; + env->stages[1].time = dahdsr->attack * sr; + env->stages[1].next_if_pressed = 2; + env->stages[1].next_if_released = 5; + env->stages[1].keep_last_value = 2; + env->stages[1].break_on_release = 1; + env->stages[1].is_exp = 0; + + env->stages[2].end_value = top_value; + env->stages[2].time = dahdsr->hold * sr; + env->stages[2].next_if_pressed = 3; + env->stages[2].next_if_released = 5; + env->stages[2].keep_last_value = 2; + env->stages[2].break_on_release = 1; + env->stages[2].is_exp = 0; + + env->stages[3].end_value = dahdsr->sustain; + env->stages[3].time = dahdsr->decay * sr; + env->stages[3].next_if_pressed = 4; + env->stages[3].next_if_released = 5; + env->stages[3].keep_last_value = 1; + env->stages[3].break_on_release = 1; + env->stages[3].is_exp = 0; + + env->stages[4].end_value = dahdsr->sustain; + env->stages[4].time = 1 * sr; + env->stages[4].next_if_pressed = 4; + env->stages[4].next_if_released = 5; + env->stages[4].keep_last_value = 1; + env->stages[4].break_on_release = 1; + env->stages[4].is_exp = 0; + + env->stages[5].end_value = 0; + env->stages[5].time = dahdsr->release * sr; + env->stages[5].next_if_pressed = -1; + env->stages[5].next_if_released = -1; + env->stages[5].keep_last_value = 0; + env->stages[5].break_on_release = 0; + env->stages[5].is_exp = is_release_exp; + + env->stages[15].end_value = 0; + env->stages[15].time = 0.01 * sr; + env->stages[15].next_if_pressed = -1; + env->stages[15].next_if_released = -1; + env->stages[15].keep_last_value = 0; + env->stages[15].break_on_release = 0; + env->stages[15].is_exp = 0; +} + +static inline void cbox_envelope_modify_dahdsr(struct cbox_envelope_shape *env, int part, float value, int sr) +{ + switch(part) + { + case 0: // delay + case 1: // attack + case 2: // hold + case 3: // decay + case 5: // release + env->stages[part].time += value * sr; + // Allow negative times (deal with them in get_next) to make multiple signed modulations work correctly + break; + case 4: // sustain + env->stages[3].end_value += value; + env->stages[4].end_value += value; + env->stages[4].time = 0.02 * sr; // more rapid transition + break; + case 6: // start + env->stages[0].end_value += value; + env->start_value += value; + break; + } +} + + +#endif diff --git a/template/calfbox/eq.c b/template/calfbox/eq.c new file mode 100644 index 0000000..f53c7ae --- /dev/null +++ b/template/calfbox/eq.c @@ -0,0 +1,194 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "biquad-float.h" +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "eq.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS parametric_eq_params +#define MAX_EQ_BANDS 4 + +struct parametric_eq_params +{ + struct eq_band bands[MAX_EQ_BANDS]; +}; + +struct parametric_eq_module +{ + struct cbox_module module; + + struct parametric_eq_params *params, *old_params; + + struct cbox_biquadf_state state[MAX_EQ_BANDS][2]; + struct cbox_biquadf_coeffs coeffs[MAX_EQ_BANDS]; +}; + +static void redo_filters(struct parametric_eq_module *m) +{ + for (int i = 0; i < MAX_EQ_BANDS; i++) + { + struct eq_band *band = &m->params->bands[i]; + if (band->active) + { + cbox_biquadf_set_peakeq_rbj(&m->coeffs[i], band->center, band->q, band->gain, m->module.srate); + } + } + m->old_params = m->params; +} + +gboolean parametric_eq_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct parametric_eq_module *m = (struct parametric_eq_module *)ct->user_data; + + EFFECT_PARAM_ARRAY("/active", "i", bands, active, int, , 0, 1) else + EFFECT_PARAM_ARRAY("/center", "f", bands, center, double, , 10, 20000) else + EFFECT_PARAM_ARRAY("/q", "f", bands, q, double, , 0.01, 100) else + EFFECT_PARAM_ARRAY("/gain", "f", bands, gain, double, dB2gain_simple, -100, 100) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + for (int i = 0; i < MAX_EQ_BANDS; i++) + { + if (!cbox_execute_on(fb, NULL, "/active", "ii", error, i, (int)m->params->bands[i].active)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/center", "if", error, i, m->params->bands[i].center)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/q", "if", error, i, m->params->bands[i].q)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/gain", "if", error, i, gain2dB_simple(m->params->bands[i].gain))) + return FALSE; + } + // return cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry); + return CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void parametric_eq_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct parametric_eq_module *m = (struct parametric_eq_module *)module; +} + +void parametric_eq_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct parametric_eq_module *m = (struct parametric_eq_module *)module; + + if (m->params != m->old_params) + redo_filters(m); + + for (int c = 0; c < 2; c++) + { + gboolean first = TRUE; + for (int i = 0; i < MAX_EQ_BANDS; i++) + { + if (!m->params->bands[i].active) + continue; + if (first) + { + cbox_biquadf_process_to(&m->state[i][c], &m->coeffs[i], inputs[c], outputs[c]); + first = FALSE; + } + else + { + cbox_biquadf_process(&m->state[i][c], &m->coeffs[i], outputs[c]); + } + } + if (first) + memcpy(outputs[c], inputs[c], sizeof(float) * CBOX_BLOCK_SIZE); + } +} + +float cbox_eq_get_band_param(const char *cfg_section, int band, const char *param, float defvalue) +{ + gchar *s = g_strdup_printf("band%d_%s", band + 1, param); + float v = cbox_config_get_float(cfg_section, s, defvalue); + g_free(s); + + return v; +} + +float cbox_eq_get_band_param_db(const char *cfg_section, int band, const char *param, float defvalue) +{ + gchar *s = g_strdup_printf("band%d_%s", band + 1, param); + float v = cbox_config_get_gain_db(cfg_section, s, defvalue); + g_free(s); + + return v; +} + +void cbox_eq_reset_bands(struct cbox_biquadf_state state[1][2], int bands) +{ + for (int b = 0; b < MAX_EQ_BANDS; b++) + for (int c = 0; c < 2; c++) + cbox_biquadf_reset(&state[b][c]); +} + +MODULE_SIMPLE_DESTROY_FUNCTION(parametric_eq) + +MODULE_CREATE_FUNCTION(parametric_eq) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct parametric_eq_module *m = malloc(sizeof(struct parametric_eq_module)); + CALL_MODULE_INIT(m, 2, 2, parametric_eq); + m->module.process_event = parametric_eq_process_event; + m->module.process_block = parametric_eq_process_block; + struct parametric_eq_params *p = malloc(sizeof(struct parametric_eq_params)); + m->params = p; + m->old_params = NULL; + + for (int b = 0; b < MAX_EQ_BANDS; b++) + { + p->bands[b].active = cbox_eq_get_band_param(cfg_section, b, "active", 0) > 0; + p->bands[b].center = cbox_eq_get_band_param(cfg_section, b, "center", 50 * pow(4.0, b)); + p->bands[b].q = cbox_eq_get_band_param(cfg_section, b, "q", 0.707); + p->bands[b].gain = cbox_eq_get_band_param_db(cfg_section, b, "gain", 0); + } + + cbox_eq_reset_bands(m->state, MAX_EQ_BANDS); + + return &m->module; +} + + +struct cbox_module_keyrange_metadata parametric_eq_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata parametric_eq_controllers[] = { +}; + +DEFINE_MODULE(parametric_eq, 2, 2) + diff --git a/template/calfbox/eq.h b/template/calfbox/eq.h new file mode 100644 index 0000000..2a5e070 --- /dev/null +++ b/template/calfbox/eq.h @@ -0,0 +1,31 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include + +struct eq_band +{ + gboolean active; + float center; + float q; + float gain; +}; + +extern float cbox_eq_get_band_param(const char *cfg_section, int band, const char *param, float defvalue); +extern float cbox_eq_get_band_param_db(const char *cfg_section, int band, const char *param, float defvalue); +extern void cbox_eq_reset_bands(struct cbox_biquadf_state (*state)[2], int bands); diff --git a/template/calfbox/errors.c b/template/calfbox/errors.c new file mode 100644 index 0000000..0fec3e0 --- /dev/null +++ b/template/calfbox/errors.c @@ -0,0 +1,70 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "errors.h" + +GQuark cbox_module_error_quark() +{ + return g_quark_from_string("cbox-module-error-quark"); +} + +void cbox_force_error(GError **error) +{ + if (error && !*error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "unknown error"); +} + +void cbox_print_error(GError *error) +{ + if (!error) + { + g_warning("Unspecified error"); + return; + } + g_warning("%s", error->message); + g_error_free(error); +} + +void cbox_print_error_if(GError *error) +{ + if (!error) + return; + g_warning("%s", error->message); + g_error_free(error); +} + +gboolean cbox_set_command_error(GError **error, const struct cbox_osc_command *cmd) +{ + if (error && !*error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_OUT_OF_RANGE, "Invalid command '%s' with args '%s'", cmd->command, cmd->arg_types); + return FALSE; +} + +gboolean cbox_set_command_error_with_msg(GError **error, const struct cbox_osc_command *cmd, const char *extra_msg) +{ + if (error && !*error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_OUT_OF_RANGE, "Invalid command '%s' with args '%s': %s", cmd->command, cmd->arg_types, extra_msg); + return FALSE; +} + +gboolean cbox_set_range_error(GError **error, const char *param, double minv, double maxv) +{ + if (error && !*error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_OUT_OF_RANGE, "Parameter %s not within a valid range of [%f, %f]", param, minv, maxv); + return FALSE; +} diff --git a/template/calfbox/errors.h b/template/calfbox/errors.h new file mode 100644 index 0000000..ae8206d --- /dev/null +++ b/template/calfbox/errors.h @@ -0,0 +1,44 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_ERRORS_H +#define CBOX_ERRORS_H + +#include +#include "cmd.h" + +#define CBOX_MODULE_ERROR cbox_module_error_quark() + +enum CboxModuleError +{ + CBOX_MODULE_ERROR_FAILED, + CBOX_MODULE_ERROR_INVALID_COMMAND, + CBOX_MODULE_ERROR_OUT_OF_RANGE, +}; + +struct cbox_osc_command; + +extern GQuark cbox_module_error_quark(void); +extern void cbox_force_error(GError **error); +extern void cbox_print_error(GError *error); +extern void cbox_print_error_if(GError *error); +extern gboolean cbox_set_command_error(GError **error, const struct cbox_osc_command *cmd); +extern gboolean cbox_set_command_error_with_msg(GError **error, const struct cbox_osc_command *cmd, const char *extra_msg); +extern gboolean cbox_set_range_error(GError **error, const char *param, double minv, double maxv); + +#endif diff --git a/template/calfbox/example.py b/template/calfbox/example.py new file mode 100644 index 0000000..c50284c --- /dev/null +++ b/template/calfbox/example.py @@ -0,0 +1,554 @@ +import sys +sys.argv = [] +import gi +gi.require_version('Gdk', '3.0') +gi.require_version('Gtk', '3.0') +from gi.repository import GObject, Gdk, Gtk +import math + +sys.path = ["./py"] + sys.path + +import cbox +from gui_tools import * +import fx_gui +import instr_gui +import drumkit_editor +#import drum_pattern_editor + +class SceneDialog(SelectObjectDialog): + title = "Select a scene" + def __init__(self, parent): + SelectObjectDialog.__init__(self, parent=parent) + def update_model(self, model): + for s in cbox.Config.sections("scene:"): + title = s["title"] + model.append((s.name[6:], "Scene", s.name, title)) + for s in cbox.Config.sections("instrument:"): + title = s["title"] + model.append((s.name[11:], "Instrument", s.name, title)) + for s in cbox.Config.sections("layer:"): + title = s["title"] + model.append((s.name[6:], "Layer", s.name, title)) + +class NewLayerDialog(SelectObjectDialog): + title = "Create a layer" + def __init__(self, parent): + SelectObjectDialog.__init__(self, parent=parent) + def update_model(self, model): + for engine_name, wclass in instr_gui.instrument_window_map.items(): + model.append((engine_name, "Engine", engine_name, "")) + +class LoadLayerDialog(SelectObjectDialog): + title = "Load a layer" + def __init__(self, parent): + SelectObjectDialog.__init__(self, parent=parent) + def update_model(self, model): + for s in cbox.Config.sections("instrument:"): + title = s["title"] + model.append((s.name[11:], "Instrument", s.name, title)) + for s in cbox.Config.sections("layer:"): + title = s["title"] + model.append((s.name[6:], "Layer", s.name, title)) + +class PlayPatternDialog(SelectObjectDialog): + title = "Play a drum pattern" + def __init__(self, parent): + SelectObjectDialog.__init__(self, parent) + def update_model(self, model): + model.append((None, "Stop", "", "")) + for s in cbox.Config.sections("drumpattern:"): + title = s["title"] + model.append((s.name[12:], "Pattern", s.name, title)) + for s in cbox.Config.sections("drumtrack:"): + title = s["title"] + model.append((s.name[10:], "Track", s.name, title)) + +in_channels_ls = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) +in_channels_ls.append((0, "All")) +out_channels_ls = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) +out_channels_ls.append((0, "Same")) +for i in range(1, 17): + in_channels_ls.append((i, str(i))) + out_channels_ls.append((i, str(i))) +notes_ls = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) +opt_notes_ls = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) +opt_notes_ls.append((-1, "N/A")) +for i in range(0, 128): + notes_ls.append((i, note_to_name(i))) + opt_notes_ls.append((i, note_to_name(i))) +transpose_ls = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) +for i in range(-60, 61): + transpose_ls.append((i, str(i))) + +class SceneLayersModel(Gtk.ListStore): + def __init__(self): + Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_BOOLEAN, + GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_BOOLEAN, + GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_STRING) + #def make_row_item(self, opath, tree_path): + # return opath % self[(1 + int(tree_path))] + def make_row_item(self, opath, tree_path): + return cbox.Document.uuid_cmd(self[int(tree_path)][-1], opath) + def refresh(self, scene_status): + self.clear() + for layer in scene_status.layers: + ls = layer.status() + self.append((ls.instrument_name, ls.enable != 0, ls.in_channel, ls.out_channel, ls.consume != 0, ls.low_note, ls.high_note, ls.fixed_note, ls.transpose, layer.uuid)) + +class SceneLayersView(Gtk.TreeView): + def __init__(self, model): + Gtk.TreeView.__init__(self, model=model) + self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [("text/plain", 0, 1)], Gdk.DragAction.MOVE) + self.enable_model_drag_dest([("text/plain", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 1)], Gdk.DragAction.MOVE) + self.connect('drag_data_get', self.drag_data_get) + self.connect('drag_data_received', self.drag_data_received) + self.insert_column_with_attributes(0, "On?", standard_toggle_renderer(model, "/enable", 1), active=1) + self.insert_column_with_attributes(1, "Name", Gtk.CellRendererText(), text=0) + self.insert_column_with_data_func(2, "In Ch#", standard_combo_renderer(model, in_channels_ls, "/in_channel", 2), lambda column, cell, model, iter, data: cell.set_property('text', "%s" % model[iter][2] if model[iter][2] != 0 else 'All'), None) + self.insert_column_with_data_func(3, "Out Ch#", standard_combo_renderer(model, out_channels_ls, "/out_channel", 3), lambda column, cell, model, iter, data: cell.set_property('text', "%s" % model[iter][3] if model[iter][3] != 0 else 'Same'), None) + self.insert_column_with_attributes(4, "Eat?", standard_toggle_renderer(model, "/consume", 4), active=4) + self.insert_column_with_data_func(5, "Lo N#", standard_combo_renderer(model, notes_ls, "/low_note", 5), lambda column, cell, model, iter, data: cell.set_property('text', note_to_name(model[iter][5])), None) + self.insert_column_with_data_func(6, "Hi N#", standard_combo_renderer(model, notes_ls, "/high_note", 6), lambda column, cell, model, iter, data: cell.set_property('text', note_to_name(model[iter][6])), None) + self.insert_column_with_data_func(7, "Fix N#", standard_combo_renderer(model, opt_notes_ls, "/fixed_note", 7), lambda column, cell, model, iter, data: cell.set_property('text', note_to_name(model[iter][7])), None) + self.insert_column_with_attributes(8, "Transpose", standard_combo_renderer(model, transpose_ls, "/transpose", 8), text=8) + def drag_data_get(self, treeview, context, selection, target_id, etime): + cursor = treeview.get_cursor() + if cursor is not None: + selection.set('text/plain', 8, str(cursor[0][0])) + def drag_data_received(self, treeview, context, x, y, selection, info, etime): + src_row = int(selection.data) + dest_row_info = treeview.get_dest_row_at_pos(x, y) + if dest_row_info is not None: + dest_row = dest_row_info[0][0] + #print src_row, dest_row, dest_row_info[1] + scene = cbox.Document.get_scene() + scene.move_layer(src_row, dest_row) + self.get_model().refresh(scene.status()) + +class SceneAuxBusesModel(Gtk.ListStore): + def __init__(self): + Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING) + def refresh(self, scene_status): + self.clear() + for aux_name, aux_obj in scene_status.auxes.items(): + slot = aux_obj.slot.status() + self.append((slot.insert_preset, slot.insert_engine)) + +class SceneAuxBusesView(Gtk.TreeView): + def __init__(self, model): + Gtk.TreeView.__init__(self, model=model) + self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=0) + self.insert_column_with_attributes(1, "Engine", Gtk.CellRendererText(), text=1) + def get_current_row(self): + if self.get_cursor()[0] is None: + return None, None + row = self.get_cursor()[0][0] + return row + 1, self.get_model()[row] + +class StatusBar(Gtk.Statusbar): + def __init__(self): + Gtk.Statusbar.__init__(self) + self.sample_rate_label = Gtk.Label(label="") + self.pack_start(self.sample_rate_label, False, False, 2) + self.status = self.get_context_id("Status") + self.sample_rate = self.get_context_id("Sample rate") + self.push(self.status, "") + self.push(self.sample_rate, "-") + def update(self, status, sample_rate): + self.pop(self.status) + self.push(self.status, status) + self.sample_rate_label.set_text("%s Hz" % sample_rate) + +class MainWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self, type = Gtk.WindowType.TOPLEVEL) + self.vbox = Gtk.VBox(spacing = 5) + self.add(self.vbox) + self.create() + set_timer(self, 30, self.update) + #self.drum_pattern_editor = None + self.drumkit_editor = None + + def create(self): + self.menu_bar = Gtk.MenuBar() + + self.menu_bar.append(create_menu("_Scene", [ + ("_Load", self.load_scene), + ("_Quit", self.quit), + ])) + self.menu_bar.append(create_menu("_Layer", [ + ("_New", self.layer_new), + ("_Load", self.layer_load), + ("_Remove", self.layer_remove), + ])) + self.menu_bar.append(create_menu("_AuxBus", [ + ("_Add", self.aux_bus_add), + ("_Edit", self.aux_bus_edit), + ("_Remove", self.aux_bus_remove), + ])) + self.menu_bar.append(create_menu("_Tools", [ + ("_Drum Kit Editor", self.tools_drumkit_editor), + ("_Play Drum Pattern", self.tools_play_drum_pattern), + #("_Edit Drum Pattern", self.tools_drum_pattern_editor), + ("_Un-zombify", self.tools_unzombify), + ("_Object list", self.tools_object_list), + ("_Wave bank dump", self.tools_wave_bank_dump), + ])) + + self.vbox.pack_start(self.menu_bar, False, False, 0) + rt_status = cbox.Document.get_rt().status() + scene = cbox.Document.get_scene() + self.nb = Gtk.Notebook() + self.vbox.add(self.nb) + self.nb.append_page(self.create_master(scene), Gtk.Label(label="Master")) + self.status_bar = StatusBar() + self.vbox.pack_start(self.status_bar, False, False, 0) + self.create_instrument_pages(scene.status(), rt_status) + + def create_master(self, scene): + scene_status = scene.status() + self.master_info = left_label("") + self.timesig_info = left_label("") + + t = Gtk.Grid() + t.set_column_spacing(5) + t.set_row_spacing(5) + + t.attach(bold_label("Scene"), 0, 0, 1, 1) + self.scene_label = left_label(scene_status.name) + t.attach(self.scene_label, 1, 0, 2, 1) + + self.title_label = left_label(scene_status.title) + t.attach(bold_label("Title"), 0, 1, 1, 1) + t.attach(self.title_label, 1, 1, 2, 1) + + t.attach(bold_label("Play pos"), 0, 2, 1, 1) + t.attach(self.master_info, 1, 2, 2, 1) + + t.attach(bold_label("Time sig"), 0, 3, 1, 1) + t.attach(self.timesig_info, 1, 3, 2, 1) + hb = Gtk.HButtonBox() + b = Gtk.Button(label="Play") + b.connect('clicked', lambda w: cbox.Transport.play()) + hb.pack_start(b, False, False, 5) + b = Gtk.Button(label="Stop") + b.connect('clicked', lambda w: cbox.Transport.stop()) + hb.pack_start(b, False, False, 5) + b = Gtk.Button(label="Rewind") + b.connect('clicked', lambda w: cbox.Transport.seek_ppqn(0)) + hb.pack_start(b, False, False, 5) + b = Gtk.Button(label="Panic") + b.connect('clicked', lambda w: cbox.Transport.panic()) + hb.pack_start(b, False, False, 5) + t.attach(hb, 2, 3, 1, 1) + + t.attach(bold_label("Tempo"), 0, 4, 1, 1) + self.tempo_adj = Gtk.Adjustment(value=40, lower=40, upper=300, step_increment=1, page_increment=5, page_size=0) + self.tempo_adj.connect('value_changed', adjustment_changed_float, cbox.VarPath("/master/set_tempo")) + t.attach(standard_hslider(self.tempo_adj), 1, 4, 2, 1) + + t.attach(bold_label("Transpose"), 0, 5, 1, 1) + self.transpose_adj = Gtk.Adjustment(value=scene_status.transpose, lower=-24, upper=24, step_increment=1, page_increment=5, page_size=0) + self.transpose_adj.connect('value_changed', adjustment_changed_int, cbox.VarPath('/scene/transpose')) + t.attach(standard_align(Gtk.SpinButton(adjustment = self.transpose_adj), 0, 0, 0, 0), 1, 5, 2, 1) + + self.layers_model = SceneLayersModel() + self.layers_view = SceneLayersView(self.layers_model) + t.attach(standard_vscroll_window(-1, 160, self.layers_view), 0, 7, 3, 1) + + self.auxes_model = SceneAuxBusesModel() + self.auxes_view = SceneAuxBusesView(self.auxes_model) + t.attach(standard_vscroll_window(-1, 120, self.auxes_view), 0, 8, 3, 1) + + me = cbox.Document.get_engine().master_effect + me_status = me.status() + + hb = Gtk.HBox(spacing = 5) + self.master_chooser = fx_gui.InsertEffectChooser(me.path, "slot", me_status.insert_engine, me_status.insert_preset, me_status.bypass, self) + hb.pack_start(self.master_chooser.fx_engine, True, True, 0) + hb.pack_start(self.master_chooser.fx_preset, True, True, 5) + hb.pack_start(self.master_chooser.fx_edit, False, False, 5) + hb.pack_start(self.master_chooser.fx_bypass, False, False, 5) + + t.attach(bold_label("Master effect"), 0, 6, 1, 1) + t.attach(standard_align(hb, 0, 0, 0, 0), 1, 6, 3, 1) + + self.layers_model.refresh(scene_status) + self.auxes_model.refresh(scene_status) + + return t + + def quit(self, w): + self.destroy() + + def load_scene(self, w): + d = SceneDialog(self) + response = d.run() + try: + if response == Gtk.ResponseType.OK: + scene = cbox.Document.get_scene() + item_name, item_type, item_key, item_label = d.get_selected_object() + if item_type == 'Scene': + scene.load(item_name) + elif item_type == 'Layer': + scene.clear() + scene.add_layer(item_name) + elif item_type == 'Instrument': + scene.clear() + scene.add_instrument_layer(item_name) + scene_status = scene.status() + self.scene_label.set_text(scene_status.name) + self.title_label.set_text(scene_status.title) + self.refresh_instrument_pages(scene_status) + finally: + d.destroy() + + def layer_load(self, w): + d = LoadLayerDialog(self) + response = d.run() + try: + if response == Gtk.ResponseType.OK: + scene = cbox.Document.get_scene() + item_name, item_type, item_key, item_label = d.get_selected_object() + if item_type == 'Layer': + scene.add_layer(item_name) + elif item_type == 'Instrument': + scene.add_instrument_layer(item_name) + self.refresh_instrument_pages() + finally: + d.destroy() + + def layer_new(self, w): + d = NewLayerDialog(self) + response = d.run() + try: + if response == Gtk.ResponseType.OK: + scene = cbox.Document.get_scene() + keys = scene.status().instruments.keys() + engine_name = d.get_selected_object()[0] + for i in range(1, 1001): + name = "%s%s" % (engine_name, i) + if name not in keys: + break + scene.add_new_instrument_layer(name, engine_name) + self.refresh_instrument_pages() + finally: + d.destroy() + + def layer_remove(self, w): + if self.layers_view.get_cursor()[0] is not None: + pos = self.layers_view.get_cursor()[0][0] + cbox.Document.get_scene().delete_layer(pos) + self.refresh_instrument_pages() + + def aux_bus_add(self, w): + d = fx_gui.LoadEffectDialog(self) + response = d.run() + try: + cbox.do_cmd("/scene/load_aux", None, [d.get_selected_object()[0]]) + self.refresh_instrument_pages() + finally: + d.destroy() + def aux_bus_remove(self, w): + rowid, row = self.auxes_view.get_current_row() + if rowid is None: + return + cbox.do_cmd("/scene/delete_aux", None, [row[0]]) + self.refresh_instrument_pages() + + def aux_bus_edit(self, w): + rowid, row = self.auxes_view.get_current_row() + if rowid is None: + return + wclass = fx_gui.effect_window_map[row[1]] + popup = wclass("Aux: %s" % row[0], self, "/scene/aux/%s/slot/engine" % row[0]) + popup.show_all() + popup.present() + + def tools_unzombify(self, w): + cbox.do_cmd("/rt/cycle", None, []) + + def tools_drumkit_editor(self, w): + if self.drumkit_editor is None: + self.drumkit_editor = drumkit_editor.EditorDialog(self) + self.refresh_instrument_pages() + self.drumkit_editor.connect('destroy', self.on_drumkit_editor_destroy) + self.drumkit_editor.show_all() + self.drumkit_editor.present() + + def on_drumkit_editor_destroy(self, w): + self.drumkit_editor = None + + def tools_object_list(self, w): + cbox.Document.dump() + + def tools_wave_bank_dump(self, w): + for w in cbox.get_thing('/waves/list', '/waveform', [str]): + info = cbox.GetThings("/waves/info", ["filename", "name", "bytes", "loop"], [w]) + print("%s: %d bytes, loop = %s" % (info.filename, info.bytes, info.loop)) + + def tools_play_drum_pattern(self, w): + d = PlayPatternDialog(self) + response = d.run() + try: + if response == Gtk.ResponseType.OK: + row = d.get_selected_object() + if row[1] == 'Pattern': + song = cbox.Document().get_song() + song.loop_single_pattern(lambda: song.load_drum_pattern(row[0])) + elif row[1] == 'Track': + song = cbox.Document().get_song() + song.loop_single_pattern(lambda: song.load_drum_track(row[0])) + elif row[1] == 'Stop': + song = cbox.Document().get_song() + song.clear() + song.update_playback() + tracks = song.status().tracks + if len(tracks): + for track_item in tracks: + track_item.track.set_external_output(cbox.Document.get_scene().uuid) + song.update_playback() + + finally: + d.destroy() + + def tools_drum_pattern_editor(self, w): + if self.drum_pattern_editor is None: + length = drum_pattern_editor.PPQN * 4 + pat_data = cbox.Pattern.get_pattern() + if pat_data is not None: + pat_data, length = pat_data + self.drum_pattern_editor = drum_pattern_editor.DrumSeqWindow(length, pat_data) + self.drum_pattern_editor.set_title("Drum pattern editor") + self.drum_pattern_editor.show_all() + self.drum_pattern_editor.connect('destroy', self.on_drum_pattern_editor_destroy) + self.drum_pattern_editor.pattern.connect('changed', self.on_drum_pattern_changed) + self.drum_pattern_editor.pattern.changed() + self.drum_pattern_editor.present() + + def on_drum_pattern_changed(self, pattern): + data = bytearray() + for i in pattern.items(): + ch = i.channel - 1 + data += cbox.Pattern.serialize_event(int(i.pos), 0x90 + ch, int(i.row), int(i.vel)) + if i.len > 1: + data += cbox.Pattern.serialize_event(int(i.pos + i.len - 1), 0x80 + ch, int(i.row), int(i.vel)) + else: + data += cbox.Pattern.serialize_event(int(i.pos + 1), 0x80 + ch, int(i.row), int(i.vel)) + + length = pattern.get_length() + + song = cbox.Document().get_song() + song.loop_single_pattern(lambda: song.pattern_from_blob(data, length)) + + def on_drum_pattern_editor_destroy(self, w): + self.drum_pattern_editor = None + + def refresh_instrument_pages(self, scene_status = None): + self.delete_instrument_pages() + rt_status = cbox.Document.get_rt().status() + if scene_status is None: + scene_status = cbox.Document.get_scene().status() + self.layers_model.refresh(scene_status) + self.auxes_model.refresh(scene_status) + self.create_instrument_pages(scene_status, rt_status) + self.nb.show_all() + self.title_label.set_text(scene_status.title) + + def create_instrument_pages(self, scene_status, rt_status): + self.path_widgets = {} + self.path_popups = {} + self.fx_choosers = {} + + outputs_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT) + for out in range(0, rt_status.audio_channels[1]//2): + outputs_ls.append(("Out %s/%s" % (out * 2 + 1, out * 2 + 2), out)) + + auxbus_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING) + auxbus_ls.append(("", "")) + for bus_name in scene_status.auxes.keys(): + auxbus_ls.append(("Aux: %s" % bus_name, bus_name)) + + for iname, (iengine, iobj) in scene_status.instruments.items(): + ipath = "/scene/instr/%s" % iname + idata = iobj.status() + #attribs = cbox.GetThings("/scene/instr_info", ['engine', 'name'], [i]) + #markup += 'Instrument %d: engine %s, name %s\n' % (i, attribs.engine, attribs.name) + b = Gtk.VBox(spacing = 5) + b.set_border_width(5) + b.pack_start(Gtk.Label(label="Engine: %s" % iengine), False, False, 5) + b.pack_start(Gtk.HSeparator(), False, False, 5) + t = Gtk.Table(n_rows=1 + idata.outputs, n_columns=7) + t.attach(bold_label("Instr. output", 0.5), 0, 1, 0, 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + t.attach(bold_label("Send to", 0.5), 1, 2, 0, 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + t.attach(bold_label("Gain [dB]", 0.5), 2, 3, 0, 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(bold_label("Effect", 0.5), 3, 4, 0, 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(bold_label("Preset", 0.5), 4, 7, 0, 1, 0, Gtk.AttachOptions.SHRINK) + b.pack_start(t, False, False, 5) + + y = 1 + for o in range(1, idata.outputs + 1): + is_aux = o >= idata.aux_offset + if not is_aux: + opath = "%s/output/%s" % (ipath, o) + output_name = "Out %s" % o + else: + opath = "%s/aux/%s" % (ipath, o - idata.aux_offset + 1) + output_name = "Aux %s" % (o - idata.aux_offset + 1) + odata = cbox.GetThings(opath + "/status", ['gain', 'output', 'bus', 'insert_engine', 'insert_preset', 'bypass'], []) + engine = odata.insert_engine + preset = odata.insert_preset + bypass = odata.bypass + + t.attach(Gtk.Label(label=output_name), 0, 1, y, y + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + + if not is_aux: + cb = standard_combo(outputs_ls, odata.output - 1) + cb.connect('changed', combo_value_changed, cbox.VarPath(opath + '/output'), 1) + else: + cb = standard_combo(auxbus_ls, ls_index(auxbus_ls, odata.bus, 1)) + cb.connect('changed', combo_value_changed_use_column, cbox.VarPath(opath + '/bus'), 1) + t.attach(cb, 1, 2, y, y + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + + adj = Gtk.Adjustment(value=odata.gain, lower=-96, upper=24, step_increment=1, page_increment=6, page_size=0) + adj.connect('value_changed', adjustment_changed_float, cbox.VarPath(opath + '/gain')) + t.attach(standard_hslider(adj), 2, 3, y, y + 1, Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + + chooser = fx_gui.InsertEffectChooser(opath, "%s: %s" % (iname, output_name), engine, preset, bypass, self) + self.fx_choosers[opath] = chooser + t.attach(chooser.fx_engine, 3, 4, y, y + 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(chooser.fx_preset, 4, 5, y, y + 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(chooser.fx_edit, 5, 6, y, y + 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(chooser.fx_bypass, 6, 7, y, y + 1, 0, Gtk.AttachOptions.SHRINK) + y += 1 + if iengine in instr_gui.instrument_window_map: + b.pack_start(Gtk.HSeparator(), False, False, 5) + b.pack_start(instr_gui.instrument_window_map[iengine](iname, iobj), True, True, 5) + self.nb.append_page(b, Gtk.Label(label=iname)) + self.update() + + def delete_instrument_pages(self): + while self.nb.get_n_pages() > 1: + self.nb.remove_page(self.nb.get_n_pages() - 1) + + def update(self): + cbox.call_on_idle() + master = cbox.Transport.status() + if master.tempo is not None: + self.master_info.set_markup('%s (%s)' % (master.pos, master.pos_ppqn)) + self.timesig_info.set_markup("%s/%s" % tuple(master.timesig)) + self.tempo_adj.set_value(master.tempo) + state = cbox.Document.get_rt().status().state + self.status_bar.update(state[1], master.sample_rate) + return True + +def do_quit(window): + Gtk.main_quit() + +w = MainWindow() +w.set_title("My UI") +w.show_all() +w.connect('destroy', do_quit) + +Gtk.main() + diff --git a/template/calfbox/experiments/interactive.py b/template/calfbox/experiments/interactive.py new file mode 100755 index 0000000..e2d23cc --- /dev/null +++ b/template/calfbox/experiments/interactive.py @@ -0,0 +1,57 @@ +#! /usr/bin/env -S python3 -i +# -*- coding: utf-8 -*- +""" +This is a minimal calfbox python example. It is meant as a starting +point to find bugs and test performance. + +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +import atexit +from pprint import pprint +from calfbox import cbox + + +cbox.init_engine("") +cbox.Config.set("io", "outputs", 8) +NAME = "Cbox Interactive" +cbox.Config.set("io", "client_name", NAME) + +cbox.start_audio() +scene = cbox.Document.get_engine().new_scene() +scene.clear() + +trackName = "trackOne" +cboxMidiOutUuid = cbox.JackIO.create_midi_output(trackName) +calfboxTrack = cbox.Document.get_song().add_track() + + +pblob = bytes() +pblob += cbox.Pattern.serialize_event(0, 0x90, 60, 100) # note on +pblob += cbox.Pattern.serialize_event(383, 0x80, 60, 100) # note off +pattern = cbox.Document.get_song().pattern_from_blob(pblob, 384) +calfboxTrack.add_clip(0, 0, 384, pattern) #pos, offset, length(and not end-position, but is the same for the complete track), pattern +cbox.Document.get_song().set_loop(384, 384) #set playback length for the entire score. Why is the first value not zero? That would create an actual loop from the start to end. We want the song to play only once. The cbox way of doing that is to set the loop range to zero at the end of the track. Zero length is stop. +cbox.Document.get_song().update_playback() + +print() + +def exit_handler(): + #Restore initial state and stop the engine + cbox.Transport.stop() + cbox.stop_audio() + cbox.shutdown_engine() +atexit.register(exit_handler) diff --git a/template/calfbox/experiments/meta.py b/template/calfbox/experiments/meta.py new file mode 100644 index 0000000..3651b53 --- /dev/null +++ b/template/calfbox/experiments/meta.py @@ -0,0 +1,602 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + + +import re +from calfbox import cbox #use the globally installed calfbox +from asyncio import get_event_loop +from sys import stdout, maxsize +import os, signal + +D1024 =210 * 2**0 # = 210. The lcm of 2, 3, 5, 7 . according to www.informatics.indiana.edu/donbyrd/CMNExtremes.htm this is the real world limit. +D512 = 210 * 2**1 +D256 = 210 * 2**2 +D128 = 210 * 2**3 +D64 = 210 * 2**4 +D32 = 210 * 2**5 +D16 = 210 * 2**6 #16th 13440 ticks +D8 = 210 * 2**7 #eigth 26880 ticks +D4 = 210 * 2**8 #quarter 53760 ticks +D2 = 210 * 2**9 #half 107520 ticks +D1 = 210 * 2**10 #whole 215040 ticks +DB = 210 * 2**11 #brevis 430080 ticks +DL = 210 * 2**12 #longa +DM = 210 * 2**13 #maxima +#MAXIMUM = 0x7FFFFFFF # 31bit. maximum number of calfbox ticks allowed for its timeline, for example for the song duration +MAXIMUM = 100 * D1 +#max_pulses = min(2**31, 2**31 * ppqn * bpm / (60 * sample_rate)) + +ly2pitch = { + "ceses,,," : 00, + "ces,,," : 10, + "c,,," : 20, + "cis,,," : 30, + "cisis,,," : 40, + "deses,,," : 50, + "des,,," : 60, + "d,,," : 70, + "dis,,," : 80, + "disis,,," : 90, + "eeses,,," : 100, + "ees,,," : 110, + "e,,," : 120, + "eis,,," : 130, + "eisis,,," : 140, + "feses,,," : 150, + "fes,,," : 160, + "f,,," : 170, + "fis,,," : 180, + "fisis,,," : 190, + "geses,,," : 200, + "ges,,," : 210, + "g,,," : 220, + "gis,,," : 230, + "gisis,,," : 240, + "aeses,,," : 250, + "aes,,," : 260, + "a,,," : 270, + "ais,,," : 280, + "aisis,,," : 290, + "beses,,," : 300, + "bes,,," : 310, + "b,,," : 320, + "bis,,," : 330, + "bisis,,," : 340, + "ceses,," : 350, + "ces,," : 360, + "c,," : 370, + "cis,," : 380, + "cisis,," : 390, + "deses,," : 400, + "des,," : 410, + "d,," : 420, + "dis,," : 430, + "disis,," : 440, + "eeses,," : 450, + "ees,," : 460, + "e,," : 470, + "eis,," : 480, + "eisis,," : 490, + "feses,," : 500, + "fes,," : 510, + "f,," : 520, + "fis,," : 530, + "fisis,," : 540, + "geses,," : 550, + "ges,," : 560, + "g,," : 570, + "gis,," : 580, + "gisis,," : 590, + "aeses,," : 600, + "aes,," : 610, + "a,," : 620, + "ais,," : 630, + "aisis,," : 640, + "beses,," : 650, + "bes,," : 660, + "b,," : 670, + "bis,," : 680, + "bisis,," : 690, + "ceses," : 700, + "ces," : 710, + "c," : 720, + "cis," : 730, + "cisis," : 740, + "deses," : 750, + "des," : 760, + "d," : 770, + "dis," : 780, + "disis," : 790, + "eeses," : 800, + "ees," : 810, + "e," : 820, + "eis," : 830, + "eisis," : 840, + "feses," : 850, + "fes," : 860, + "f," : 870, + "fis," : 880, + "fisis," : 890, + "geses," : 900, + "ges," : 910, + "g," : 920, + "gis," : 930, + "gisis," : 940, + "aeses," : 950, + "aes," : 960, + "a," : 970, + "ais," : 980, + "aisis," : 990, + "beses," : 1000, + "bes," : 1010, + "b," : 1020, + "bis," : 1030, + "bisis," : 1040, + "ceses" : 1050, + "ces" : 1060, + "c" : 1070, + "cis" : 1080, + "cisis" : 1090, + "deses" : 1100, + "des" : 1110, + "d" : 1120, + "dis" : 1130, + "disis" : 1140, + "eeses" : 1150, + "ees" : 1160, + "e" : 1170, + "eis" : 1180, + "eisis" : 1190, + "feses" : 1200, + "fes" : 1210, + "f" : 1220, + "fis" : 1230, + "fisis" : 1240, + "geses" : 1250, + "ges" : 1260, + "g" : 1270, + "gis" : 1280, + "gisis" : 1290, + "aeses" : 1300, + "aes" : 1310, + "a" : 1320, + "ais" : 1330, + "aisis" : 1340, + "beses" : 1350, + "bes" : 1360, + "b" : 1370, + "bis" : 1380, + "bisis" : 1390, + "ceses'" : 1400, + "ces'" : 1410, + "c'" : 1420, + "cis'" : 1430, + "cisis'" : 1440, + "deses'" : 1450, + "des'" : 1460, + "d'" : 1470, + "dis'" : 1480, + "disis'" : 1490, + "eeses'" : 1500, + "ees'" : 1510, + "e'" : 1520, + "eis'" : 1530, + "eisis'" : 1540, + "feses'" : 1550, + "fes'" : 1560, + "f'" : 1570, + "fis'" : 1580, + "fisis'" : 1590, + "geses'" : 1600, + "ges'" : 1610, + "g'" : 1620, + "gis'" : 1630, + "gisis'" : 1640, + "aeses'" : 1650, + "aes'" : 1660, + "a'" : 1670, + "ais'" : 1680, + "aisis'" : 1690, + "beses'" : 1700, + "bes'" : 1710, + "b'" : 1720, + "bis'" : 1730, + "bisis'" : 1740, + "ceses''" : 1750, + "ces''" : 1760, + "c''" : 1770, + "cis''" : 1780, + "cisis''" : 1790, + "deses''" : 1800, + "des''" : 1810, + "d''" : 1820, + "dis''" : 1830, + "disis''" : 1840, + "eeses''" : 1850, + "ees''" : 1860, + "e''" : 1870, + "eis''" : 1880, + "eisis''" : 1890, + "feses''" : 1900, + "fes''" : 1910, + "f''" : 1920, + "fis''" : 1930, + "fisis''" : 1940, + "geses''" : 1950, + "ges''" : 1960, + "g''" : 1970, + "gis''" : 1980, + "gisis''" : 1990, + "aeses''" : 2000, + "aes''" : 2010, + "a''" : 2020, + "ais''" : 2030, + "aisis''" : 2040, + "beses''" : 2050, + "bes''" : 2060, + "b''" : 2070, + "bis''" : 2080, + "bisis''" : 2090, + "ceses'''" : 2100, + "ces'''" : 2110, + "c'''" : 2120, + "cis'''" : 2130, + "cisis'''" : 2140, + "deses'''" : 2150, + "des'''" : 2160, + "d'''" : 2170, + "dis'''" : 2180, + "disis'''" : 2190, + "eeses'''" : 2200, + "ees'''" : 2210, + "e'''" : 2220, + "eis'''" : 2230, + "eisis'''" : 2240, + "feses'''" : 2250, + "fes'''" : 2260, + "f'''" : 2270, + "fis'''" : 2280, + "fisis'''" : 2290, + "geses'''" : 2300, + "ges'''" : 2310, + "g'''" : 2320, + "gis'''" : 2330, + "gisis'''" : 2340, + "aeses'''" : 2350, + "aes'''" : 2360, + "a'''" : 2370, + "ais'''" : 2380, + "aisis'''" : 2390, + "beses'''" : 2400, + "bes'''" : 2410, + "b'''" : 2420, + "bis'''" : 2430, + "bisis'''" : 2440, + "ceses''''" : 2450, + "ces''''" : 2460, + "c''''" : 2470, + "cis''''" : 2480, + "cisis''''" : 2490, + "deses''''" : 2500, + "des''''" : 2510, + "d''''" : 2520, + "dis''''" : 2530, + "disis''''" : 2540, + "eeses''''" : 2550, + "ees''''" : 2560, + "e''''" : 2570, + "eis''''" : 2580, + "eisis''''" : 2590, + "feses''''" : 2600, + "fes''''" : 2610, + "f''''" : 2620, + "fis''''" : 2630, + "fisis''''" : 2640, + "geses''''" : 2650, + "ges''''" : 2660, + "g''''" : 2670, + "gis''''" : 2680, + "gisis''''" : 2690, + "aeses''''" : 2700, + "aes''''" : 2710, + "a''''" : 2720, + "ais''''" : 2730, + "aisis''''" : 2740, + "beses''''" : 2750, + "bes''''" : 2760, + "b''''" : 2770, + "bis''''" : 2780, + "bisis''''" : 2790, + "ceses'''''" : 2800, + "ces'''''" : 2810, + "c'''''" : 2820, + "cis'''''" : 2830, + "cisis'''''" : 2840, + "deses'''''" : 2850, + "des'''''" : 2860, + "d'''''" : 2870, + "dis'''''" : 2880, + "disis'''''" : 2890, + "eeses'''''" : 2900, + "ees'''''" : 2910, + "e'''''" : 2920, + "eis'''''" : 2930, + "eisis'''''" : 2940, + "feses'''''" : 2950, + "fes'''''" : 2960, + "f'''''" : 2970, + "fis'''''" : 2980, + "fisis'''''" : 2990, + "geses'''''" : 3000, + "ges'''''" : 3010, + "g'''''" : 3020, + "gis'''''" : 3030, + "gisis'''''" : 3040, + "aeses'''''" : 3050, + "aes'''''" : 3060, + "a'''''" : 3070, + "ais'''''" : 3080, + "aisis'''''" : 3090, + "beses'''''" : 3100, + "bes'''''" : 3110, + "b'''''" : 3120, + "bis'''''" : 3130, + "bisis'''''" : 3140, + #"r" : float('inf'), a rest is not a pitch + } + +def plain(pitch): + """ Extract the note from a note-number, without any octave but with the tailing zero. + This means we double-use the lowest octave as abstract version.""" + #Dividing through the octave, 350, results in the number of the octave and the note as remainder. + return divmod(pitch, 350)[1] + +def octave(pitch): + """Return the octave of given note. Lowest 0 is X,,,""" + return divmod(pitch, 350)[0] + +def halfToneDistanceFromC(pitch): + """Return the half-tone step distance from C. The "sounding" interval""" + return { + #00 : 10, # ceses,,, -> bes + #10 : 11, # ces,,, -> b + + 00 : -2, # ceses,,, -> bes + 10 : -1, # ces,,, -> b + 20 : 0, # c,,, + 30 : 1, # cis,,, + 40 : 2, # cisis,,, -> d ... + 50 : 0, # deses,,, + 60 : 1, # des,,, + 70 : 2, # d,,, + 80 : 3, # dis,,, + 90 : 4, # disis,,, + 100 : 2, # eeses,,, + 110 : 3, # ees,,, + 120 : 4, # e,,, + 130 : 5, # eis,,, + 140 : 6, # eisis,,, + 150 : 3, # feses,,, + 160 : 4, # fes,,, + 170 : 5, # f,,, + 180 : 6, # fis,,, + 190 : 7, # fisis,,, + 200 : 5, # geses,,, + 210 : 6, # ges,,, + 220 : 7, # g,,, + 230 : 8, # gis,,, + 240 : 9, # gisis,,, + 250 : 7, # aeses,,, + 260 : 8, # aes,,, + 270 : 9, # a,,, + 280 : 10, # ais,,, + 290 : 11, # aisis,,, + 300 : 9, # beses,,, + 310 : 10, # bes,,, + 320 : 11, # b,,, + 330 : 12, # bis,,, + 340 : 13, # bisis,,, + #330 : 0, # bis,,, + #340 : 1, # bisis,,, + }[plain(pitch)] + + +lyToMidi = {} #filled for all pitches on startup, below +for ly, pitch in ly2pitch.items(): + octOffset = (octave(pitch) +1) * 12 #twelve tones per midi octave + lyToMidi[ly] = octOffset + halfToneDistanceFromC(pitch) + + +lyToTicks = { + "16" : D16, + "8" : D8, + "4" : D4, + "2" : D2, + "1" : D1, + } + + + +def ly(lilypondString): + """Take string of simple lilypond notes, return midi pitches as generator of (pitch, ticks)""" + lastDur = "4" + for lyNote in lilypondString.split(" "): + try: + lyPitch, lyDur = re.split(r'(\d+)', lyNote)[0:2] + lastDur = lyDur + except ValueError: + lyPitch = re.split(r'(\d+)', lyNote)[0] + lyDur = lastDur + + yield (lyToMidi[lyPitch], lyToTicks[lyDur]) + + +def ly2cbox(lilypondString): + """Return (pbytes, durationInTicks) + a python byte data type with midi data for cbox""" + #cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity)) + pblob = bytes() + startTick = 0 + for midiPitch, durationInTicks in ly(lilypondString): + endTick = startTick + durationInTicks - 1 #-1 ticks to create a small logical gap. This is nothing compared to our tick value dimensions, but it is enough for the midi protocol to treat two notes as separate ones. Imporant to say that this does NOT affect the next note on. This will be mathematically correct anyway. + pblob += cbox.Pattern.serialize_event(startTick, 0x90, midiPitch, 100) # note on + pblob += cbox.Pattern.serialize_event(endTick , 0x80, midiPitch, 100) # note off + startTick = startTick + durationInTicks #no -1 for the next note + return pblob, startTick + + +cboxTracks = {} #trackName:(cboxTrack,cboxMidiOutUuid) +def cboxSetTrack(trackName, durationInTicks, pattern): + """Creates or resets calfbox tracks including jack connections + Keeps jack connections alive. + + pattern is most likely a single pattern created through cbox.Document.get_song().pattern_from_blob + But it can also be a list of such patterns. In this case all patterns must be the same duration + and the parameter durationInTicks is the length of ONE pattern. + """ + + if not trackName in cboxTracks: + cboxMidiOutUuid = cbox.JackIO.create_midi_output(trackName) + calfboxTrack = cbox.Document.get_song().add_track() + cboxTracks[trackName] = (calfboxTrack, cboxMidiOutUuid) + else: + calfboxTrack, cboxMidiOutUuid = cboxTracks[trackName] + calfboxTrack.delete() + + calfboxTrack = cbox.Document.get_song().add_track() + calfboxTrack.set_external_output(cboxMidiOutUuid) + cbox.JackIO.rename_midi_output(cboxMidiOutUuid, trackName) + calfboxTrack.set_name(trackName) + + if type(pattern) is cbox.DocPattern: + calfboxTrack.add_clip(0, 0, durationInTicks, pattern) #pos, offset, length(and not end-position, but is the same for the complete track), pattern + else: #iterable + assert iter(pattern) + #durationInTicks is the length of ONE pattern. + for i, pat in enumerate(pattern): + calfboxTrack.add_clip(i*durationInTicks, 0, durationInTicks, pat) #pos, offset, length, pattern. + calfboxTrack.add_clip(i*durationInTicks, 0, durationInTicks, pat) #pos, offset, length, pattern. + + return calfboxTrack + + +def ly2Track(trackName, lyString): + """Convert a simple string of lilypond notes to a cbox track and add that to the score""" + music = lyString + cboxBlob, durationInTicks = ly2cbox(music) + pattern = cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks) + cboxSetTrack(trackName, durationInTicks, pattern) + +def getLongestTrackDuationInTicks(): + return max( max(clip.pos + clip.length for clip in cboxTrack.track.status().clips) for cboxTrack in cbox.Document.get_song().status().tracks if cboxTrack.track.status().clips) + #for cboxTrack in cbox.Document.get_song().status().tracks: + # print (cboxTrack.track) + #TODO: These are different than the one above. It is more. Why? + #for cboxTrack, cboxMidiOutUuid in cboxTracks.values(): + # print (cboxTrack.status()) + + +def cboxLoop(eventLoop): + cbox.call_on_idle() + assert eventLoop.is_running() + + #it is not that simple. status = "[Running]" if cbox.Transport.status().playing else "[Stopped]" + if cbox.Transport.status().playing == 1: + status = "[Running]" + elif cbox.Transport.status().playing == 0: + status = "[Stopped]" + elif cbox.Transport.status().playing == 2: + status = "[Stopping]" + elif cbox.Transport.status().playing is None: + status = "[Uninitialized]" + else: + raise ValueError("Unknown playback status: {}".format(cbox.Transport.status().playing)) + + stdout.write(" \r") #it is a hack but it cleans the line from old artefacts + stdout.write('{}: {}\r'.format(status, cbox.Transport.status().pos_ppqn)) + stdout.flush() + eventLoop.call_later(0.1, cboxLoop, eventLoop) #100ms delay + +eventLoop = get_event_loop() +def initCbox(clientName, internalEventProcessor=True, commonMidiInput=True): + cbox.init_engine("") + cbox.Config.set("io", "client_name", clientName) + cbox.Config.set("io", "enable_common_midi_input", commonMidiInput) #the default "catch all" midi input + cbox.start_audio() + scene = cbox.Document.get_engine().new_scene() + scene.clear() + cbox.do_cmd("/master/set_ppqn_factor", None, [D4]) #quarter note has how many ticks? + + cbox.Transport.stop() + cbox.Transport.seek_ppqn(0) + cbox.Transport.set_tempo(120.0) #must be float + + if internalEventProcessor: + eventLoop.call_soon(cboxLoop, eventLoop) + return scene, cbox, eventLoop + + +def connectPhysicalKeyboards(port="midi"): + midiKeyboards = cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL) + ourMidiInPort = cbox.Config.get("io", "client_name",) + ":" + port + for keyboard in midiKeyboards: + cbox.JackIO.port_connect(keyboard, ourMidiInPort) + +def start(autoplay = False, userfunction = None, tempo = 120): + def ask_exit(): + print() + eventLoop.stop() + shutdownCbox() + + try: + dur = getLongestTrackDuationInTicks() + print("Starting with supplied music data. Setting sond duration to longest track") + assert dur > 0 + cbox.Document.get_song().set_loop(dur, dur) #set playback length for the entire score. + except ValueError: + print ("Starting without a track. Setting song duration to a high value to generate recording space") + cbox.Document.get_song().set_loop(MAXIMUM, MAXIMUM) #set playback length for the entire score. + + cbox.Transport.set_tempo(float(tempo)) + cbox.Document.get_song().update_playback() + + for signame in ('SIGINT', 'SIGTERM'): + eventLoop.add_signal_handler(getattr(signal, signame), ask_exit) + + if userfunction: + print ("Send SIGUSR1 with following command to trigger user function") + print ("kill -10 {}".format(os.getpid())) + print () + eventLoop.add_signal_handler(getattr(signal, "SIGUSR1"), userfunction) + + print ("Use jack transport to control playback") + print ("Press Ctrl+C to abort") + print("pid %s: send SIGINT or SIGTERM to exit." % os.getpid()) + + try: + eventLoop.run_forever() + finally: + eventLoop.close() + + +def shutdownCbox(): + cbox.Transport.stop() + cbox.Transport.seek_ppqn(0) + cbox.stop_audio() + cbox.shutdown_engine() diff --git a/template/calfbox/experiments/playPatternsAsMeasures.py b/template/calfbox/experiments/playPatternsAsMeasures.py new file mode 100755 index 0000000..8f3657f --- /dev/null +++ b/template/calfbox/experiments/playPatternsAsMeasures.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +from meta import ly2cbox, cboxSetTrack, initCbox, start, D4 + +scene, cbox, eventLoop = initCbox("test02") + +#Generate Music +music = "c'4 d' e' f'" +cboxBlob, durationInTicks = ly2cbox(music) +#cboxSetTrack("someInstrument", durationInTicks, pattern) + +oneMeasureInTicks = D4 * 4 +patternList = [ + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), + cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks), +] + +cboxSetTrack("metronome", oneMeasureInTicks, patternList) + +def userfunction(): + D4 = 210 * 2**8 + MEASURE = 4 * D4 + cbox.Transport.stop() + cbox.Document.get_song().update_playback() + cbox.Transport.seek_ppqn(4 * MEASURE + 2 * D4 ) #4th measure in the middle + cbox.Transport.play() + +start(userfunction=userfunction) diff --git a/template/calfbox/experiments/printAllMidiEvents.py b/template/calfbox/experiments/printAllMidiEvents.py new file mode 100755 index 0000000..394663d --- /dev/null +++ b/template/calfbox/experiments/printAllMidiEvents.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +from meta import initCbox, start, connectPhysicalKeyboards + +def processMidiIn(eventLoop): + eventList = cbox.get_new_events() + if eventList: + for (address, uninterestingStuff, event) in eventList: #we are only interested in the event, which is a list again. + print (address, "event:", event, "playback:", cbox.Transport.status().playing) + eventLoop.call_later(0.1, processMidiIn, eventLoop) + +scene, cbox, eventLoop = initCbox("test01", internalEventProcessor=False) +eventLoop.call_later(0.1, processMidiIn, eventLoop) #100ms +connectPhysicalKeyboards() +start() diff --git a/template/calfbox/experiments/printAllMidiEventsSpecificPort.py b/template/calfbox/experiments/printAllMidiEventsSpecificPort.py new file mode 100755 index 0000000..0f9c218 --- /dev/null +++ b/template/calfbox/experiments/printAllMidiEventsSpecificPort.py @@ -0,0 +1,35 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +from meta import initCbox, start, connectPhysicalKeyboards + +def processMidiIn(eventLoop): + eventList = cbox.JackIO.get_new_events(cboxMidiPortUid) + if eventList: + for (address, uninterestingStuff, event) in eventList: #we are only interested in the event, which is a list again. + print (address, "event:", event, "playback:", cbox.Transport.status().playing) + eventLoop.call_later(0.1, processMidiIn, eventLoop) + +scene, cbox, eventLoop = initCbox("test01", internalEventProcessor=False, commonMidiInput=False) +cboxMidiPortUid = cbox.JackIO.create_midi_input("customInput") +cbox.JackIO.set_appsink_for_midi_input(cboxMidiPortUid, True) #This sounds like a program wide sink, but it is needed for every port. +cbox.JackIO.route_midi_input(cboxMidiPortUid, scene.uuid) +eventLoop.call_later(0.1, processMidiIn, eventLoop) #100ms +connectPhysicalKeyboards(port="customInput") +start() diff --git a/template/calfbox/experiments/printNotesOnlyDuringPlayback.py b/template/calfbox/experiments/printNotesOnlyDuringPlayback.py new file mode 100755 index 0000000..29a5715 --- /dev/null +++ b/template/calfbox/experiments/printNotesOnlyDuringPlayback.py @@ -0,0 +1,79 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +from meta import ly2Track, initCbox, start, connectPhysicalKeyboards + +def processMidiIn(eventLoop): + """cbox event: ("event address", None, [firstByte, pitch, velocit]) + + Cbox event come in pairs + + first is the normal midi-event + channel + channel = first & 0x0F + mode = first & 0xF0 + + Of course it doesn't need to be pitch and velocity. + But this is easier to document than "data2, data3" + + Either call cbox.call_on_idle or cbox.get_new_events. + Both will clean the event queue but only the latter will give us + the results as python data. + """ + eventList = cbox.get_new_events() + lenList = len(eventList) + if lenList >= 2 and lenList % 2 == 0: + for (address, uninterestingStuff, event) in eventList: #we are only interested in the event, which is a list again. + #print (address, "event:", event, "playback:", cbox.Transport.status().playing) + if address in ("/io/midi/event_time_samples", "/io/midi/event_time_ppqn", ): + assert len(event) == 1 + #We are very strict at the moment. A timestamp is only allowed if there wasn't another timestamp waiting. It is strict because only simple_event with len==3 eat up the timestamp. Any other event will trigger an error. + if processMidiIn.lastTimestamp: + raise NotImplementedError("the previous event didn't eat up the timestamp") + else: + if address == "/io/midi/event_time_ppqn": + assert cbox.Transport.status().playing == 1 + during_recording = True + else: + assert not cbox.Transport.status().playing == 1 + during_recording = False + processMidiIn.lastTimestamp = event[0] + + elif address == "/io/midi/simple_event" and len(event) == 3: #we can only unpack the event after knowing its length. + assert processMidiIn.lastTimestamp # Midi events are always preceded by timestamps + first, second, third = event + channel = first & 0x0F + mode = first & 0xF0 #0x90 note on, 0x80 note off and so on. + + if mode == 0x90: #Note On. 144 in decimal + midipitch = second + velocity = third + if during_recording: + print("ON: {}, Channel: {}, Pitch: {}, Velocity: {}".format(processMidiIn.lastTimestamp, channel, midipitch, velocity)) + #else: + # print("ON Time-Samples: {}, Channel: {}, Pitch: {}, Velocity: {}".format(processMidiIn.lastTimestamp, channel, midipitch, velocity)) + + elif mode == 0x80: #Note Off. 128 in decimal + midipitch = second + velocity = third + if during_recording: + print("OFF: {}, Channel: {}, Pitch: {}, Velocity: {}".format(processMidiIn.lastTimestamp, channel, midipitch, velocity)) + + #elif mode == 0xB0: #CC + # ccNumber = second + # ccValue = third + #else: + #discard the events + + processMidiIn.lastTimestamp = None + + else: + raise NotImplementedError("Address type {} unknown".format(address)) + + + eventLoop.call_later(0.1, processMidiIn, eventLoop) +processMidiIn.lastTimestamp = None + +scene, cbox, eventLoop = initCbox("test01", internalEventProcessor=False) +#ly2Track(trackName="doWeNeedThis", lyString="c8") +eventLoop.call_later(0.1, processMidiIn, eventLoop) #100ms +connectPhysicalKeyboards() +start() diff --git a/template/calfbox/experiments/simplePlayback.py b/template/calfbox/experiments/simplePlayback.py new file mode 100755 index 0000000..6312894 --- /dev/null +++ b/template/calfbox/experiments/simplePlayback.py @@ -0,0 +1,30 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +from meta import ly2cbox, cboxSetTrack, initCbox, start + +scene, cbox, eventLoop = initCbox("test01") + +#Generate Music +music = "c'4 d' e' f'2 g' c''" +cboxBlob, durationInTicks = ly2cbox(music) +pattern = cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks) +cboxSetTrack("someInstrument", durationInTicks, pattern) + +start() diff --git a/template/calfbox/experiments/simplerPlayback.py b/template/calfbox/experiments/simplerPlayback.py new file mode 100755 index 0000000..43e8c7d --- /dev/null +++ b/template/calfbox/experiments/simplerPlayback.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +from meta import ly2Track, initCbox, start + +scene, cbox, eventLoop = initCbox("test01") + +ly2Track(trackName="someInstrument", lyString="c'4 d' e' f'2 g' c''") +start() diff --git a/template/calfbox/experiments/testmetadata.py b/template/calfbox/experiments/testmetadata.py new file mode 100755 index 0000000..7f50180 --- /dev/null +++ b/template/calfbox/experiments/testmetadata.py @@ -0,0 +1,82 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This is a minimal calfbox python example. It is meant as a starting +point to find bugs and test performance. + +Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This code 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 . +""" + +import atexit +from pprint import pprint +from calfbox import cbox + + +cbox.init_engine("") +cbox.Config.set("io", "outputs", 8) +NAME = "Cbox Interactive" +cbox.Config.set("io", "client_name", NAME) + +cbox.start_audio() +scene = cbox.Document.get_engine().new_scene() +scene.clear() + +print("Setting nonsense meta data to our first two ports and midi port") +cbox.JackIO.Metadata.set_property("Cbox Interactive:out_1", "foo", "bar") +cbox.JackIO.Metadata.set_property("Cbox Interactive:out_1", "faz", "baz") +cbox.JackIO.Metadata.set_property("Cbox Interactive:out_2", "rolf", "hello") +cbox.JackIO.Metadata.set_property("Cbox Interactive:out_2", "rolf", "hello") +cbox.JackIO.Metadata.set_property("Cbox Interactive:midi", "wolf", "world", "stryng") +cbox.JackIO.Metadata.set_property("Cbox Interactive:midi", "asd", "qwe", "") + + +print ("Setting port order for all 8 ports") + +portOrderDict = { + "Cbox Interactive:out_1": 50, + "Cbox Interactive:out_2": 40, + "Cbox Interactive:out_3": 3, + "Cbox Interactive:out_4": 5, + "Cbox Interactive:out_5": 7, + "Cbox Interactive:out_6": 999, + "Cbox Interactive:out_7": 4, + "Cbox Interactive:out_8": 4, + } + +try: + cbox.JackIO.Metadata.set_all_port_order(portOrderDict) + print ("Test to catch non-unique indices failed!. Quitting") + quit() +except ValueError as e: + print ("Caught expected ValueError for double index entry.\nAdjusting value and try again to set all ports.") + +portOrderDict["Cbox Interactive:out_8"] = 0 +cbox.JackIO.Metadata.set_all_port_order(portOrderDict) + +print("List of all metadata follows") +pprint (cbox.JackIO.Metadata.get_all_properties()) + +print() +print ("Now check your port order in QJackCtl or similar. Press [Return] to quit") +input() #wait for key to confirm order visually in qjackctl +quit() + +def exit_handler(): + #Restore initial state and stop the engine + cbox.Transport.stop() + cbox.stop_audio() + cbox.shutdown_engine() +atexit.register(exit_handler) diff --git a/template/calfbox/fbr.c b/template/calfbox/fbr.c new file mode 100644 index 0000000..f4bd803 --- /dev/null +++ b/template/calfbox/fbr.c @@ -0,0 +1,373 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "biquad-float.h" +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "eq.h" +#include "module.h" +#include "rt.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS feedback_reducer_params + +#define MAX_FBR_BANDS 16 + +#define ANALYSIS_BUFFER_SIZE 8192 +#define ANALYSIS_BUFFER_BITS 13 + +// Sine table +static complex float euler_table[ANALYSIS_BUFFER_SIZE]; + +// Bit reversal table +static int map_table[ANALYSIS_BUFFER_SIZE]; + +// Bit-reversed von Hann window +static float von_hann_window_transposed[ANALYSIS_BUFFER_SIZE]; + +struct feedback_reducer_params +{ + struct eq_band bands[MAX_FBR_BANDS]; +}; + +struct feedback_reducer_module +{ + struct cbox_module module; + + struct feedback_reducer_params *params, *old_params; + + struct cbox_biquadf_coeffs coeffs[MAX_FBR_BANDS]; + struct cbox_biquadf_state state[MAX_FBR_BANDS][2]; + + float analysis_buffer[ANALYSIS_BUFFER_SIZE]; + float *wrptr; + int analysed; + + complex float fft_buffers[2][ANALYSIS_BUFFER_SIZE]; +}; + +// Trivial implementation of Cooley-Tukey (+ my own mistakes) + von Hann window +static int do_fft(struct feedback_reducer_module *m) +{ + // Copy + bit reversal addressing + for (int i = 0; i < ANALYSIS_BUFFER_SIZE; i++) + { + m->fft_buffers[0][i] = von_hann_window_transposed[i] * m->analysis_buffer[map_table[i]] * (2.0 / ANALYSIS_BUFFER_SIZE); + } + + for (int i = 0; i < ANALYSIS_BUFFER_BITS; i++) + { + complex float *src = m->fft_buffers[i & 1]; + complex float *dst = m->fft_buffers[(~i) & 1]; + int invi = ANALYSIS_BUFFER_BITS - i - 1; + int disp = 1 << i; + int mask = disp - 1; + + for (int j = 0; j < ANALYSIS_BUFFER_SIZE / 2; j++) + { + int jj1 = (j & mask) + ((j & ~mask) << 1); // insert 0 at i'th bit to get the left arm of the butterfly + int jj2 = jj1 + disp; // insert 1 at i'th bit to get the right arm + + // e^iw + complex float eiw1 = euler_table[(jj1 << invi) & (ANALYSIS_BUFFER_SIZE - 1)]; + complex float eiw2 = euler_table[(jj2 << invi) & (ANALYSIS_BUFFER_SIZE - 1)]; + + // printf("%d -> %d, %d\n", j, jj, jj + disp); + butterfly(&dst[jj1], &dst[jj2], src[jj1], src[jj2], eiw1, eiw2); + } + } + return ANALYSIS_BUFFER_BITS & 1; +} + +#define PEAK_REGION_RADIUS 3 + +struct potential_peak_info +{ + int bin; + float avg; + float centre; + float peak; + float dist; + float points; +}; + +static int peak_compare(const void *peak1, const void *peak2) +{ + const struct potential_peak_info *pi1 = peak1; + const struct potential_peak_info *pi2 = peak2; + + if (pi1->points < pi2->points) + return +1; + if (pi1->points > pi2->points) + return -1; + return 0; +} + +static int find_peaks(complex float *spectrum, float srate, float peak_freqs[16]) +{ + struct potential_peak_info pki[ANALYSIS_BUFFER_SIZE / 2 + 1]; + for (int i = 0; i <= ANALYSIS_BUFFER_SIZE / 2; i++) + { + pki[i].bin = i; + pki[i].points = 0.f; + } + float gmax = 0; + for (int i = PEAK_REGION_RADIUS; i <= ANALYSIS_BUFFER_SIZE / 2 - PEAK_REGION_RADIUS; i++) + { + struct potential_peak_info *pi = &pki[i]; + float sum = 0; + float sumf = 0; + float peak = 0; + for (int j = -PEAK_REGION_RADIUS; j <= PEAK_REGION_RADIUS; j++) + { + float f = (i + j); + float bin = cabs(spectrum[i + j]); + if (bin > peak) + peak = bin; + sum += bin; + sumf += f * bin; + } + pi->avg = sum / (2 * PEAK_REGION_RADIUS + 1); + pi->peak = peak; + pi->centre = sumf / sum; + pi->dist = (sumf / sum - i); + if (peak > gmax) + gmax = peak; + // printf("Bin %d sumf/sum %f avg %f peak %f p/a %f dist %f val %f\n", i, sumf / sum, pki[i].avg, peak, peak / pki[i].avg, sumf/sum - i, cabs(spectrum[i])); + } + for (int i = PEAK_REGION_RADIUS; i <= ANALYSIS_BUFFER_SIZE / 2 - PEAK_REGION_RADIUS; i++) + { + struct potential_peak_info *tpi = &pki[i]; + // ignore peaks below -40dB of the max bin + if (pki[(int)tpi->centre].peak < gmax * 0.01) + continue; + pki[(int)tpi->centre].points += 1; + } + #if 0 + for (int i = 0; i <= ANALYSIS_BUFFER_SIZE / 2; i++) + { + float freq = i * srate / ANALYSIS_BUFFER_SIZE; + printf("Bin %d freq %f points %f\n", i, freq, pki[i].points); + } + #endif + qsort(pki, ANALYSIS_BUFFER_SIZE / 2 + 1, sizeof(struct potential_peak_info), peak_compare); + + float peaks[16]; + int peak_count = 0; + for (int i = 0; i <= ANALYSIS_BUFFER_SIZE / 2; i++) + { + if (pki[i].points <= 1) + break; + if (pki[i].peak <= 0.0001) + break; + gboolean dupe = FALSE; + for (int j = 0; j < peak_count; j++) + { + if (fabs(peaks[j] - pki[i].centre) < PEAK_REGION_RADIUS) + { + dupe = TRUE; + break; + } + } + if (dupe) + continue; + peak_freqs[peak_count] = pki[i].centre * srate / ANALYSIS_BUFFER_SIZE; + peaks[peak_count++] = pki[i].centre; + printf("Mul %f freq %f points %f peak %f\n", pki[i].centre, pki[i].centre * srate / ANALYSIS_BUFFER_SIZE, pki[i].points, pki[i].peak); + if (peak_count == 4) + break; + } + return peak_count; +} + +static void redo_filters(struct feedback_reducer_module *m) +{ + for (int i = 0; i < MAX_FBR_BANDS; i++) + { + struct eq_band *band = &m->params->bands[i]; + if (band->active) + { + cbox_biquadf_set_peakeq_rbj(&m->coeffs[i], band->center, band->q, band->gain, m->module.srate); + } + } + m->old_params = m->params; +} + +gboolean feedback_reducer_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct feedback_reducer_module *m = (struct feedback_reducer_module *)ct->user_data; + + EFFECT_PARAM_ARRAY("/active", "i", bands, active, int, , 0, 1) else + EFFECT_PARAM_ARRAY("/center", "f", bands, center, double, , 10, 20000) else + EFFECT_PARAM_ARRAY("/q", "f", bands, q, double, , 0.01, 100) else + EFFECT_PARAM_ARRAY("/gain", "f", bands, gain, double, dB2gain_simple, -100, 100) else + if (!strcmp(cmd->command, "/start") && !strcmp(cmd->arg_types, "")) + { + m->analysed = 0; + cbox_rt_swap_pointers(m->module.rt, (void **)&m->wrptr, m->analysis_buffer); + } + else if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (m->wrptr == m->analysis_buffer + ANALYSIS_BUFFER_SIZE && m->analysed == 0) + { + float freqs[16]; + int count = find_peaks(m->fft_buffers[do_fft(m)], m->module.srate, freqs); + struct feedback_reducer_params *p = malloc(sizeof(struct feedback_reducer_params)); + memcpy(p->bands + count, &m->params->bands[0], sizeof(struct eq_band) * (MAX_FBR_BANDS - count)); + for (int i = 0; i < count; i++) + { + p->bands[i].active = TRUE; + p->bands[i].center = freqs[i]; + p->bands[i].q = freqs[i] / 50; // each band ~100 Hz (not really sure about filter Q vs bandwidth) + p->bands[i].gain = 0.125; + } + free(cbox_rt_swap_pointers(m->module.rt, (void **)&m->params, p)); \ + m->analysed = 1; + if (!cbox_execute_on(fb, NULL, "/refresh", "i", error, 1)) + return FALSE; + } + if (!cbox_execute_on(fb, NULL, "/finished", "i", error, m->analysed)) + return FALSE; + for (int i = 0; i < MAX_FBR_BANDS; i++) + { + if (!cbox_execute_on(fb, NULL, "/active", "ii", error, i, (int)m->params->bands[i].active)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/center", "if", error, i, m->params->bands[i].center)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/q", "if", error, i, m->params->bands[i].q)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/gain", "if", error, i, gain2dB_simple(m->params->bands[i].gain))) + return FALSE; + } + // return cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry); + return CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void feedback_reducer_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct feedback_reducer_module *m = module->user_data; +} + +void feedback_reducer_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct feedback_reducer_module *m = module->user_data; + + if (m->params != m->old_params) + redo_filters(m); + + if (m->wrptr && m->wrptr != m->analysis_buffer + ANALYSIS_BUFFER_SIZE) + { + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + if (m->wrptr == m->analysis_buffer + ANALYSIS_BUFFER_SIZE) + break; + *m->wrptr++ = inputs[0][i] + inputs[1][i]; + } + } + for (int c = 0; c < 2; c++) + { + gboolean first = TRUE; + for (int i = 0; i < MAX_FBR_BANDS; i++) + { + if (!m->params->bands[i].active) + continue; + if (first) + { + cbox_biquadf_process_to(&m->state[i][c], &m->coeffs[i], inputs[c], outputs[c]); + first = FALSE; + } + else + { + cbox_biquadf_process(&m->state[i][c], &m->coeffs[i], outputs[c]); + } + } + if (first) + memcpy(outputs[c], inputs[c], sizeof(float) * CBOX_BLOCK_SIZE); + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION(feedback_reducer) + +MODULE_CREATE_FUNCTION(feedback_reducer) +{ + static int inited = 0; + if (!inited) + { + for (int i = 0; i < ANALYSIS_BUFFER_SIZE; i++) + { + euler_table[i] = cos(i * 2 * M_PI / ANALYSIS_BUFFER_SIZE) + I * sin(i * 2 * M_PI / ANALYSIS_BUFFER_SIZE); + int ni = 0; + for (int j = 0; j < ANALYSIS_BUFFER_BITS; j++) + { + if (i & (1 << (ANALYSIS_BUFFER_BITS - 1 - j))) + ni = ni | (1 << j); + } + map_table[i] = ni; + von_hann_window_transposed[i] = 0.5 * (1 - cos (ni * 2 * M_PI / (ANALYSIS_BUFFER_SIZE - 1))); + } + + inited = 1; + } + + struct feedback_reducer_module *m = malloc(sizeof(struct feedback_reducer_module)); + CALL_MODULE_INIT(m, 2, 2, feedback_reducer); + m->module.process_event = feedback_reducer_process_event; + m->module.process_block = feedback_reducer_process_block; + struct feedback_reducer_params *p = malloc(sizeof(struct feedback_reducer_params)); + m->params = p; + m->old_params = NULL; + m->analysed = 0; + m->wrptr = NULL; + + for (int b = 0; b < MAX_FBR_BANDS; b++) + { + p->bands[b].active = cbox_eq_get_band_param(cfg_section, b, "active", 0) > 0; + p->bands[b].center = cbox_eq_get_band_param(cfg_section, b, "center", 50 * pow(2.0, b / 2.0)); + p->bands[b].q = cbox_eq_get_band_param(cfg_section, b, "q", 0.707 * 2); + p->bands[b].gain = cbox_eq_get_band_param_db(cfg_section, b, "gain", 0); + } + redo_filters(m); + cbox_eq_reset_bands(m->state, MAX_FBR_BANDS); + + return &m->module; +} + + +struct cbox_module_keyrange_metadata feedback_reducer_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata feedback_reducer_controllers[] = { +}; + +DEFINE_MODULE(feedback_reducer, 2, 2) + diff --git a/template/calfbox/fifo.c b/template/calfbox/fifo.c new file mode 100644 index 0000000..5c9852b --- /dev/null +++ b/template/calfbox/fifo.c @@ -0,0 +1,22 @@ +#include "fifo.h" +#include + +struct cbox_fifo *cbox_fifo_new(uint32_t size) +{ + struct cbox_fifo *fifo = calloc(1, sizeof(struct cbox_fifo) + size); + if (!fifo) + return NULL; + fifo->data = (uint8_t *)(fifo + 1); + fifo->size = size; + fifo->write_count = 0; + fifo->write_offset= 0; + fifo->read_count = 0; + fifo->read_offset = 0; + return fifo; +} + +void cbox_fifo_destroy(struct cbox_fifo *fifo) +{ + free(fifo); +} + diff --git a/template/calfbox/fifo.h b/template/calfbox/fifo.h new file mode 100644 index 0000000..d9446cd --- /dev/null +++ b/template/calfbox/fifo.h @@ -0,0 +1,136 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_FIFO_H +#define CBOX_FIFO_H + +#include +#include +#include +#include + +struct cbox_fifo +{ + uint8_t *data; + uint32_t size; + uint64_t pad; // ensure the write-related and read-related structs are on 64 bit boundary + uint32_t write_count; + uint32_t write_offset; + uint32_t read_count; + uint32_t read_offset; +}; + +extern struct cbox_fifo *cbox_fifo_new(uint32_t size); + +static inline uint32_t cbox_fifo_readsize(struct cbox_fifo *fifo); +static inline uint32_t cbox_fifo_writespace(struct cbox_fifo *fifo); +static inline gboolean cbox_fifo_read_atomic(struct cbox_fifo *fifo, void *dest, uint32_t bytes); +static inline gboolean cbox_fifo_write_atomic(struct cbox_fifo *fifo, const void *src, uint32_t bytes); +static inline gboolean cbox_fifo_peek(struct cbox_fifo *fifo, void *dest, uint32_t bytes); +static inline gboolean cbox_fifo_consume(struct cbox_fifo *fifo, uint32_t bytes); + +extern void cbox_fifo_destroy(struct cbox_fifo *fifo); + + +static inline uint32_t cbox_fifo_readsize(struct cbox_fifo *fifo) +{ + return fifo->write_count - fifo->read_count; +} + +static inline uint32_t cbox_fifo_writespace(struct cbox_fifo *fifo) +{ + return fifo->size - (fifo->write_count - fifo->read_count); +} + +static inline gboolean cbox_fifo_read_impl(struct cbox_fifo *fifo, void *dest, uint32_t bytes, gboolean advance) +{ + __sync_synchronize(); + if (fifo->write_count - fifo->read_count < bytes) + return FALSE; + + if (dest) + { + uint32_t ofs = fifo->read_count - fifo->read_offset; + assert(ofs >= 0 && ofs < fifo->size); + if (ofs + bytes > fifo->size) + { + uint8_t *dstb = dest; + uint32_t firstpart = fifo->size - ofs; + memcpy(dstb, fifo->data + ofs, firstpart); + memcpy(dstb + firstpart, fifo->data, bytes - firstpart); + } + else + memcpy(dest, fifo->data + ofs, bytes); + } + + if (advance) + { + __sync_synchronize(); + // Make sure data are copied before signalling that they can be overwritten + fifo->read_count += bytes; + if (fifo->read_count - fifo->read_offset >= fifo->size) + fifo->read_offset += fifo->size; + } + __sync_synchronize(); + + return TRUE; +} + +static inline gboolean cbox_fifo_read_atomic(struct cbox_fifo *fifo, void *dest, uint32_t bytes) +{ + return cbox_fifo_read_impl(fifo, dest, bytes, TRUE); +} + +static inline gboolean cbox_fifo_peek(struct cbox_fifo *fifo, void *dest, uint32_t bytes) +{ + return cbox_fifo_read_impl(fifo, dest, bytes, FALSE); +} + +static inline gboolean cbox_fifo_consume(struct cbox_fifo *fifo, uint32_t bytes) +{ + return cbox_fifo_read_impl(fifo, NULL, bytes, TRUE); +} + +static inline gboolean cbox_fifo_write_atomic(struct cbox_fifo *fifo, const void *src, uint32_t bytes) +{ + if (fifo->size - (fifo->write_count - fifo->read_count) < bytes) + return FALSE; + + uint32_t ofs = fifo->write_count - fifo->write_offset; + assert(ofs >= 0 && ofs < fifo->size); + if (ofs + bytes > fifo->size) + { + const uint8_t *srcb = src; + uint32_t firstpart = fifo->size - ofs; + memcpy(fifo->data + ofs, srcb, firstpart); + memcpy(fifo->data, srcb + firstpart, bytes - firstpart); + } + else + memcpy(fifo->data + ofs, src, bytes); + + // Make sure data are in the buffer before announcing the availability + __sync_synchronize(); + fifo->write_count += bytes; + if (fifo->write_count - fifo->write_offset >= fifo->size) + fifo->write_offset += fifo->size; + + return TRUE; +} + + +#endif diff --git a/template/calfbox/fluid.c b/template/calfbox/fluid.c new file mode 100644 index 0000000..20580e4 --- /dev/null +++ b/template/calfbox/fluid.c @@ -0,0 +1,404 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#include "config.h" + +#if USE_FLUIDSYNTH + +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if FLUIDSYNTH_VERSION_MAJOR < 2 + +typedef unsigned int cbox_fluidsynth_id_t; +#define fluid_sfont_iteration_setup() fluid_preset_t sfont_iterator; +#define fluid_sfont_iteration_start(sfont) (sfont)->iteration_start(sfont) +#define fluid_sfont_iteration_next(sfont) (sfont)->iteration_next(sfont, &sfont_iterator) ? &sfont_iterator : NULL +#define fluid_preset_get_num(preset) (preset)->get_num(preset) +#define fluid_preset_get_banknum(preset) (preset)->get_banknum(preset) +#define fluid_preset_get_name(preset) (preset)->get_name(preset) + +#else + +typedef int cbox_fluidsynth_id_t; +#define fluid_sfont_iteration_setup() + +#endif + +#define CBOX_FLUIDSYNTH_ERROR cbox_fluidsynth_error_quark() + +enum CboxFluidsynthError +{ + CBOX_FLUIDSYNTH_ERROR_FAILED, +}; + +GQuark cbox_fluidsynth_error_quark(void) +{ + return g_quark_from_string("cbox-fluidsynth-error-quark"); +} + +static void fluidsynth_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs); +static void fluidsynth_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len); +static gboolean fluidsynth_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); +static void fluidsynth_destroyfunc(struct cbox_module *module); + +struct fluidsynth_module +{ + struct cbox_module module; + + fluid_settings_t *settings; + fluid_synth_t *synth; + char *bank_name; + int sfid; + int output_pairs; + int is_multi; + float **left_outputs, **right_outputs; +}; + +static gboolean select_patch_by_name(struct fluidsynth_module *m, int channel, const gchar *preset, GError **error) +{ + fluid_sfont_t* sfont = fluid_synth_get_sfont(m->synth, 0); + fluid_preset_t* tmp; + fluid_sfont_iteration_setup(); + + fluid_sfont_iteration_start(sfont); + while((tmp = fluid_sfont_iteration_next(sfont)) != NULL) + { + // trailing spaces are common in some SF2s + const char *pname = fluid_preset_get_name(tmp); + int len = strlen(pname); + while (len > 0 && pname[len - 1] == ' ') + len--; + + if (!strncmp(pname, preset, len) && preset[len] == '\0') + { + fluid_synth_program_select(m->synth, channel, m->sfid, fluid_preset_get_banknum(tmp), fluid_preset_get_num(tmp)); + return TRUE; + } + } + + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Preset not found: %s", preset); + return FALSE; +} + +MODULE_CREATE_FUNCTION(fluidsynth) +{ + int result = 0; + int i; + const char *bankname = cbox_config_get_string(cfg_section, "sf2"); + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct fluidsynth_module *m = malloc(sizeof(struct fluidsynth_module)); + int pairs = cbox_config_get_int(cfg_section, "output_pairs", 0); + m->output_pairs = pairs ? pairs : 1; + m->is_multi = pairs > 0; + if (m->output_pairs < 1 || m->output_pairs > 16) + { + free(m); + g_set_error(error, CBOX_FLUIDSYNTH_ERROR, CBOX_FLUIDSYNTH_ERROR_FAILED, "Invalid number of output pairs (found %d, supported range 1-16)", m->output_pairs); + return NULL; + } + if (pairs == 0) + { + CALL_MODULE_INIT(m, 0, 2 * m->output_pairs, fluidsynth); + m->left_outputs = NULL; + m->right_outputs = NULL; + } + else + { + g_message("Multichannel mode enabled, %d output pairs, 2 effects", m->output_pairs); + CALL_MODULE_INIT(m, 0, 2 * m->output_pairs + 4, fluidsynth); + m->left_outputs = malloc(sizeof(float *) * (m->output_pairs + 2)); + m->right_outputs = malloc(sizeof(float *) * (m->output_pairs + 2)); + } + m->module.process_event = fluidsynth_process_event; + m->module.process_block = fluidsynth_process_block; + m->module.aux_offset = 2 * m->output_pairs; + m->settings = new_fluid_settings(); + fluid_settings_setnum(m->settings, "synth.sample-rate", m->module.srate); + fluid_settings_setint(m->settings, "synth.audio-channels", m->output_pairs); + fluid_settings_setint(m->settings, "synth.audio-groups", m->output_pairs); + m->synth = new_fluid_synth(m->settings); + fluid_synth_set_reverb_on(m->synth, cbox_config_get_int(cfg_section, "reverb", 1)); + fluid_synth_set_chorus_on(m->synth, cbox_config_get_int(cfg_section, "chorus", 1)); + + m->bank_name = NULL; + m->sfid = -1; + if (bankname) + { + m->bank_name = g_strdup(bankname); + g_message("Loading soundfont %s", bankname); + result = fluid_synth_sfload(m->synth, bankname, 1); + if (result == FLUID_FAILED) + { + g_set_error(error, CBOX_FLUIDSYNTH_ERROR, CBOX_FLUIDSYNTH_ERROR_FAILED, "Failed to load the default bank %s: %s", bankname, fluid_synth_error(m->synth)); + return NULL; + } + m->sfid = result; + g_message("Soundfont %s loaded", bankname); + } + if (bankname) + { + for (i = 0; i < 16; i++) + { + gchar *key = g_strdup_printf("channel%d", i + 1); + gchar *preset = cbox_config_get_string(cfg_section, key); + fluid_synth_sfont_select(m->synth, i, m->sfid); + if (preset) + { + if (!select_patch_by_name(m, i, preset, error)) + { + CBOX_DELETE(&m->module); + return NULL; + } + } + g_free(key); + } + } + + return &m->module; +} + +void fluidsynth_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct fluidsynth_module *m = (struct fluidsynth_module *)module; + if (!m->is_multi) + fluid_synth_write_float(m->synth, CBOX_BLOCK_SIZE, outputs[0], 0, 1, outputs[1], 0, 1); + else + { + for (int i = 0; i < 2 + m->output_pairs; i++) + { + m->left_outputs[i] = outputs[2 * i]; + m->right_outputs[i] = outputs[2 * i + 1]; + } + + fluid_synth_nwrite_float(m->synth, CBOX_BLOCK_SIZE, m->left_outputs, m->right_outputs, m->left_outputs + m->output_pairs, m->right_outputs + m->output_pairs); + } +} + +void fluidsynth_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + struct fluidsynth_module *m = (struct fluidsynth_module *)module; + if (len > 0) + { + int cmd = data[0] >> 4; + int chn = data[0] & 15; + switch(cmd) + { + case 8: + fluid_synth_noteoff(m->synth, chn, data[1]); + break; + + case 9: + fluid_synth_noteon(m->synth, chn, data[1], data[2]); + break; + + case 10: + // polyphonic pressure not handled + break; + + case 11: + fluid_synth_cc(m->synth, chn, data[1], data[2]); + break; + + case 12: + fluid_synth_program_change(m->synth, chn, data[1]); + break; + + case 13: + fluid_synth_channel_pressure(m->synth, chn, data[1]); + break; + + case 14: + fluid_synth_pitch_bend(m->synth, chn, data[1] + 128 * data[2]); + break; + + } + } +} + +gboolean fluidsynth_process_load_patch(struct fluidsynth_module *m, const char *bank_name, GError **error) +{ + if (bank_name && !*bank_name) + bank_name = NULL; + int old_sfid = m->sfid; + char *old_bank_name = m->bank_name; + if (bank_name) + { + int result = fluid_synth_sfload(m->synth, bank_name, 1); + if (result == FLUID_FAILED) + { + g_set_error(error, CBOX_FLUIDSYNTH_ERROR, CBOX_FLUIDSYNTH_ERROR_FAILED, "Failed to load the bank %s: %s", bank_name, fluid_synth_error(m->synth)); + return FALSE; + } + g_message("Soundfont %s loaded at ID %d", bank_name, result); + m->sfid = result; + } + else + m->sfid = -1; + if (old_sfid != -1) + { + free(old_bank_name); + fluid_synth_sfunload(m->synth, old_sfid, 1); + } + if (m->sfid != -1) + { + for (int i = 0; i < 16; i++) + fluid_synth_sfont_select(m->synth, i, m->sfid); + } + m->bank_name = bank_name ? g_strdup(bank_name) : NULL; + return TRUE; +} + +gboolean fluidsynth_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct fluidsynth_module *m = (struct fluidsynth_module *)ct->user_data; + + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/polyphony", "i", error, fluid_synth_get_polyphony(m->synth))) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/soundfont", "s", error, m->bank_name ? m->bank_name : "")) + return FALSE; + for (int i = 0; i < 16; i++) + { + cbox_fluidsynth_id_t sfont_id, bank_num, preset_num; + fluid_synth_get_program(m->synth, i, &sfont_id, &bank_num, &preset_num); + fluid_preset_t *preset = fluid_synth_get_channel_preset(m->synth, i); + if (!cbox_execute_on(fb, NULL, "/patch", "iis", error, 1 + i, preset_num + 128 * bank_num, preset ? fluid_preset_get_name(preset) : "(unknown)")) + return FALSE; + } + return CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else if (!strcmp(cmd->command, "/patches") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (m->sfid == -1) + return TRUE; + fluid_sfont_t* sfont = fluid_synth_get_sfont(m->synth, 0); + fluid_preset_t *tmp; + + fluid_sfont_iteration_setup(); + fluid_sfont_iteration_start(sfont); + while((tmp = fluid_sfont_iteration_next(sfont)) != NULL) + { + const char *pname = fluid_preset_get_name(tmp); + if (!cbox_execute_on(fb, NULL, "/patch", "is", error, (int)(fluid_preset_get_num(tmp) + 128 * fluid_preset_get_banknum(tmp)), pname)) + return FALSE; + } + return TRUE; + } + else if (!strcmp(cmd->command, "/set_patch") && !strcmp(cmd->arg_types, "ii")) + { + if (m->sfid == -1) + { + g_set_error(error, CBOX_FLUIDSYNTH_ERROR, CBOX_FLUIDSYNTH_ERROR_FAILED, "No soundfont loaded"); + return FALSE; + } + int channel = CBOX_ARG_I(cmd, 0); + if (channel < 1 || channel > 16) + { + g_set_error(error, CBOX_FLUIDSYNTH_ERROR, CBOX_FLUIDSYNTH_ERROR_FAILED, "Invalid channel %d", channel); + return FALSE; + } + int value = CBOX_ARG_I(cmd, 1); + return fluid_synth_program_select(m->synth, channel - 1, m->sfid, value >> 7, value & 127) == FLUID_OK; + } + else if (!strcmp(cmd->command, "/polyphony") && !strcmp(cmd->arg_types, "i")) + { + int polyphony = CBOX_ARG_I(cmd, 0); + if (polyphony < 2 || polyphony > 256) + { + g_set_error(error, CBOX_FLUIDSYNTH_ERROR, CBOX_FLUIDSYNTH_ERROR_FAILED, "Invalid polyphony %d (must be between 2 and 256)", polyphony); + return FALSE; + } + return fluid_synth_set_polyphony(m->synth, polyphony) == FLUID_OK; + } + else if (!strcmp(cmd->command, "/load_soundfont") && !strcmp(cmd->arg_types, "s")) + { + return fluidsynth_process_load_patch(m, CBOX_ARG_S(cmd, 0), error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void fluidsynth_destroyfunc(struct cbox_module *module) +{ + struct fluidsynth_module *m = (struct fluidsynth_module *)module; + + if (m->output_pairs) + { + free(m->left_outputs); + free(m->right_outputs); + } + free(m->bank_name); + + delete_fluid_settings(m->settings); + delete_fluid_synth(m->synth); +} + +struct cbox_module_livecontroller_metadata fluidsynth_controllers[] = { + { -1, cmlc_continuouscc, 1, "Modulation", NULL}, + { -1, cmlc_continuouscc, 7, "Volume", NULL}, + { -1, cmlc_continuouscc, 10, "Pan", NULL}, + { -1, cmlc_continuouscc, 91, "Reverb", NULL}, + { -1, cmlc_continuouscc, 93, "Chorus", NULL}, + { -1, cmlc_onoffcc, 64, "Hold", NULL}, + { -1, cmlc_onoffcc, 66, "Sostenuto", NULL}, +}; + +struct cbox_module_keyrange_metadata fluidsynth_keyranges[] = { + { 1, 0, 127, "Channel 1" }, + { 2, 0, 127, "Channel 2" }, + { 3, 0, 127, "Channel 3" }, + { 4, 0, 127, "Channel 4" }, + { 5, 0, 127, "Channel 5" }, + { 6, 0, 127, "Channel 6" }, + { 7, 0, 127, "Channel 7" }, + { 8, 0, 127, "Channel 8" }, + { 9, 0, 127, "Channel 9" }, + { 10, 0, 127, "Channel 10" }, + { 11, 0, 127, "Channel 11" }, + { 12, 0, 127, "Channel 12" }, + { 13, 0, 127, "Channel 13" }, + { 14, 0, 127, "Channel 14" }, + { 15, 0, 127, "Channel 15" }, + { 16, 0, 127, "Channel 16" }, +}; + +DEFINE_MODULE(fluidsynth, 0, 2) + +#endif diff --git a/template/calfbox/fuzz.c b/template/calfbox/fuzz.c new file mode 100644 index 0000000..677d3d0 --- /dev/null +++ b/template/calfbox/fuzz.c @@ -0,0 +1,173 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "biquad-float.h" +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS fuzz_params + +struct fuzz_params +{ + float drive; + float wet_dry; + float rectify; + float band; + float bandwidth; + float band2; + float bandwidth2; +}; + +struct fuzz_module +{ + struct cbox_module module; + + struct fuzz_params *params, *old_params; + + struct cbox_biquadf_coeffs split_coeffs; + struct cbox_biquadf_coeffs post_coeffs; + struct cbox_biquadf_state split_state[2]; + struct cbox_biquadf_state post_state[2]; +}; + +gboolean fuzz_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct fuzz_module *m = (struct fuzz_module *)ct->user_data; + + EFFECT_PARAM("/drive", "f", drive, double, dB2gain_simple, -36, 36) else + EFFECT_PARAM("/wet_dry", "f", wet_dry, double, , 0, 1) else + EFFECT_PARAM("/rectify", "f", rectify, double, , 0, 1) else + EFFECT_PARAM("/band", "f", band, double, , 100, 5000) else + EFFECT_PARAM("/bandwidth", "f", bandwidth, double, , 0.25, 4) else + EFFECT_PARAM("/band2", "f", band2, double, , 100, 5000) else + EFFECT_PARAM("/bandwidth2", "f", bandwidth2, double, , 0.25, 4) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/drive", "f", error, gain2dB_simple(m->params->drive)) + && cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry) + && cbox_execute_on(fb, NULL, "/rectify", "f", error, m->params->rectify) + && cbox_execute_on(fb, NULL, "/band", "f", error, m->params->band) + && cbox_execute_on(fb, NULL, "/bandwidth", "f", error, m->params->bandwidth) + && cbox_execute_on(fb, NULL, "/band2", "f", error, m->params->band2) + && cbox_execute_on(fb, NULL, "/bandwidth2", "f", error, m->params->bandwidth2) + && CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error) + ; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void fuzz_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct fuzz_module *m = module->user_data; +} + +void fuzz_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct fuzz_module *m = module->user_data; + + if (m->params != m->old_params) + { + // update calculated values + } + + cbox_biquadf_set_bp_rbj(&m->split_coeffs, m->params->band, 0.7 / m->params->bandwidth, m->module.srate); + cbox_biquadf_set_bp_rbj(&m->post_coeffs, m->params->band2, 0.7 / m->params->bandwidth2, m->module.srate); + + float splitbuf[2][CBOX_BLOCK_SIZE]; + float drive = m->params->drive; + float sdrive = pow(drive, -0.7); + for (int c = 0; c < 2; c++) + { + cbox_biquadf_process_to(&m->split_state[c], &m->split_coeffs, inputs[c], splitbuf[c]); + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float in = inputs[c][i]; + + float val = splitbuf[c][i]; + + val *= drive; + + val += m->params->rectify; + if (fabs(val) > 1.0) + val = (val > 0) ? 1 : -1; + else + val = val * (3 - val * val) * 0.5; + + val *= sdrive; + + val = cbox_biquadf_process_sample(&m->post_state[c], &m->post_coeffs, val); + + outputs[c][i] = in + (val - in) * m->params->wet_dry; + } + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION(fuzz) + +MODULE_CREATE_FUNCTION(fuzz) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct fuzz_module *m = malloc(sizeof(struct fuzz_module)); + CALL_MODULE_INIT(m, 2, 2, fuzz); + m->module.process_event = fuzz_process_event; + m->module.process_block = fuzz_process_block; + struct fuzz_params *p = malloc(sizeof(struct fuzz_params)); + p->drive = cbox_config_get_gain_db(cfg_section, "drive", 0.f); + p->wet_dry = cbox_config_get_float(cfg_section, "wet_dry", 0.5f); + p->rectify = cbox_config_get_float(cfg_section, "rectify", 0.5f); + p->band = cbox_config_get_float(cfg_section, "band", 1000.f); + p->bandwidth = cbox_config_get_float(cfg_section, "bandwidth", 1); + p->band2 = cbox_config_get_float(cfg_section, "band2", 2000.f); + p->bandwidth2 = cbox_config_get_float(cfg_section, "bandwidth2", 1); + m->params = p; + m->old_params = NULL; + cbox_biquadf_reset(&m->split_state[0]); + cbox_biquadf_reset(&m->split_state[1]); + cbox_biquadf_reset(&m->post_state[0]); + cbox_biquadf_reset(&m->post_state[1]); + + return &m->module; +} + + +struct cbox_module_keyrange_metadata fuzz_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata fuzz_controllers[] = { +}; + +DEFINE_MODULE(fuzz, 2, 2) + diff --git a/template/calfbox/fxchain.c b/template/calfbox/fxchain.c new file mode 100644 index 0000000..5cd123d --- /dev/null +++ b/template/calfbox/fxchain.c @@ -0,0 +1,231 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "rt.h" +#include +#include +#include +#include +#include +#include +#include + +struct fxchain_module +{ + struct cbox_module module; + + struct cbox_module **modules; + uint32_t module_count; +}; + +void fxchain_move(struct fxchain_module *m, unsigned int oldpos, unsigned int newpos) +{ + if (oldpos == newpos) + return; + struct cbox_module **modules = malloc(sizeof(struct cbox_module *) * m->module_count); + for (uint32_t i = 0; i < m->module_count; i++) + { + int s; + if (i == newpos) + s = oldpos; + else + { + if (oldpos < newpos) + s = (i < oldpos || i > newpos) ? i : i + 1; + else + s = (i < newpos || i > oldpos) ? i : i - 1; + } + modules[i] = m->modules[s]; + } + free(cbox_rt_swap_pointers(m->module.rt, (void **)&m->modules, modules)); +} + +gboolean fxchain_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct fxchain_module *m = (struct fxchain_module *)ct->user_data; + const char *subcommand = NULL; + int index = 0; + + //EFFECT_PARAM("/module_count", "i", stages, int, , 1, 12) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (uint32_t i = 0; i < m->module_count; i++) + { + gboolean res = FALSE; + if (m->modules[i]) + res = cbox_execute_on(fb, NULL, "/module", "ss", error, m->modules[i]->engine_name, m->modules[i]->instance_name); + else + res = cbox_execute_on(fb, NULL, "/module", "ss", error, "", ""); + if (!res) + return FALSE; + res = cbox_execute_on(fb, NULL, "/bypass", "ii", error, i + 1, m->modules[i] ? m->modules[i]->bypass : 0); + } + return CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else if (cbox_parse_path_part_int(cmd, "/module/", &subcommand, &index, 1, m->module_count, error)) + { + if (!subcommand) + return FALSE; + return cbox_module_slot_process_cmd(&m->modules[index - 1], fb, cmd, subcommand, CBOX_GET_DOCUMENT(&m->module), m->module.rt, m->module.engine, error); + } + else if (!strcmp(cmd->command, "/insert") && !strcmp(cmd->arg_types, "i")) + { + int pos = CBOX_ARG_I(cmd, 0) - 1; + struct cbox_module **new_modules = malloc((m->module_count + 1) * sizeof(struct cbox_module *)); + memcpy(new_modules, m->modules, pos * sizeof(struct cbox_module *)); + new_modules[pos] = NULL; + memcpy(new_modules + pos + 1, m->modules + pos, (m->module_count - pos) * sizeof(struct cbox_module *)); + void *old_modules = cbox_rt_swap_pointers_and_update_count(m->module.rt, (void **)&m->modules, new_modules, &m->module_count, m->module_count + 1); + free(old_modules); + return TRUE; + } + else if (!strcmp(cmd->command, "/delete") && !strcmp(cmd->arg_types, "i")) + { + int pos = CBOX_ARG_I(cmd, 0) - 1; + struct cbox_module **new_modules = malloc((m->module_count + 1) * sizeof(struct cbox_module *)); + memcpy(new_modules, m->modules, pos * sizeof(struct cbox_module *)); + memcpy(new_modules + pos, m->modules + pos + 1, (m->module_count - pos - 1) * sizeof(struct cbox_module *)); + struct cbox_module *deleted_module = m->modules[pos]; + void *old_modules = cbox_rt_swap_pointers_and_update_count(m->module.rt, (void **)&m->modules, new_modules, &m->module_count, m->module_count - 1); + free(old_modules); + if (deleted_module) + CBOX_DELETE(deleted_module); + return TRUE; + } + else if (!strcmp(cmd->command, "/move") && !strcmp(cmd->arg_types, "ii")) + { + int oldpos = CBOX_ARG_I(cmd, 0) - 1; + int newpos = CBOX_ARG_I(cmd, 1) - 1; + fxchain_move(m, oldpos, newpos); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void fxchain_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct fxchain_module *m = module->user_data; +} + +void fxchain_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct fxchain_module *m = module->user_data; + + float bufs[2][2][CBOX_BLOCK_SIZE]; + + for (uint32_t i = 0; i < m->module_count; i++) + { + float *input_bufs[2], *output_bufs[2]; + for (int c = 0; c < 2; c++) + { + input_bufs[c] = i == 0 ? inputs[c] : bufs[i & 1][c]; + output_bufs[c] = i == m->module_count - 1 ? outputs[c] : bufs[(i + 1) & 1][c]; + } + if (m->modules[i] && !m->modules[i]->bypass) + m->modules[i]->process_block(m->modules[i]->user_data, input_bufs, output_bufs); + else + { + // this is not eficient at all, but empty modules aren't likely to be used except + // when setting up a chain. + for (int c = 0; c < 2; c++) + memcpy(output_bufs[c], input_bufs[c], CBOX_BLOCK_SIZE * sizeof(float)); + } + } + +} + +static void fxchain_destroyfunc(struct cbox_module *module) +{ + struct fxchain_module *m = module->user_data; + for (uint32_t i = 0; i < m->module_count; i++) + { + CBOX_DELETE(m->modules[i]); + m->modules[i] = NULL; + } + free(m->modules); +} + +MODULE_CREATE_FUNCTION(fxchain) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + int i, fx_count = 0; + for (i = 0; ; i++) + { + gchar *name = g_strdup_printf("effect%d", i + 1); + const char *fx_name = cbox_config_get_string(cfg_section, name); + g_free(name); + if (!fx_name) + break; + } + fx_count = i; + if (cfg_section && !fx_count) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No effects defined"); + return NULL; + } + + struct fxchain_module *m = malloc(sizeof(struct fxchain_module)); + CALL_MODULE_INIT(m, 2, 2, fxchain); + m->module.process_event = fxchain_process_event; + m->module.process_block = fxchain_process_block; + m->modules = malloc(sizeof(struct cbox_module *) * fx_count); + m->module_count = fx_count; + + for (i = 0; i < fx_count; i++) + m->modules[i] = NULL; + + for (i = 0; i < fx_count; i++) + { + gchar *name = g_strdup_printf("effect%d", i + 1); + const char *fx_preset_name = cbox_config_get_string(cfg_section, name); + g_free(name); + m->modules[i] = cbox_module_new_from_fx_preset(fx_preset_name, doc, rt, engine, error); + if (!m->modules[i]) + goto failed; + } + fx_count = i; + + return &m->module; + +failed: + m->module_count = i; + CBOX_DELETE(&m->module); + return NULL; +} + + +struct cbox_module_keyrange_metadata fxchain_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata fxchain_controllers[] = { +}; + +DEFINE_MODULE(fxchain, 0, 2) + diff --git a/template/calfbox/gate.c b/template/calfbox/gate.c new file mode 100644 index 0000000..fac233f --- /dev/null +++ b/template/calfbox/gate.c @@ -0,0 +1,181 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2012 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-float.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS gate_params + +struct gate_params +{ + float threshold; + float ratio; + float attack; + float hold; + float release; +}; + +struct gate_module +{ + struct cbox_module module; + + struct gate_params *params, *old_params; + struct cbox_onepolef_coeffs attack_lp, release_lp, shifter_lp; + struct cbox_onepolef_state shifter1, shifter2; + struct cbox_onepolef_state tracker; + int hold_time, hold_threshold; +}; + +gboolean gate_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct gate_module *m = (struct gate_module *)ct->user_data; + + EFFECT_PARAM("/threshold", "f", threshold, double, dB2gain_simple, -100, 100) else + EFFECT_PARAM("/ratio", "f", ratio, double, , 1, 100) else + EFFECT_PARAM("/attack", "f", attack, double, , 1, 1000) else + EFFECT_PARAM("/hold", "f", hold, double, , 1, 1000) else + EFFECT_PARAM("/release", "f", release, double, , 1, 1000) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/threshold", "f", error, gain2dB_simple(m->params->threshold)) + && cbox_execute_on(fb, NULL, "/ratio", "f", error, m->params->ratio) + && cbox_execute_on(fb, NULL, "/attack", "f", error, m->params->attack) + && cbox_execute_on(fb, NULL, "/hold", "f", error, m->params->hold) + && cbox_execute_on(fb, NULL, "/release", "f", error, m->params->release) + && CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error) + ; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void gate_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct gate_module *m = module->user_data; +} + +void gate_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct gate_module *m = module->user_data; + + if (m->params != m->old_params) + { + float scale = M_PI * 1000 / m->module.srate; + cbox_onepolef_set_lowpass(&m->attack_lp, scale / m->params->attack); + cbox_onepolef_set_lowpass(&m->release_lp, scale / m->params->release); + cbox_onepolef_set_allpass(&m->shifter_lp, M_PI * 100 / m->module.srate); + m->hold_threshold = (int)(m->module.srate * m->params->hold * 0.001); + m->old_params = m->params; + } + + float threshold = m->params->threshold; + float threshold2 = threshold * threshold * 1.73; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float left = inputs[0][i], right = inputs[1][i]; + float sig = fabs(left) > fabs(right) ? fabs(left) : fabs(right); + + // Primitive envelope detector - may not work so well with more interesting stereo signals + float shf1 = cbox_onepolef_process_sample(&m->shifter1, &m->shifter_lp, 0.5 * (left + right)); + float shf2 = cbox_onepolef_process_sample(&m->shifter2, &m->shifter_lp, shf1); + sig = sig*sig + shf1*shf1 + shf2 * shf2; + + // attack - hold - release logic based on signal envelope + int release = 1; + float gain = 1.0; + if (sig < threshold2) + { + // hold vs release + if (m->hold_time >= m->hold_threshold) + { + gain = powf(sig / threshold2, 0.5 * (m->params->ratio - 1)); + // gain = powf(sqrt(sig) / threshold, (m->params->ratio - 1)); + } + else + m->hold_time++; + } + else + { + // attack - going to 1 using attack rate + m->hold_time = 0; + gain = 1.0; + release = 0; + } + + gain = cbox_onepolef_process_sample(&m->tracker, release ? &m->release_lp : &m->attack_lp, gain); + + outputs[0][i] = left * gain; + outputs[1][i] = right * gain; + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION(gate) + +MODULE_CREATE_FUNCTION(gate) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct gate_module *m = malloc(sizeof(struct gate_module)); + CALL_MODULE_INIT(m, 2, 2, gate); + m->module.process_event = gate_process_event; + m->module.process_block = gate_process_block; + m->hold_time = 0; + m->hold_threshold = 0; + + struct gate_params *p = malloc(sizeof(struct gate_params)); + p->threshold = cbox_config_get_gain_db(cfg_section, "threshold", -28.0); + p->ratio = cbox_config_get_float(cfg_section, "ratio", 3.0); + p->attack = cbox_config_get_float(cfg_section, "attack", 3.0); + p->hold = cbox_config_get_float(cfg_section, "hold", 100.0); + p->release = cbox_config_get_float(cfg_section, "release", 100.0); + m->params = p; + m->old_params = NULL; + + cbox_onepolef_reset(&m->tracker); + cbox_onepolef_reset(&m->shifter1); + cbox_onepolef_reset(&m->shifter2); + + return &m->module; +} + + +struct cbox_module_keyrange_metadata gate_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata gate_controllers[] = { +}; + +DEFINE_MODULE(gate, 2, 2) + diff --git a/template/calfbox/hwcfg.c b/template/calfbox/hwcfg.c new file mode 100644 index 0000000..b86cb7b --- /dev/null +++ b/template/calfbox/hwcfg.c @@ -0,0 +1,172 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#include "config.h" + +#if USE_JACK + +#include "config-api.h" +#include "io.h" + +#include +#include +#include +#include + +static char *cfg(const char *section, const char *name, char *defvalue) +{ + char *value = cbox_config_get_string(section, name); + if (value) + return value; + return cbox_config_get_string_with_default("autojack", name, defvalue); +} + +static void generate_jack_config(const char *section, const char *id) +{ + char *rcfile = cbox_config_get_string("autojack", "jackdrc"); + FILE *f; + if (!rcfile) + { + rcfile = g_strdup_printf("%s/.jackdrc", getenv("HOME")); + g_message("Generating JACK config: %s\n", rcfile); + f = fopen(rcfile, "w"); + if (!f) + { + g_error("Cannot open file %s", rcfile); + return; + } + g_free(rcfile); + } + else + { + g_message("Generating JACK config: %s\n", rcfile); + f = fopen(rcfile, "w"); + if (!f) + { + g_error("Cannot open file %s", rcfile); + return; + } + } + + fprintf(f, "%s %s -d alsa -d hw:%s -r 44100 %s\n", + cfg(section, "jackd", "/usr/bin/jackd"), + cfg(section, "jack_options", "-R -T"), + id, + cfg(section, "alsa_options", "")); + fclose(f); +} + +static int try_soundcard(const char *name) +{ + gchar *id; + if (!cbox_config_has_section(name)) + return 0; + + g_message("Trying section %s", name); + + id = cbox_config_get_string(name, "device"); + if (id != NULL) + { + struct stat s; + int result; + gchar *fn = g_strdup_printf("/proc/asound/%s", id); + result = stat(fn, &s); + if (!result) + generate_jack_config(name, id); + g_free(fn); + return !result; + } + + id = cbox_config_get_string(name, "usbid"); + if (id != NULL) + { + int vid, pid; + if (sscanf(id, "%x:%x\n", &vid, &pid) !=2) + { + g_error("Invalid VID:PID value: %s", id); + return 0; + } + for (int i = 0; ; i++) + { + struct stat s; + int result; + FILE *f = NULL; + int tvid, tpid; + + // check if it's not beyond the last soundcard index + gchar *fn = g_strdup_printf("/proc/asound/card%d", i); + result = stat(fn, &s); + g_free(fn); + if (result) + break; + + // check if it has a USB ID + fn = g_strdup_printf("/proc/asound/card%d/usbid", i); + f = fopen(fn, "r"); + g_free(fn); + + if (!f) + continue; + + if (fscanf(f, "%x:%x", &tvid, &tpid) == 2) + { + if (vid == tvid && pid == tpid) + { + gchar *fn = g_strdup_printf("%d", i); + generate_jack_config(name, fn); + g_free(fn); + fclose(f); + return 1; + } + } + fclose(f); + } + return 0; + } + + return 0; +} + +int cbox_hwcfg_setup_jack(void) +{ + int i; + if (!cbox_config_has_section("autojack")) + return 0; + + for (i = 0; ; i++) + { + int result; + gchar *cardnum = g_strdup_printf("soundcard%d", i); + char *secname = cbox_config_get_string("autojack", cardnum); + g_free(cardnum); + + if (!secname) + break; + + secname = g_strdup_printf("soundcard:%s", secname); + result = try_soundcard(secname); + g_free(secname); + + if (result) + return 1; + } + + return 0; +} + +#endif diff --git a/template/calfbox/hwcfg.h b/template/calfbox/hwcfg.h new file mode 100644 index 0000000..a5061b2 --- /dev/null +++ b/template/calfbox/hwcfg.h @@ -0,0 +1,28 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_HWCFG_H +#define CBOX_HWCFG_H + +/** + * Autodetect JACK config based on cbox configuration vs ALSA devices present. + * @retval 1 if OK + */ +extern int cbox_hwcfg_setup_jack(void); + +#endif diff --git a/template/calfbox/instr.c b/template/calfbox/instr.c new file mode 100644 index 0000000..d030c7c --- /dev/null +++ b/template/calfbox/instr.c @@ -0,0 +1,251 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "auxbus.h" +#include "config-api.h" +#include "instr.h" +#include "module.h" +#include "io.h" +#include "rt.h" +#include "scene.h" +#include +#include + +CBOX_CLASS_DEFINITION_ROOT(cbox_instrument) + +static gboolean cbox_instrument_output_process_cmd(struct cbox_instrument *instr, struct cbox_instrument_output *output, struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, GError **error) +{ + if (!strcmp(subcmd, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!(cbox_execute_on(fb, NULL, "/gain_linear", "f", error, output->gain_obj.lin_gain) && + cbox_execute_on(fb, NULL, "/gain", "f", error, output->gain_obj.db_gain) && + cbox_execute_on(fb, NULL, "/output", "i", error, output->output_bus + 1))) + return FALSE; + return cbox_module_slot_process_cmd(&output->insert, fb, cmd, subcmd, CBOX_GET_DOCUMENT(instr->scene), instr->scene->rt, instr->scene->engine, error); + } + if (!strcmp(subcmd, "/gain") && !strcmp(cmd->arg_types, "f")) + { + // XXXKF this needs proper handling of concurrency/race conditions, might + // be built into the gain class in future. + cbox_gain_set_db(&output->gain_obj, CBOX_ARG_F(cmd, 0)); + return TRUE; + } + if (!strcmp(subcmd, "/output") && !strcmp(cmd->arg_types, "i")) + { + int obus = CBOX_ARG_I(cmd, 0); + int max_outputs = instr->scene->rt->io ? instr->scene->rt->io->io_env.output_count : 2; + int max_obus = 1 + (max_outputs - 1) / 2; + if (obus < 0 || obus > max_obus) { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid output %d (must be between 1 and %d, or 0 for none)", obus, max_obus); + return FALSE; + } + output->output_bus = obus - 1; + return TRUE; + } + if (!strncmp(subcmd, "/rec_dry/", 9)) + return cbox_execute_sub(&output->rec_dry.cmd_target, fb, cmd, subcmd + 8, error); + if (!strncmp(subcmd, "/rec_wet/", 9)) + return cbox_execute_sub(&output->rec_wet.cmd_target, fb, cmd, subcmd + 8, error); + return cbox_module_slot_process_cmd(&output->insert, fb, cmd, subcmd, CBOX_GET_DOCUMENT(instr->scene), instr->scene->rt, instr->scene->engine, error); +} + +static gboolean cbox_instrument_aux_process_cmd(struct cbox_instrument *instr, struct cbox_instrument_output *output, int id, struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, GError **error) +{ + if (!strcmp(subcmd, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!(cbox_execute_on(fb, NULL, "/gain_linear", "f", error, output->gain_obj.lin_gain) && + cbox_execute_on(fb, NULL, "/gain", "f", error, output->gain_obj.db_gain) && + cbox_execute_on(fb, NULL, "/bus", "s", error, instr->aux_output_names[id] ? instr->aux_output_names[id] : ""))) + return FALSE; + return cbox_module_slot_process_cmd(&output->insert, fb, cmd, subcmd, CBOX_GET_DOCUMENT(instr->scene), instr->scene->rt, instr->scene->engine, error); + } + else if (!strcmp(subcmd, "/bus") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_scene *scene = instr->scene; + if (!CBOX_ARG_S(cmd, 0)) + { + struct cbox_aux_bus *old_bus = cbox_rt_swap_pointers(instr->module->rt, (void **)&instr->aux_outputs[id], NULL); + if (old_bus) + cbox_aux_bus_unref(old_bus); + return TRUE; + } + for (uint32_t i = 0; i < scene->aux_bus_count; i++) + { + if (!scene->aux_buses[i]) + continue; + if (!strcmp(scene->aux_buses[i]->name, CBOX_ARG_S(cmd, 0))) + { + g_free(instr->aux_output_names[id]); + instr->aux_output_names[id] = g_strdup(scene->aux_buses[i]->name); + cbox_aux_bus_ref(scene->aux_buses[i]); + struct cbox_aux_bus *old_bus = cbox_rt_swap_pointers(instr->module->rt, (void **)&instr->aux_outputs[id], scene->aux_buses[i]); + if (old_bus) + cbox_aux_bus_unref(old_bus); + return TRUE; + } + } + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown aux bus: %s", CBOX_ARG_S(cmd, 0)); + return FALSE; + } + else if (!strcmp(subcmd, "/output") && !strcmp(cmd->arg_types, "i")) // not supported + { + cbox_set_command_error(error, cmd); + return FALSE; + } + else // otherwise, treat just like an command on normal (non-aux) output + return cbox_instrument_output_process_cmd(instr, output, fb, cmd, subcmd, error); +} + +gboolean cbox_instrument_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_instrument *instr = ct->user_data; + const char *subcommand = NULL; + int index = 0; + int aux_offset = instr->module->aux_offset / 2; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/engine", "s", error, instr->module->engine_name)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/aux_offset", "i", error, instr->module->aux_offset / 2 + 1)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/outputs", "i", error, instr->module->outputs / 2)) + return FALSE; + return CBOX_OBJECT_DEFAULT_STATUS(instr, fb, error); + } + else if (cbox_parse_path_part_int(cmd, "/output/", &subcommand, &index, 1, aux_offset, error)) + { + if (!subcommand) + return FALSE; + if (index < 1 || index > (int)(1 + instr->module->aux_offset)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d)", index, instr->module->aux_offset); + return FALSE; + } + return cbox_instrument_output_process_cmd(instr, &instr->outputs[index - 1], fb, cmd, subcommand, error); + } + else if (cbox_parse_path_part_int(cmd, "/aux/", &subcommand, &index, 1, instr->aux_output_count, error)) + { + if (!subcommand) + return FALSE; + int acount = 1 + instr->module->outputs - instr->module->aux_offset; + if (index < 1 || index > acount) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d)", index, acount); + return FALSE; + } + return cbox_instrument_aux_process_cmd(instr, &instr->outputs[aux_offset + index - 1], index - 1, fb, cmd, subcommand, error); + } + else + if (!strncmp(cmd->command, "/engine/",8)) + { + if (!instr->module->cmd_target.process_cmd) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "The engine %s has no command target defined", instr->module->engine_name); + return FALSE; + } + return cbox_execute_sub(&instr->module->cmd_target, fb, cmd, cmd->command + 7, error); + } + else if (!strcmp(cmd->command, "/move_to") && !strcmp(cmd->arg_types, "si")) + { + struct cbox_scene *new_scene = (struct cbox_scene *)CBOX_ARG_O(cmd, 0, instr->scene, cbox_scene, error); + if (!new_scene) + return FALSE; + int dstpos = CBOX_ARG_I(cmd, 1) - 1; + + if (dstpos < 0 || (uint32_t)dstpos > new_scene->layer_count) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d or 0 for append)", dstpos + 1, 1 + new_scene->layer_count); + return FALSE; + } + + return cbox_scene_move_instrument_to(instr->scene, instr, new_scene, dstpos, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +void cbox_instrument_destroy_if_unused(struct cbox_instrument *instrument) +{ + if (instrument->refcount == 0) + CBOX_DELETE(instrument); +} + +void cbox_instrument_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_instrument *instrument = CBOX_H2O(objhdr); + assert(instrument->refcount == 0); + for (uint32_t i = 0; i < (uint32_t)instrument->module->outputs >> 1; i ++) + { + cbox_instrument_output_uninit(&instrument->outputs[i]); + } + free(instrument->outputs); + for (uint32_t i = 0; i < instrument->aux_output_count; i++) + { + g_free(instrument->aux_output_names[i]); + } + free(instrument->aux_output_names); + free(instrument->aux_outputs); + CBOX_DELETE(instrument->module); + free(instrument); +} + +void cbox_instrument_unref_aux_buses(struct cbox_instrument *instrument) +{ + for (uint32_t j = 0; j < instrument->aux_output_count; j++) + { + if (instrument->aux_outputs[j]) + cbox_aux_bus_unref(instrument->aux_outputs[j]); + } +} + +void cbox_instrument_disconnect_aux_bus(struct cbox_instrument *instrument, struct cbox_aux_bus *bus) +{ + for (uint32_t j = 0; j < instrument->aux_output_count; j++) + { + if (instrument->aux_outputs[j] == bus) + { + cbox_aux_bus_unref(instrument->aux_outputs[j]); + instrument->aux_outputs[j] = NULL; + } + } +} + +void cbox_instrument_output_init(struct cbox_instrument_output *output, struct cbox_scene *scene, uint32_t max_numsamples) +{ + cbox_recording_source_init(&output->rec_dry, scene, max_numsamples, 2); + cbox_recording_source_init(&output->rec_wet, scene, max_numsamples, 2); + output->insert = NULL; + output->output_bus = 0; + cbox_gain_init(&output->gain_obj); +} + + +void cbox_instrument_output_uninit(struct cbox_instrument_output *output) +{ + cbox_recording_source_uninit(&output->rec_dry); + cbox_recording_source_uninit(&output->rec_wet); + if (output->insert) + { + CBOX_DELETE(output->insert); + output->insert = NULL; + } +} diff --git a/template/calfbox/instr.h b/template/calfbox/instr.h new file mode 100644 index 0000000..a8b4eb8 --- /dev/null +++ b/template/calfbox/instr.h @@ -0,0 +1,61 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_INSTR_H +#define CBOX_INSTR_H + +#include "dspmath.h" +#include "recsrc.h" + +CBOX_EXTERN_CLASS(cbox_instrument) + +struct cbox_module; +struct cbox_rt; +struct cbox_scene; +struct cbox_instruments; + +struct cbox_instrument_output +{ + struct cbox_module *insert; + int output_bus; + struct cbox_gain gain_obj; + struct cbox_recording_source rec_dry, rec_wet; +}; + +struct cbox_instrument +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + struct cbox_module *module; + struct cbox_instrument_output *outputs; + struct cbox_scene *scene; + int refcount; + gchar **aux_output_names; + struct cbox_aux_bus **aux_outputs; + uint32_t aux_output_count; +}; + +extern void cbox_instrument_unref_aux_buses(struct cbox_instrument *instrument); +extern void cbox_instrument_disconnect_aux_bus(struct cbox_instrument *instrument, struct cbox_aux_bus *bus); +extern void cbox_instrument_destroy_if_unused(struct cbox_instrument *instrument); +extern gboolean cbox_instrument_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +extern void cbox_instrument_output_init(struct cbox_instrument_output *output, struct cbox_scene *scene, uint32_t max_numsamples); +extern void cbox_instrument_output_uninit(struct cbox_instrument_output *output); + +#endif diff --git a/template/calfbox/io.c b/template/calfbox/io.c new file mode 100644 index 0000000..9ffa7df --- /dev/null +++ b/template/calfbox/io.c @@ -0,0 +1,625 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "config.h" +#include "config-api.h" +#include "engine.h" +#include "errors.h" +#include "hwcfg.h" +#include "io.h" +#include "meter.h" +#include "midi.h" +#include "mididest.h" +#include "recsrc.h" +#include "seq.h" + +#include +#include +#include +#include + +const char *cbox_io_section = "io"; + +gboolean cbox_io_init(struct cbox_io *io, struct cbox_open_params *const params, struct cbox_command_target *fb, GError **error) +{ +#if USE_JACK +#if USE_LIBUSB + if (cbox_config_get_int(cbox_io_section, "use_usb", 0)) + return cbox_io_init_usb(io, params, fb, error); +#endif + return cbox_io_init_jack(io, params, fb, error); +#else +#if USE_LIBUSB + return cbox_io_init_usb(io, params, fb, error); +#endif +#endif +} + +int cbox_io_get_sample_rate(struct cbox_io *io) +{ + return io->impl->getsampleratefunc(io->impl); +} + +void cbox_io_poll_ports(struct cbox_io *io, struct cbox_command_target *fb) +{ + io->impl->pollfunc(io->impl, fb); +} + +int cbox_io_get_midi_data(struct cbox_io *io, struct cbox_midi_buffer *destination) +{ + return io->impl->getmidifunc(io->impl, destination); +} + +int cbox_io_start(struct cbox_io *io, struct cbox_io_callbacks *cb, struct cbox_command_target *fb) +{ + io->cb = cb; + return io->impl->startfunc(io->impl, fb, NULL); +} + +gboolean cbox_io_get_disconnect_status(struct cbox_io *io, GError **error) +{ + return io->impl->getstatusfunc(io->impl, error); +} + +gboolean cbox_io_cycle(struct cbox_io *io, struct cbox_command_target *fb, GError **error) +{ + return io->impl->cyclefunc(io->impl, fb, error); +} + +int cbox_io_stop(struct cbox_io *io) +{ + int result = io->impl->stopfunc(io->impl, NULL); + if (io->cb && io->cb->on_stopped) + io->cb->on_stopped(io->cb->user_data); + io->cb = NULL; + return result; +} + +struct cbox_midi_output *cbox_io_get_midi_output(struct cbox_io *io, const char *name, const struct cbox_uuid *uuid) +{ + if (uuid) + { + for (GSList *p = io->midi_outputs; p; p = g_slist_next(p)) + { + struct cbox_midi_output *midiout = p->data; + if (!midiout->removing && cbox_uuid_equal(&midiout->uuid, uuid)) + return midiout; + } + } + if (name) + { + for (GSList *p = io->midi_outputs; p; p = g_slist_next(p)) + { + struct cbox_midi_output *midiout = p->data; + if (!midiout->removing && !strcmp(midiout->name, name)) + return midiout; + } + } + return NULL; +} + +struct cbox_midi_input *cbox_io_get_midi_input(struct cbox_io *io, const char *name, const struct cbox_uuid *uuid) +{ + if (uuid) + { + for (GSList *p = io->midi_inputs; p; p = g_slist_next(p)) + { + struct cbox_midi_input *midiin = p->data; + if (!midiin->removing && cbox_uuid_equal(&midiin->uuid, uuid)) + return midiin; + } + } + if (name) + { + for (GSList *p = io->midi_inputs; p; p = g_slist_next(p)) + { + struct cbox_midi_input *midiin = p->data; + if (!midiin->removing && !strcmp(midiin->name, name)) + return midiin; + } + } + return NULL; +} + +struct cbox_audio_output *cbox_io_get_audio_output(struct cbox_io *io, const char *name, const struct cbox_uuid *uuid) +{ + if (uuid) + { + for (GSList *p = io->audio_outputs; p; p = g_slist_next(p)) + { + struct cbox_audio_output *audioout = p->data; + if (!audioout->removing && cbox_uuid_equal(&audioout->uuid, uuid)) + return audioout; + } + } + if (name) + { + for (GSList *p = io->audio_outputs; p; p = g_slist_next(p)) + { + struct cbox_audio_output *audioout = p->data; + if (!audioout->removing && !strcmp(audioout->name, name)) + return audioout; + } + } + return NULL; +} + +struct cbox_midi_output *cbox_io_create_midi_output(struct cbox_io *io, const char *name, GError **error) +{ + struct cbox_midi_output *midiout = cbox_io_get_midi_output(io, name, NULL); + if (midiout) + return midiout; + + midiout = io->impl->createmidioutfunc(io->impl, name, error); + if (!midiout) + return NULL; + + io->midi_outputs = g_slist_prepend(io->midi_outputs, midiout); + + // Notify client code to connect to new outputs if needed + if (io->cb->on_midi_outputs_changed) + io->cb->on_midi_outputs_changed(io->cb->user_data); + return midiout; +} + +void cbox_io_destroy_midi_output(struct cbox_io *io, struct cbox_midi_output *midiout) +{ + midiout->removing = TRUE; + + // This is not a very efficient way to do it. However, in this case, + // the list will rarely contain more than 10 elements, so simplicity + // and correctness may be more important. + GSList *copy = g_slist_copy(io->midi_outputs); + copy = g_slist_remove(copy, midiout); + + GSList *old = io->midi_outputs; + io->midi_outputs = copy; + + cbox_midi_merger_close(&midiout->merger, app.rt); + assert(!midiout->merger.inputs); + + // Notify client code to disconnect the output and to make sure the RT code + // is not using the old list anymore + if (io->cb->on_midi_outputs_changed) + io->cb->on_midi_outputs_changed(io->cb->user_data); + + assert(!midiout->merger.inputs); + + g_slist_free(old); + io->impl->destroymidioutfunc(io->impl, midiout); +} + +struct cbox_midi_input *cbox_io_create_midi_input(struct cbox_io *io, const char *name, GError **error) +{ + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, name, NULL); + if (midiin) + return midiin; + + midiin = io->impl->createmidiinfunc(io->impl, name, error); + if (!midiin) + return NULL; + + io->midi_inputs = g_slist_prepend(io->midi_inputs, midiin); + + // Notify client code to connect to new inputs if needed + if (io->cb->on_midi_inputs_changed) + io->cb->on_midi_inputs_changed(io->cb->user_data); + return midiin; +} + +void cbox_io_destroy_midi_input(struct cbox_io *io, struct cbox_midi_input *midiin) +{ + midiin->removing = TRUE; + + // This is not a very efficient way to do it. However, in this case, + // the list will rarely contain more than 10 elements, so simplicity + // and correctness may be more important. + GSList *copy = g_slist_copy(io->midi_inputs); + copy = g_slist_remove(copy, midiin); + + GSList *old = io->midi_inputs; + io->midi_inputs = copy; + + // Notify client code to disconnect the input and to make sure the RT code + // is not using the old list anymore + if (io->cb->on_midi_inputs_changed) + io->cb->on_midi_inputs_changed(io->cb->user_data); + + g_slist_free(old); + io->impl->destroymidiinfunc(io->impl, midiin); +} + +void cbox_io_destroy_all_midi_ports(struct cbox_io *io) +{ + for (GSList *p = io->midi_outputs; p; p = g_slist_next(p)) + { + struct cbox_midi_output *midiout = p->data; + midiout->removing = TRUE; + } + for (GSList *p = io->midi_inputs; p; p = g_slist_next(p)) + { + struct cbox_midi_output *midiin = p->data; + midiin->removing = TRUE; + } + + GSList *old_i = io->midi_inputs, *old_o = io->midi_outputs; + io->midi_outputs = NULL; + io->midi_inputs = NULL; + // Notify client code to disconnect the output and to make sure the RT code + // is not using the old list anymore + if (io->cb && io->cb->on_midi_outputs_changed) + io->cb->on_midi_outputs_changed(io->cb->user_data); + if (io->cb && io->cb->on_midi_inputs_changed) + io->cb->on_midi_inputs_changed(io->cb->user_data); + + while(old_o) + { + struct cbox_midi_output *midiout = old_o->data; + cbox_midi_merger_close(&midiout->merger, app.rt); + assert(!midiout->merger.inputs); + io->impl->destroymidioutfunc(io->impl, midiout); + old_o = g_slist_remove(old_o, midiout); + } + g_slist_free(old_o); + + while(old_i) + { + struct cbox_midi_input *midiin = old_i->data; + io->impl->destroymidiinfunc(io->impl, midiin); + old_i = g_slist_remove(old_i, midiin); + } + g_slist_free(old_i); +} + +static void cbox_audio_output_router_record_block(struct cbox_recorder *handler, const float **buffers, uint32_t offset, uint32_t numsamples) +{ + struct cbox_audio_output_router *router = handler->user_data; + if (router->left->removing || router->right->removing) + return; + cbox_gain_add_stereo(&router->gain, &router->left->buffer[offset], buffers[0], &router->right->buffer[offset], buffers[1], numsamples); +} + +static gboolean cbox_audio_output_router_attach(struct cbox_recorder *handler, struct cbox_recording_source *src, GError **error) +{ + struct cbox_audio_output_router *router = handler->user_data; + if (router->attached) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Router already attached"); + return FALSE; + } + router->source = src; + router->attached++; + return TRUE; +} + +static gboolean cbox_audio_output_router_detach(struct cbox_recorder *handler, GError **error) +{ + struct cbox_audio_output_router *router = handler->user_data; + if (router->attached != 1) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Router not yet attached"); + return FALSE; + } + assert (router->source); + --router->attached; + assert (router->attached == 0); + router->source = NULL; + return TRUE; +} + +static void cbox_audio_output_router_destroy(struct cbox_recorder *handler) +{ + struct cbox_audio_output_router *router = handler->user_data; + if (router->attached) + cbox_recording_source_detach(router->source, &router->recorder, NULL); + assert(!router->attached); + router->left->users--; + router->right->users--; +} + +static gboolean cbox_audio_output_router_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_audio_output_router *router = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (!cbox_execute_on(fb, NULL, "/gain", "f", error, router->gain.db_gain)) + return FALSE; + return CBOX_OBJECT_DEFAULT_STATUS(&router->recorder, fb, error); + } + if (!strcmp(cmd->command, "/gain") && !strcmp(cmd->arg_types, "f")) + { + cbox_gain_set_db(&router->gain, CBOX_ARG_F(cmd, 0)); + return TRUE; + } + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +struct cbox_audio_output_router *cbox_io_create_audio_output_router(struct cbox_io *io, struct cbox_engine *engine, struct cbox_audio_output *left, struct cbox_audio_output *right) +{ + struct cbox_audio_output_router *router = calloc(1, sizeof(struct cbox_audio_output_router)); + CBOX_OBJECT_HEADER_INIT(&router->recorder, cbox_recorder, CBOX_GET_DOCUMENT(engine)); + cbox_command_target_init(&router->recorder.cmd_target, cbox_audio_output_router_process_cmd, &router->recorder); + router->recorder.user_data = router; + router->recorder.attach = cbox_audio_output_router_attach; + router->recorder.record_block = cbox_audio_output_router_record_block; + router->recorder.detach = cbox_audio_output_router_detach; + router->recorder.destroy = cbox_audio_output_router_destroy; + router->source = NULL; + router->left = left; + router->right = right; + router->attached = 0; + cbox_gain_init(&router->gain); + cbox_gain_set_db(&router->gain, 12.0); + left->users++; + right->users++; + CBOX_OBJECT_REGISTER(&router->recorder); + return router; +} + +struct cbox_audio_output *cbox_io_create_audio_output(struct cbox_io *io, const char *name, GError **error) +{ + struct cbox_audio_output *audioout = cbox_io_get_audio_output(io, name, NULL); + if (audioout) + return audioout; + + audioout = io->impl->createaudiooutfunc(io->impl, name, error); + if (!audioout) + return NULL; + + io->audio_outputs = g_slist_prepend(io->audio_outputs, audioout); + + // Notify client code to connect to new outputs if needed + if (io->cb->on_audio_outputs_changed) + io->cb->on_audio_outputs_changed(io->cb->user_data); + return audioout; +} + +struct cbox_audio_output *cbox_io_get_audio_output_by_uuid_string(struct cbox_io *io, const char *uuidstr, GError **error) +{ + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return NULL; + struct cbox_audio_output *audioout = cbox_io_get_audio_output(io, NULL, &uuid); + if (!audioout) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return NULL; + } + return audioout; +} + +gboolean cbox_io_destroy_audio_output(struct cbox_io *io, struct cbox_audio_output *audioout, GError **error) +{ + if (audioout->users) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' is in use", audioout->name); + return FALSE; + } + audioout->removing = TRUE; + + // This is not a very efficient way to do it. However, in this case, + // the list will rarely contain more than 10 elements, so simplicity + // and correctness may be more important. + GSList *copy = g_slist_copy(io->audio_outputs); + copy = g_slist_remove(copy, audioout); + + GSList *old = io->audio_outputs; + io->audio_outputs = copy; + + // Notify client code to disconnect the output and to make sure the RT code + // is not using the old list anymore + if (io->cb->on_audio_outputs_changed) + io->cb->on_audio_outputs_changed(io->cb->user_data); + + g_slist_free(old); + io->impl->destroyaudiooutfunc(io->impl, audioout); + return TRUE; +} + +gboolean cbox_io_process_cmd(struct cbox_io *io, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error, gboolean *cmd_handled) +{ + *cmd_handled = FALSE; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + *cmd_handled = TRUE; + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (GSList *p = io->midi_inputs; p; p = g_slist_next(p)) + { + struct cbox_midi_input *midiin = p->data; + if (!midiin->removing) + { + if (!cbox_execute_on(fb, NULL, "/midi_input", "su", error, midiin->name, &midiin->uuid)) + return FALSE; + } + } + for (GSList *p = io->midi_outputs; p; p = g_slist_next(p)) + { + struct cbox_midi_output *midiout = p->data; + if (!midiout->removing) + { + if (!cbox_execute_on(fb, NULL, "/midi_output", "su", error, midiout->name, &midiout->uuid)) + return FALSE; + } + } + return cbox_execute_on(fb, NULL, "/audio_inputs", "i", error, io->io_env.input_count) && + cbox_execute_on(fb, NULL, "/audio_outputs", "i", error, io->io_env.output_count) && + cbox_execute_on(fb, NULL, "/sample_rate", "i", error, io->io_env.srate) && + cbox_execute_on(fb, NULL, "/buffer_size", "i", error, io->io_env.buffer_size); + } + else if (io->impl->createmidiinfunc && !strcmp(cmd->command, "/create_midi_input") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + struct cbox_midi_input *midiin; + midiin = cbox_io_create_midi_input(io, CBOX_ARG_S(cmd, 0), error); + if (!midiin) + return FALSE; + cbox_midi_appsink_init(&midiin->appsink, app.rt, &app.engine->stmap->tmap); + return cbox_uuid_report(&midiin->uuid, fb, error); + } + else if (!strcmp(cmd->command, "/route_midi_input") && !strcmp(cmd->arg_types, "ss")) + { + *cmd_handled = TRUE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + if (!midiin) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + if (*CBOX_ARG_S(cmd, 1)) + { + if (cbox_uuid_fromstring(&midiin->output, CBOX_ARG_S(cmd, 1), error)) + midiin->output_set = TRUE; + } + else + midiin->output_set = FALSE; + if (io->impl->updatemidiinroutingfunc) + io->impl->updatemidiinroutingfunc(io->impl); + if (io->cb->on_midi_inputs_changed) + io->cb->on_midi_inputs_changed(io->cb->user_data); + return TRUE; + } + else if (!strcmp(cmd->command, "/set_appsink_for_midi_input") && !strcmp(cmd->arg_types, "si")) + { + *cmd_handled = TRUE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + if (!midiin) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + midiin->enable_appsink = CBOX_ARG_I(cmd, 1); + return TRUE; + } + else if (!strcmp(cmd->command, "/get_new_events") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + if (!midiin) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + if (!midiin->enable_appsink) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "App sink not enabled for port '%s'", uuidstr); + return FALSE; + } + return cbox_midi_appsink_send_to(&midiin->appsink, fb, error); + } + else if (io->impl->createmidioutfunc && !strcmp(cmd->command, "/create_midi_output") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + struct cbox_midi_output *midiout; + midiout = cbox_io_create_midi_output(io, CBOX_ARG_S(cmd, 0), error); + if (!midiout) + return FALSE; + return cbox_uuid_report(&midiout->uuid, fb, error); + } + else if (io->impl->destroymidiinfunc && !strcmp(cmd->command, "/delete_midi_input") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + if (!midiin) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + cbox_io_destroy_midi_input(io, midiin); + return TRUE; + } + else if (io->impl->destroymidioutfunc && !strcmp(cmd->command, "/delete_midi_output") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_output *midiout = cbox_io_get_midi_output(io, NULL, &uuid); + if (!midiout) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + cbox_io_destroy_midi_output(io, midiout); + return TRUE; + } + else if (io->impl->createaudiooutfunc && !strcmp(cmd->command, "/create_audio_output") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + struct cbox_audio_output *audioout; + audioout = cbox_io_create_audio_output(io, CBOX_ARG_S(cmd, 0), error); + if (!audioout) + return FALSE; + return cbox_uuid_report(&audioout->uuid, fb, error); + } + else if (io->impl->destroyaudiooutfunc && !strcmp(cmd->command, "/delete_audio_output") && !strcmp(cmd->arg_types, "s")) + { + *cmd_handled = TRUE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_audio_output *audioout = cbox_io_get_audio_output_by_uuid_string(io, uuidstr, error); + if (!audioout) + return FALSE; + return cbox_io_destroy_audio_output(io, audioout, error); + } + else if (io->impl->createmidioutfunc && !strcmp(cmd->command, "/create_audio_output_router") && !strcmp(cmd->arg_types, "ss")) + { + *cmd_handled = TRUE; + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_audio_output *left = cbox_io_get_audio_output_by_uuid_string(io, uuidstr, error); + if (!left) + return FALSE; + uuidstr = CBOX_ARG_S(cmd, 1); + struct cbox_audio_output *right = cbox_io_get_audio_output_by_uuid_string(io, uuidstr, error); + if (!right) + return FALSE; + // XXXKF hack alert + struct cbox_audio_output_router *router = cbox_io_create_audio_output_router(io, app.engine, left, right); + return cbox_uuid_report(&router->recorder._obj_hdr.instance_uuid, fb, error); + } + return FALSE; +} + +void cbox_io_close(struct cbox_io *io) +{ + io->impl->destroyfunc(io->impl); + io->impl = NULL; +} + diff --git a/template/calfbox/io.h b/template/calfbox/io.h new file mode 100644 index 0000000..f200c63 --- /dev/null +++ b/template/calfbox/io.h @@ -0,0 +1,194 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_IO_H +#define CBOX_IO_H + +#include +#include "config.h" +#if USE_JACK +#include +#endif +#include "dspmath.h" +#include "dom.h" +#include "ioenv.h" +#include "master.h" +#include "mididest.h" +#include "recsrc.h" + +struct cbox_io; +struct cbox_io_callbacks; +struct cbox_recording_source; +struct cbox_meter; +struct cbox_midi_buffer; +struct cbox_scene; + +struct cbox_open_params +{ +}; + +struct cbox_io_impl +{ + struct cbox_io *pio; + + int (*getsampleratefunc)(struct cbox_io_impl *ioi); + gboolean (*startfunc)(struct cbox_io_impl *ioi, struct cbox_command_target *fb, GError **error); + gboolean (*stopfunc)(struct cbox_io_impl *ioi, GError **error); + gboolean (*cyclefunc)(struct cbox_io_impl *ioi, struct cbox_command_target *fb, GError **error); + gboolean (*getstatusfunc)(struct cbox_io_impl *ioi, GError **error); + void (*pollfunc)(struct cbox_io_impl *ioi, struct cbox_command_target *fb); + int (*getmidifunc)(struct cbox_io_impl *ioi, struct cbox_midi_buffer *destination); + struct cbox_midi_output *(*createmidioutfunc)(struct cbox_io_impl *ioi, const char *name, GError **error); + void (*destroymidioutfunc)(struct cbox_io_impl *ioi, struct cbox_midi_output *midiout); + struct cbox_midi_input *(*createmidiinfunc)(struct cbox_io_impl *ioi, const char *name, GError **error); + void (*destroymidiinfunc)(struct cbox_io_impl *ioi, struct cbox_midi_input *midiout); + void (*updatemidiinroutingfunc)(struct cbox_io_impl *ioi); + struct cbox_audio_output *(*createaudiooutfunc)(struct cbox_io_impl *ioi, const char *name, GError **error); + void (*destroyaudiooutfunc)(struct cbox_io_impl *ioi, struct cbox_audio_output *audioout); + void (*controltransportfunc)(struct cbox_io_impl *ioi, gboolean roll, uint32_t pos); // (uint32_t)-1 if no change + gboolean (*getsynccompletedfunc)(struct cbox_io_impl *ioi); + void (*destroyfunc)(struct cbox_io_impl *ioi); +}; + +struct cbox_io +{ + struct cbox_io_impl *impl; + struct cbox_command_target cmd_target; + + float **input_buffers; // only valid inside jack_rt_process + float **output_buffers; // only valid inside jack_rt_process + struct cbox_io_env io_env; + + struct cbox_io_callbacks *cb; + GSList *midi_inputs; + GSList *midi_outputs; + GSList *audio_outputs; + uint32_t free_running_frame_counter; +}; + +enum cbox_transport_state +{ + ts_stopping, + ts_stopped, + ts_starting, + ts_rolling, +}; + +struct cbox_transport_position +{ + uint32_t bar; + uint32_t beat; + uint32_t tick; + uint32_t offset; + double tempo; + double ticks_per_beat; + double bar_start_tick; + uint32_t timesig_num; + uint32_t timesig_denom; +}; + +struct cbox_io_callbacks +{ + void *user_data; + + void (*process)(void *user_data, struct cbox_io *io, uint32_t nframes); + void (*on_started)(void *user_data); + void (*on_stopped)(void *user_data); + void (*on_disconnected)(void *user_data); + void (*on_reconnected)(void *user_data); + void (*on_midi_inputs_changed)(void *user_data); + void (*on_midi_outputs_changed)(void *user_data); + void (*on_audio_outputs_changed)(void *user_data); + gboolean (*on_transport_sync)(void *user_data, enum cbox_transport_state state, uint32_t frame); + void (*get_transport_data)(void *user_data, gboolean explicit_pos, uint32_t time_samples, struct cbox_transport_position *tp); + gboolean (*on_tempo_sync)(void *user_data, double beats_per_minute); +}; + +struct cbox_midi_input +{ + gchar *name; + struct cbox_uuid uuid; + struct cbox_midi_buffer buffer; + gboolean removing; + gboolean output_set; + struct cbox_uuid output; + gboolean enable_appsink; + struct cbox_midi_appsink appsink; +}; + +struct cbox_midi_output +{ + gchar *name; + struct cbox_uuid uuid; + struct cbox_midi_buffer buffer; + struct cbox_midi_merger merger; + // This is set if the output is in process of being removed and should not + // be used for output. + gboolean removing; +}; + +struct cbox_audio_output +{ + gchar *name; + struct cbox_uuid uuid; + // This is set if the output is in process of being removed and should not + // be used for output. + gboolean removing; + float *buffer; + uint32_t users; +}; + +struct cbox_audio_output_router +{ + struct cbox_recorder recorder; + struct cbox_recording_source *source; + struct cbox_audio_output *left, *right; + struct cbox_gain gain; + int attached; +}; + +extern gboolean cbox_io_init(struct cbox_io *io, struct cbox_open_params *const params, struct cbox_command_target *fb, GError **error); +#if USE_JACK +extern gboolean cbox_io_init_jack(struct cbox_io *io, struct cbox_open_params *const params, struct cbox_command_target *fb, GError **error); +#endif +extern gboolean cbox_io_init_usb(struct cbox_io *io, struct cbox_open_params *const params, struct cbox_command_target *fb, GError **error); + +extern int cbox_io_start(struct cbox_io *io, struct cbox_io_callbacks *cb, struct cbox_command_target *fb); +extern int cbox_io_stop(struct cbox_io *io); +extern int cbox_io_get_sample_rate(struct cbox_io *io); +extern int cbox_io_get_midi_data(struct cbox_io *io, struct cbox_midi_buffer *destination); +extern gboolean cbox_io_get_disconnect_status(struct cbox_io *io, GError **error); +extern gboolean cbox_io_cycle(struct cbox_io *io, struct cbox_command_target *fb, GError **error); +extern void cbox_io_poll_ports(struct cbox_io *io, struct cbox_command_target *fb); +extern struct cbox_midi_input *cbox_io_get_midi_input(struct cbox_io *io, const char *name, const struct cbox_uuid *uuid); +extern struct cbox_midi_output *cbox_io_get_midi_output(struct cbox_io *io, const char *name, const struct cbox_uuid *uuid); +extern struct cbox_audio_output *cbox_io_get_audio_output(struct cbox_io *io, const char *name, const struct cbox_uuid *uuid); +extern struct cbox_audio_output *cbox_io_get_audio_output_by_uuid_string(struct cbox_io *io, const char *uuidstr, GError **error); +extern struct cbox_midi_output *cbox_io_create_midi_output(struct cbox_io *io, const char *name, GError **error); +extern void cbox_io_destroy_midi_output(struct cbox_io *io, struct cbox_midi_output *midiout); +extern struct cbox_audio_output *cbox_io_create_audio_output(struct cbox_io *io, const char *name, GError **error); +extern gboolean cbox_io_destroy_audio_output(struct cbox_io *io, struct cbox_audio_output *audioout, GError **error); +extern struct cbox_midi_input *cbox_io_create_midi_input(struct cbox_io *io, const char *name, GError **error); +extern void cbox_io_destroy_midi_input(struct cbox_io *io, struct cbox_midi_input *midiin); +extern void cbox_io_destroy_all_midi_ports(struct cbox_io *io); +extern gboolean cbox_io_process_cmd(struct cbox_io *io, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error, gboolean *cmd_handled); +extern void cbox_io_close(struct cbox_io *io); + +extern const char *cbox_io_section; + +#endif diff --git a/template/calfbox/ioenv.h b/template/calfbox/ioenv.h new file mode 100644 index 0000000..9b0181f --- /dev/null +++ b/template/calfbox/ioenv.h @@ -0,0 +1,46 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_IOENV_H +#define CBOX_IOENV_H + +struct cbox_io_env +{ + int srate; + uint32_t buffer_size; + uint32_t input_count, output_count; +}; + +static inline void cbox_io_env_clear(struct cbox_io_env *env) +{ + env->srate = 0; + env->buffer_size = 0; + env->input_count = 0; + env->output_count = 0; +} + +static inline void cbox_io_env_copy(struct cbox_io_env *dest, const struct cbox_io_env *src) +{ + dest->srate = src->srate; + dest->buffer_size = src->buffer_size; + dest->input_count = src->input_count; + dest->output_count = src->output_count; +} + +#endif + diff --git a/template/calfbox/jack_api_example.py b/template/calfbox/jack_api_example.py new file mode 100644 index 0000000..be40d58 --- /dev/null +++ b/template/calfbox/jack_api_example.py @@ -0,0 +1,177 @@ +from calfbox import cbox +import time + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() + +cbox.Config.add_section("drumpattern:pat1", """ +title=Straight - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9... .... 9.6. .... +sd_trigger=.... 9..5 .2.. 9... +hh_trigger=9353 7353 7353 73.3 +ho_trigger=.... .... .... ..3. +""") + +cbox.Config.set("io", "use_usb", 0) +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +status = cbox.JackIO.status() +client_name = status.client_name +print ("Client name: %s" % client_name) +print ("Audio inputs: %d, outputs: %d" % (status.audio_inputs, status.audio_outputs)) +print ("Sample rate: %d frames/sec" % (status.sample_rate)) +print ("JACK period: %d frames" % (status.buffer_size)) +uuid_bad = cbox.JackIO.create_midi_output('bad') +uuid_bad2 = cbox.JackIO.create_midi_input('bad2') +cbox.JackIO.autoconnect_midi_output(uuid_bad, '%s:bad2' % client_name) +print (cbox.JackIO.get_connected_ports('%s:bad' % client_name)) +try: + cbox.JackIO.disconnect_midi_input(uuid_bad) + assert False +except: + pass +try: + cbox.JackIO.disconnect_midi_output(uuid_bad2) + assert False +except: + pass +assert len(cbox.JackIO.get_connected_ports('%s:bad' % client_name)) == 1 +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad) +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad2) +cbox.JackIO.disconnect_midi_output(uuid_bad) +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == [] +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad2) == [] + +cbox.JackIO.autoconnect_midi_output(uuid_bad2, '%s:bad' % client_name) +assert len(cbox.JackIO.get_connected_ports('%s:bad' % client_name)) == 1 +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad) +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad2) +cbox.JackIO.disconnect_midi_input(uuid_bad2) +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == [] +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad2) == [] + +cbox.JackIO.autoconnect_midi_output(uuid_bad2, '%s:bad' % client_name) +assert len(cbox.JackIO.get_connected_ports('%s:bad' % client_name)) == 1 +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad) +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad2) +cbox.JackIO.disconnect_midi_port(uuid_bad) +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == [] +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad2) == [] + +cbox.JackIO.autoconnect_midi_output(uuid_bad2, '%s:bad' % client_name) +assert len(cbox.JackIO.get_connected_ports('%s:bad' % client_name)) == 1 +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad) +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == cbox.JackIO.get_connected_ports(uuid_bad2) +cbox.JackIO.disconnect_midi_port(uuid_bad2) +assert cbox.JackIO.get_connected_ports('%s:bad' % client_name) == [] +assert cbox.JackIO.get_connected_ports('%s:bad2' % client_name) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad) == [] +assert cbox.JackIO.get_connected_ports(uuid_bad2) == [] + +cbox.JackIO.delete_midi_output(uuid_bad) +cbox.JackIO.delete_midi_input(uuid_bad2) + +uuid = cbox.JackIO.create_midi_output('drums') +cbox.JackIO.autoconnect_midi_output(uuid, '*alsa_pcm:.*') +cbox.JackIO.rename_midi_output(uuid, 'kettles') + +uuid_in = cbox.JackIO.create_midi_input('extra') +cbox.JackIO.autoconnect_midi_input(uuid_in, '*alsa_pcm:.*') +cbox.JackIO.rename_midi_input(uuid_in, 'extra_port') + +uuid2 = cbox.JackIO.create_midi_output('violins') + +print (cbox.JackIO.jack_transport_position()) +status = cbox.JackIO.status() +print ("Before deleting, MIDI outputs: %s" % status.midi_output) + +cbox.JackIO.delete_midi_output(uuid2) +status = cbox.JackIO.status() +print ("After deleting, MIDI outputs: %s" % status.midi_output) + +print ("Physical audio inputs: %s" % (",".join(cbox.JackIO.get_ports(".*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL)))) +print ("Physical audio outputs: %s" % (",".join(cbox.JackIO.get_ports(".*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SINK | cbox.JackIO.PORT_IS_PHYSICAL)))) +print ("Physical MIDI inputs: %s" % (",".join(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL)))) +print ("Physical MIDI outputs: %s" % (",".join(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SINK | cbox.JackIO.PORT_IS_PHYSICAL)))) + +inputs = cbox.JackIO.get_ports(".*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL) +outputs = cbox.JackIO.get_ports(".*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SINK | cbox.JackIO.PORT_IS_PHYSICAL) +cbox.JackIO.port_connect(inputs[0], outputs[0]) +cbox.JackIO.port_connect(inputs[1], outputs[1]) +#assert "cbox:in_3" in cbox.JackIO.get_connected_ports(inputs[0]) +cbox.JackIO.port_disconnect(inputs[0], outputs[0]) +cbox.JackIO.port_disconnect(inputs[1], outputs[1]) + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() +pgm_no = instrument.engine.get_unused_program() +pgm = instrument.engine.load_patch_from_file(pgm_no, 'synthbass.sfz', 'SynthBass') +instrument.engine.set_patch(1, pgm_no) +instrument.engine.set_patch(10, pgm_no) + +song = Document.get_song() +track = song.add_track() +track.set_external_output(uuid) +print ("Track outputs to: %s:%s" % (client_name, track.status().external_output)) +pattern = song.load_drum_pattern("pat1") +track.add_clip(0, 0, pattern.status().loop_end, pattern) +song.set_loop(0, pattern.status().loop_end) +song.update_playback() +cbox.Transport.play() +cbox.JackIO.transport_mode(True, False) +while not cbox.JackIO.jack_transport_position().is_master: + print ("Waiting to become the master") + time.sleep(0.01) +cbox.JackIO.transport_mode(False) +while cbox.JackIO.jack_transport_position().is_master: + print ("Waiting to stop being the master") + time.sleep(0.01) +cbox.JackIO.external_tempo(False) +assert cbox.JackIO.status().external_tempo == False +cbox.JackIO.external_tempo(True) +assert cbox.JackIO.status().external_tempo == True + +uuid3 = cbox.JackIO.create_audio_output('noises') +assert "cbox:noises" in cbox.JackIO.get_ports(".*:noises", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE) +cbox.JackIO.rename_audio_output(uuid3, "silence") +router = cbox.JackIO.create_audio_output_router(uuid3, uuid3) +assert type(router) is cbox.DocRecorder +try: + cbox.JackIO.delete_audio_output(uuid3) + assert False +except Exception as e: + assert 'is in use' in str(e) +router.delete() +assert "cbox:noises" not in cbox.JackIO.get_ports(".*:noises", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE) +assert "cbox:silence" in cbox.JackIO.get_ports(".*:silence", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE) +cbox.JackIO.delete_audio_output(uuid3) +assert "cbox:silence" not in cbox.JackIO.get_ports(".*:silence", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE) + +print("Ready!") + +while True: + events = cbox.get_new_events() + if events: + print (events) + time.sleep(0.05) diff --git a/template/calfbox/jack_audio_routing.py b/template/calfbox/jack_audio_routing.py new file mode 100644 index 0000000..17ebb94 --- /dev/null +++ b/template/calfbox/jack_audio_routing.py @@ -0,0 +1,65 @@ +from calfbox import cbox +import time + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() +cbox.Config.set("io", "use_usb", 0) +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +status = cbox.JackIO.status() +client_name = status.client_name +print ("Client name: %s" % client_name) +print ("Audio inputs: %d, outputs: %d" % (status.audio_inputs, status.audio_outputs)) +print ("Sample rate: %d frames/sec" % (status.sample_rate)) +print ("JACK period: %d frames" % (status.buffer_size)) + +inputs = cbox.JackIO.get_ports(".*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL) +outputs = cbox.JackIO.get_ports(".*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SINK | cbox.JackIO.PORT_IS_PHYSICAL) + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() +pgm_no = instrument.engine.get_unused_program() +pgm = instrument.engine.load_patch_from_file(pgm_no, 'synthbass.sfz', 'SynthBass') +instrument.engine.set_patch(1, pgm_no) +instrument.engine.set_patch(10, pgm_no) + +print ("Connecting") + +uuid = cbox.JackIO.create_audio_output('noises') +router = cbox.JackIO.create_audio_output_router(uuid, uuid) +assert type(router) is cbox.DocRecorder +router2 = cbox.JackIO.create_audio_output_router(uuid, uuid) +assert type(router2) is cbox.DocRecorder +instrument.get_output_slot(0).rec_wet.attach(router) +instrument.get_output_slot(0).rec_wet.attach(router2) + +exc = None +try: + instrument.get_output_slot(0).rec_wet.attach(router2) +except Exception as e: + exc = e +assert "Router already attached" in str(exc) + +instrument.get_output_slot(0).rec_wet.detach(router2) + +try: + instrument.get_output_slot(0).rec_wet.detach(router2) +except Exception as e: + exc = e +assert "Recorder is not attached" in str(exc) + +router.delete() + +print("Ready!") + +while True: + events = cbox.get_new_events() + if events: + print (events) + time.sleep(0.05) diff --git a/template/calfbox/jack_output_routing.py b/template/calfbox/jack_output_routing.py new file mode 100644 index 0000000..ba69bc7 --- /dev/null +++ b/template/calfbox/jack_output_routing.py @@ -0,0 +1,50 @@ +from calfbox import cbox +import time + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() + +cbox.Config.set("io", "use_usb", 0) +cbox.Config.set("io", "midi", "*.*") + +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +status = cbox.JackIO.status() +client_name = status.client_name +print ("Client name: %s" % client_name) +print ("Audio inputs: %d, outputs: %d" % (status.audio_inputs, status.audio_outputs)) +print ("Sample rate: %d frames/sec" % (status.sample_rate)) +print ("JACK period: %d frames" % (status.buffer_size)) + +left_dry = cbox.JackIO.create_audio_output('left_dry') +right_dry = cbox.JackIO.create_audio_output('right_dry') +left_wet = cbox.JackIO.create_audio_output('left_wet', '#1') +right_wet = cbox.JackIO.create_audio_output('right_wet', '#2') +router_dry = cbox.JackIO.create_audio_output_router(left_dry, right_dry) +assert type(router_dry) is cbox.DocRecorder +router_wet = cbox.JackIO.create_audio_output_router(left_wet, right_wet) +assert type(router_wet) is cbox.DocRecorder + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "tonewheel_organ").get_instrument() +instrument.get_output_slot(0).set_insert_engine("delay") +instrument.get_output_slot(0).rec_dry.attach(router_dry) +instrument.get_output_slot(0).rec_wet.attach(router_wet) +assert router_dry.uuid == instrument.get_output_slot(0).rec_dry.status().handler[0].uuid +assert router_wet.uuid == instrument.get_output_slot(0).rec_wet.status().handler[0].uuid +router_wet.set_gain(-3.0) +assert router_wet.status().gain == -3 + +print("Ready!") + +while True: + events = cbox.get_new_events() + if events: + print (events) + time.sleep(0.05) diff --git a/template/calfbox/jack_scene_routing.py b/template/calfbox/jack_scene_routing.py new file mode 100644 index 0000000..0ff0b8d --- /dev/null +++ b/template/calfbox/jack_scene_routing.py @@ -0,0 +1,36 @@ +from calfbox import cbox +import time + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() + +cbox.Config.set("io", "use_usb", 0) +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +status = cbox.JackIO.status() +client_name = status.client_name +print ("Client name: %s" % client_name) +print ("Audio inputs: %d, outputs: %d" % (status.audio_inputs, status.audio_outputs)) +print ("Sample rate: %d frames/sec" % (status.sample_rate)) +print ("JACK period: %d frames" % (status.buffer_size)) + +uuid_ext1 = cbox.JackIO.create_midi_output('ext1') +uuid_ext2 = cbox.JackIO.create_midi_output('ext2') + +scene = Document.get_scene() +scene.clear() +layer = scene.add_new_midi_layer(uuid_ext2) +#layer.set_external_output(uuid_ext1) + +print("Ready!") + +while True: + events = cbox.get_new_events() + if events: + print (events) + time.sleep(0.05) diff --git a/template/calfbox/jackinput.c b/template/calfbox/jackinput.c new file mode 100644 index 0000000..8db3b9c --- /dev/null +++ b/template/calfbox/jackinput.c @@ -0,0 +1,153 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#if USE_JACK + +struct jack_input_module +{ + struct cbox_module module; + + int inputs[2]; + int offset; +}; + +void jack_input_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct jack_input_module *m = module->user_data; +} + +void jack_input_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct jack_input_module *m = module->user_data; + + for (int i = 0; i < 2; i++) + { + if (m->inputs[i] < 0) + { + for (int j = 0; j < CBOX_BLOCK_SIZE; j++) + outputs[i][j] = 0; + } + else + { + float *src = module->rt->io->input_buffers[m->inputs[i]] + m->offset; + for (int j = 0; j < CBOX_BLOCK_SIZE; j++) + outputs[i][j] = src[j]; + } + } + m->offset = (m->offset + CBOX_BLOCK_SIZE) % app.io.io_env.buffer_size; +} + +static gboolean validate_input_index(int input, const char *cfg_section, const char *type, GError **error) +{ + if ((input < 1 || input > (int)app.io.io_env.input_count) && input != -1) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_OUT_OF_RANGE, "%s: invalid value for %s (%d), allowed values are 1..%d or -1 for unconnected", cfg_section, type, input, app.io.io_env.input_count); + return FALSE; + } + return TRUE; +} + +static void jack_input_destroyfunc(struct cbox_module *module) +{ +} + +static int to_base1(int val) +{ + if (val < 0) + return val; + return 1 + val; +} + +gboolean jack_input_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct jack_input_module *m = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/inputs", "ii", error, to_base1(m->inputs[0]), to_base1(m->inputs[1]))) + return FALSE; + return CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else if (!strcmp(cmd->command, "/inputs") && !strcmp(cmd->arg_types, "ii")) + { + int left_input = CBOX_ARG_I(cmd, 0); + int right_input = CBOX_ARG_I(cmd, 1); + if (!validate_input_index(left_input, "script", "left input", error)) + return FALSE; + if (!validate_input_index(right_input, "script", "right input", error)) + return FALSE; + m->inputs[0] = left_input < 0 ? -1 : left_input - 1; + m->inputs[1] = right_input < 0 ? -1 : right_input - 1; + return TRUE; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +MODULE_CREATE_FUNCTION(jack_input) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + int left_input = cbox_config_get_int(cfg_section, "left_input", 1); + int right_input = cbox_config_get_int(cfg_section, "right_input", 2); + if (!validate_input_index(left_input, cfg_section, "left_input", error)) + return NULL; + if (!validate_input_index(right_input, cfg_section, "right_input", error)) + return NULL; + + struct jack_input_module *m = malloc(sizeof(struct jack_input_module)); + CALL_MODULE_INIT(m, 0, 2, jack_input); + m->module.process_event = jack_input_process_event; + m->module.process_block = jack_input_process_block; + + m->inputs[0] = left_input - 1; + m->inputs[1] = right_input - 1; + m->offset = 0; + + return &m->module; +} + + +struct cbox_module_keyrange_metadata jack_input_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata jack_input_controllers[] = { +}; + +DEFINE_MODULE(jack_input, 0, 2) + +#endif diff --git a/template/calfbox/jackio.c b/template/calfbox/jackio.c new file mode 100644 index 0000000..4643b9a --- /dev/null +++ b/template/calfbox/jackio.c @@ -0,0 +1,1408 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2012 Krzysztof Foltman + +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 . +*/ + +#include "config.h" + +#if USE_JACK + +#include "app.h" +#include "config-api.h" +#include "errors.h" +#include "hwcfg.h" +#include "io.h" +#include "meter.h" +#include "midi.h" +#include "mididest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct cbox_jack_io_impl +{ + struct cbox_io_impl ioi; + + jack_client_t *client; + jack_port_t **inputs; + jack_port_t **outputs; + jack_port_t *midi; + char *error_str; // set to non-NULL if client has been booted out by JACK + char *client_name; + gboolean enable_common_midi_input; + jack_transport_state_t last_transport_state; + gboolean debug_transport; + gboolean external_tempo; + uint32_t master_detect_bits; + + jack_ringbuffer_t *rb_autoconnect; +}; + +/////////////////////////////////////////////////////////////////////////////// + +struct cbox_jack_midi_input +{ + struct cbox_midi_input hdr; + gchar *autoconnect_spec; + jack_port_t *port; + struct cbox_jack_io_impl *jii; +}; + +struct cbox_jack_midi_output +{ + struct cbox_midi_output hdr; + gchar *autoconnect_spec; + jack_port_t *port; + struct cbox_jack_io_impl *jii; +}; + +struct cbox_jack_audio_output +{ + struct cbox_audio_output hdr; + gchar *autoconnect_spec; + jack_port_t *port; + struct cbox_jack_io_impl *jii; +}; + +static struct cbox_midi_input *cbox_jackio_create_midi_in(struct cbox_io_impl *impl, const char *name, GError **error); +static struct cbox_midi_output *cbox_jackio_create_midi_out(struct cbox_io_impl *impl, const char *name, GError **error); +static struct cbox_audio_output *cbox_jackio_create_audio_out(struct cbox_io_impl *impl, const char *name, GError **error); +static void cbox_jackio_destroy_midi_in(struct cbox_io_impl *ioi, struct cbox_midi_input *midiin); +static void cbox_jackio_destroy_midi_out(struct cbox_io_impl *ioi, struct cbox_midi_output *midiout); +static void cbox_jackio_destroy_audio_out(struct cbox_io_impl *ioi, struct cbox_audio_output *audioout); +static void cbox_jack_midi_output_set_autoconnect(struct cbox_jack_midi_output *jmo, const gchar *autoconnect_spec); +static void cbox_jack_audio_output_set_autoconnect(struct cbox_jack_audio_output *jao, const gchar *autoconnect_spec); + +static const char *transport_state_names[] = {"Stopped", "Rolling", "Looping?", "Starting", "Unknown/invalid#4", "Unknown/invalid#5", "Unknown/invalid#6" }; + +void cbox_jack_midi_input_destroy(struct cbox_jack_midi_input *jmi) +{ + if (jmi->port) + { + jack_port_unregister(jmi->jii->client, jmi->port); + jmi->port = NULL; + } + g_free(jmi->hdr.name); + g_free(jmi->autoconnect_spec); + free(jmi); +} + +void cbox_jack_midi_output_destroy(struct cbox_jack_midi_output *jmo) +{ + if (jmo->port) + { + jack_port_unregister(jmo->jii->client, jmo->port); + jmo->port = NULL; + } + g_free(jmo->hdr.name); + g_free(jmo->autoconnect_spec); + assert(!jmo->hdr.merger.inputs); + free(jmo); +} + +void cbox_jack_audio_output_destroy(struct cbox_jack_audio_output *jao) +{ + if (jao->port) + { + jack_port_unregister(jao->jii->client, jao->port); + jao->port = NULL; + } + g_free(jao->hdr.name); + g_free(jao->autoconnect_spec); + free(jao); +} + +/////////////////////////////////////////////////////////////////////////////// + +static int copy_midi_data_to_buffer(jack_port_t *port, int buffer_size, struct cbox_midi_buffer *destination) +{ + void *midi = jack_port_get_buffer(port, buffer_size); + if (!midi) + return 0; + uint32_t event_count = jack_midi_get_event_count(midi); + + cbox_midi_buffer_clear(destination); + for (uint32_t i = 0; i < event_count; i++) + { + jack_midi_event_t event; + + if (!jack_midi_event_get(&event, midi, i)) + { + if (!cbox_midi_buffer_write_event(destination, event.time, event.buffer, event.size)) + return -i; + } + else + return -i; + } + + return event_count; +} + +/////////////////////////////////////////////////////////////////////////////// + +static int process_cb(jack_nframes_t nframes, void *arg) +{ + struct cbox_jack_io_impl *jii = arg; + struct cbox_io *io = jii->ioi.pio; + struct cbox_io_callbacks *cb = io->cb; + + io->io_env.buffer_size = nframes; + for (uint32_t i = 0; i < io->io_env.input_count; i++) + io->input_buffers[i] = jack_port_get_buffer(jii->inputs[i], nframes); + for (uint32_t i = 0; i < io->io_env.output_count; i++) + { + io->output_buffers[i] = jack_port_get_buffer(jii->outputs[i], nframes); + if (!io->output_buffers[i]) + continue; + for (uint32_t j = 0; j < nframes; j ++) + io->output_buffers[i][j] = 0.f; + } + if (cb->on_transport_sync || (jii->external_tempo && cb->on_tempo_sync)) { + jack_position_t pos; + memset(&pos, 0, sizeof(pos)); + jack_transport_state_t state = jack_transport_query(jii->client, &pos); + if (jii->external_tempo && cb->on_tempo_sync && (pos.valid & JackPositionBBT) && pos.beats_per_minute > 0) { + cb->on_tempo_sync(cb->user_data, pos.beats_per_minute); + } + if (cb->on_transport_sync) + { + if (state != jii->last_transport_state) + { + jack_position_t pos; + jack_transport_query(jii->client, &pos); + if (jii->debug_transport) + g_message("JACK transport: incoming state change, state = %s, last state = %s, pos = %d\n", transport_state_names[state], transport_state_names[(int)jii->last_transport_state], (int)pos.frame); + if (state == JackTransportStopped) + { + if (cb->on_transport_sync(cb->user_data, ts_stopping, pos.frame)) + jii->last_transport_state = state; + } + else + if (state == JackTransportRolling && jii->last_transport_state == JackTransportStarting) + { + if (cb->on_transport_sync(cb->user_data, ts_rolling, pos.frame)) + jii->last_transport_state = state; + } + else + jii->last_transport_state = state; + } + } + } + for (GSList *p = io->midi_inputs; p; p = p->next) + { + struct cbox_jack_midi_input *input = p->data; + if (input->hdr.output_set || input->hdr.enable_appsink) + { + copy_midi_data_to_buffer(input->port, io->io_env.buffer_size, &input->hdr.buffer); + if (input->hdr.enable_appsink) + cbox_midi_appsink_supply(&input->hdr.appsink, &input->hdr.buffer, io->free_running_frame_counter); + } + else + cbox_midi_buffer_clear(&input->hdr.buffer); + } + for (GSList *p = io->audio_outputs; p; p = p->next) + { + struct cbox_jack_audio_output *output = p->data; + float *buffer = jack_port_get_buffer(output->port, nframes); + output->hdr.buffer = buffer; + for (uint32_t j = 0; j < nframes; j ++) + buffer[j] = 0.f; + } + cb->process(cb->user_data, io, nframes); + for (uint32_t i = 0; i < io->io_env.input_count; i++) + io->input_buffers[i] = NULL; + for (uint32_t i = 0; i < io->io_env.output_count; i++) + io->output_buffers[i] = NULL; + for (GSList *p = io->midi_outputs; p; p = g_slist_next(p)) + { + struct cbox_jack_midi_output *midiout = p->data; + + void *pbuf = jack_port_get_buffer(midiout->port, nframes); + jack_midi_clear_buffer(pbuf); + + cbox_midi_merger_render(&midiout->hdr.merger); + if (midiout->hdr.buffer.count) + { + uint8_t tmp_data[4]; + for (uint32_t i = 0; i < midiout->hdr.buffer.count; i++) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(&midiout->hdr.buffer, i); + const uint8_t *pdata = cbox_midi_event_get_data(event); + if ((pdata[0] & 0xF0) == 0x90 && !pdata[2] && event->size == 3) + { + tmp_data[0] = pdata[0] & ~0x10; + tmp_data[1] = pdata[1]; + tmp_data[2] = pdata[2]; + pdata = tmp_data; + } + if (jack_midi_event_write(pbuf, event->time, pdata, event->size)) + { + g_warning("MIDI buffer overflow on JACK output port '%s'", midiout->hdr.name); + break; + } + } + } + } + io->free_running_frame_counter += nframes; + jii->master_detect_bits <<= 1; + return 0; +} + +static void autoconnect_port(jack_client_t *client, const char *port, const char *use_name, int is_cbox_input, const jack_port_t *only_connect_port, struct cbox_command_target *fb) +{ + int res; + if (only_connect_port) + { + jack_port_t *right; + right = jack_port_by_name(client, use_name); + if (only_connect_port != right) + return; + } + + const char *pfrom = is_cbox_input ? use_name : port; + const char *pto = !is_cbox_input ? use_name : port; + + res = jack_connect(client, pfrom, pto); + if (res == EEXIST) + res = 0; + gboolean suppressed = FALSE; + if (fb) + { + if (!res) + suppressed = cbox_execute_on(fb, NULL, "/io/jack/connected", "ss", NULL, pfrom, pto); + else + suppressed = cbox_execute_on(fb, NULL, "/io/jack/connect_failed", "sss", NULL, pfrom, pto, (res == EEXIST ? "already connected" : "failed")); + } + if (!suppressed) + g_message("Connect: %s %s %s (%s)", port, is_cbox_input ? "<-" : "->", use_name, res == 0 ? "success" : (res == EEXIST ? "already connected" : "failed")); +} + +static void autoconnect_by_spec(jack_client_t *client, const char *port, const char *orig_spec, int is_cbox_input, int is_midi, const jack_port_t *only_connect_port, struct cbox_command_target *fb) +{ + char *name, *copy_spec, *dpos; + const char *use_name; + + copy_spec = g_strdup(orig_spec); + name = copy_spec; + do { + dpos = strchr(name, ';'); + if (dpos) + *dpos = '\0'; + + use_name = name; + if (use_name[0] == '#') + { + char *endptr = NULL; + long portidx = strtol(use_name + 1, &endptr, 10) - 1; + if (endptr == use_name + strlen(use_name)) + { + const char **names = jack_get_ports(client, ".*", is_midi ? JACK_DEFAULT_MIDI_TYPE : JACK_DEFAULT_AUDIO_TYPE, is_cbox_input ? JackPortIsOutput : JackPortIsInput); + // Client killed by JACK + if (!names) { + g_free(copy_spec); + return; + } + int i; + for (i = 0; i < portidx && names[i]; i++) + ; + + if (names[i]) + autoconnect_port(client, port, names[i], is_cbox_input, only_connect_port, fb); + else + g_message("Connect: unmatched port index %d for autoconnecting %s", (int)portidx, port); + + jack_free(names); + } + } + else if (use_name[0] == '~' || use_name[0] == '*') + { + const char **names = jack_get_ports(client, use_name + 1, is_midi ? JACK_DEFAULT_MIDI_TYPE : JACK_DEFAULT_AUDIO_TYPE, is_cbox_input ? JackPortIsOutput : JackPortIsInput); + // Client killed by JACK + if (!names) { + g_free(copy_spec); + return; + } + + if (names && names[0]) + { + if (use_name[0] == '*') + { + int i; + for (i = 0; names[i]; i++) + autoconnect_port(client, port, names[i], is_cbox_input, only_connect_port, fb); + } + else + autoconnect_port(client, port, names[0], is_cbox_input, only_connect_port, fb); + } + else + g_message("Connect: unmatched port regexp %s", use_name); + jack_free(names); + } + else + autoconnect_port(client, port, use_name, is_cbox_input, only_connect_port, fb); + + if (dpos) + name = dpos + 1; + } while(dpos); + g_free(copy_spec); +} + +static void autoconnect_by_var(jack_client_t *client, const char *port, const char *config_var, int is_cbox_input, int is_midi, const jack_port_t *only_connect_port, struct cbox_command_target *fb) +{ + const char *orig_spec = cbox_config_get_string(cbox_io_section, config_var); + if (orig_spec) + autoconnect_by_spec(client, port, orig_spec, is_cbox_input, is_midi, only_connect_port, fb); +} + +static void port_connect_cb(jack_port_id_t port, int registered, void *arg) +{ + struct cbox_jack_io_impl *jii = arg; + if (registered) + { + jack_port_t *portobj = jack_port_by_id(jii->client, port); + + jack_ringbuffer_write(jii->rb_autoconnect, (char *)&portobj, sizeof(portobj)); + } +} + +static void port_autoconnect(struct cbox_jack_io_impl *jii, jack_port_t *portobj, struct cbox_command_target *fb) +{ + struct cbox_io *io = jii->ioi.pio; + + for (uint32_t i = 0; i < io->io_env.output_count; i++) + { + gchar *cbox_port = g_strdup_printf("%s:out_%d", jii->client_name, 1 + i); + gchar *config_key = g_strdup_printf("out_%d", 1 + i); + autoconnect_by_var(jii->client, cbox_port, config_key, 0, 0, portobj, fb); + g_free(cbox_port); + g_free(config_key); + } + for (uint32_t i = 0; i < io->io_env.input_count; i++) + { + gchar *cbox_port = g_strdup_printf("%s:in_%d", jii->client_name, 1 + i); + gchar *config_key = g_strdup_printf("in_%d", 1 + i); + autoconnect_by_var(jii->client, cbox_port, config_key, 1, 0, portobj, fb); + g_free(cbox_port); + g_free(config_key); + } + for (GSList *p = io->midi_outputs; p; p = g_slist_next(p)) + { + struct cbox_jack_midi_output *midiout = p->data; + if (midiout->autoconnect_spec) + { + gchar *cbox_port = g_strdup_printf("%s:%s", jii->client_name, midiout->hdr.name); + autoconnect_by_spec(jii->client, cbox_port, midiout->autoconnect_spec, FALSE, TRUE, portobj, fb); + g_free(cbox_port); + } + } + for (GSList *p = io->midi_inputs; p; p = g_slist_next(p)) + { + struct cbox_jack_midi_input *midiin = p->data; + if (midiin->autoconnect_spec) + { + gchar *cbox_port = g_strdup_printf("%s:%s", jii->client_name, midiin->hdr.name); + autoconnect_by_spec(jii->client, cbox_port, midiin->autoconnect_spec, TRUE, TRUE, portobj, fb); + g_free(cbox_port); + } + } + for (GSList *p = io->audio_outputs; p; p = g_slist_next(p)) + { + struct cbox_jack_audio_output *audioout = p->data; + if (audioout->autoconnect_spec) + { + gchar *cbox_port = g_strdup_printf("%s:%s", jii->client_name, audioout->hdr.name); + autoconnect_by_spec(jii->client, cbox_port, audioout->autoconnect_spec, FALSE, FALSE, portobj, fb); + g_free(cbox_port); + } + } + gchar *cbox_port = g_strdup_printf("%s:midi", jii->client_name); + autoconnect_by_var(jii->client, cbox_port, "midi", 1, 1, portobj, fb); + g_free(cbox_port); +} + +int cbox_jackio_get_sample_rate(struct cbox_io_impl *impl) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + + return jack_get_sample_rate(jii->client); +} + +gboolean cbox_jackio_get_status(struct cbox_io_impl *impl, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + if (!jii->error_str) + return TRUE; + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "%s", jii->error_str); + return FALSE; +} + +static void client_shutdown_cb(jack_status_t code, const char *reason, void *arg) +{ + struct cbox_jack_io_impl *jii = arg; + struct cbox_io *io = jii->ioi.pio; + jii->error_str = g_strdup(reason); + if (io->cb && io->cb->on_disconnected) + (io->cb->on_disconnected)(io->cb->user_data); +} + +static void timebase_cb(jack_transport_state_t state, jack_nframes_t nframes, + jack_position_t *pos, int new_pos, void *arg) +{ + struct cbox_jack_io_impl *jii = arg; + struct cbox_io *io = jii->ioi.pio; + + jii->master_detect_bits |= 1; + if (io->cb->get_transport_data) { + struct cbox_transport_position tpos; + + io->cb->get_transport_data(io->cb->user_data, new_pos, pos->frame, &tpos); + pos->valid = JackPositionBBT; + pos->bar = tpos.bar; + pos->beat = tpos.beat; + pos->tick = tpos.tick; + pos->bar_start_tick = tpos.bar_start_tick; + pos->ticks_per_beat = tpos.ticks_per_beat; + pos->beats_per_minute = tpos.tempo; + pos->beats_per_bar = tpos.timesig_num; + pos->beat_type = tpos.timesig_denom; + } +} + +static int sync_cb(jack_transport_state_t state, jack_position_t *pos, void *arg) +{ + struct cbox_jack_io_impl *jii = arg; + struct cbox_io *io = jii->ioi.pio; + gboolean result = TRUE; + int last_state = jii->last_transport_state; + switch(state) + { + case JackTransportStopped: + result = io->cb->on_transport_sync(io->cb->user_data, ts_stopped, pos->frame); + break; + case JackTransportStarting: + jii->last_transport_state = JackTransportStarting; + result = io->cb->on_transport_sync(io->cb->user_data, ts_starting, pos->frame); + break; + case JackTransportRolling: + result = io->cb->on_transport_sync(io->cb->user_data, ts_rolling, pos->frame); + break; + default: + // assume the client is ready + result = TRUE; + } + if (jii->debug_transport) + g_message("JACK transport: incoming sync callback, state = %s, last state = %s, pos = %d, result = %d\n", transport_state_names[state], transport_state_names[last_state], (int)pos->frame, result); + return result; +} + +gboolean cbox_jackio_set_master_mode(struct cbox_jack_io_impl *jii, int mode, GError **error) +{ + if (jii->ioi.pio->cb->get_transport_data) + { + if (mode) { + switch(jack_set_timebase_callback(jii->client, mode == 1, timebase_cb, jii)) + { + case 0: + return TRUE; + case EBUSY: + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Timebase master already set"); + return FALSE; + default: + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Timebase master could not be set"); + return FALSE; + + } + } else { + switch(jack_release_timebase(jii->client)) + { + case 0: + return TRUE; + case EINVAL: + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Not a current timebase master"); + return FALSE; + default: + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Timebase master could not be unset"); + return FALSE; + } + } + } + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Transport not supported by engine"); + return FALSE; +} + +gboolean cbox_jackio_start(struct cbox_io_impl *impl, struct cbox_command_target *fb, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + struct cbox_io *io = jii->ioi.pio; + + if (io->cb->on_transport_sync) + jack_set_sync_callback(jii->client, sync_cb, jii); + jack_set_process_callback(jii->client, process_cb, jii); + jack_set_port_registration_callback(jii->client, port_connect_cb, jii); + jack_on_info_shutdown(jii->client, client_shutdown_cb, jii); + + if (io->cb->on_started) + io->cb->on_started(io->cb->user_data); + + jack_activate(jii->client); + + if (cbox_config_has_section(cbox_io_section)) + port_autoconnect(jii, NULL, fb); + + return TRUE; +} + +gboolean cbox_jackio_stop(struct cbox_io_impl *impl, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + + if (jii->error_str) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "%s", jii->error_str); + return FALSE; + } + int result = jack_deactivate(jii->client); + if (result) + g_warning("jack_deactivate has failed, result = %d", result); + jack_release_timebase(jii->client); + jack_set_process_callback(jii->client, NULL, 0); + return TRUE; +} + +void cbox_jackio_poll_ports(struct cbox_io_impl *impl, struct cbox_command_target *fb) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + + while (jack_ringbuffer_read_space(jii->rb_autoconnect) >= sizeof(jack_port_t *)) + { + jack_port_t *portobj; + jack_ringbuffer_read(jii->rb_autoconnect, (char *)&portobj, sizeof(portobj)); + port_autoconnect(jii, portobj, fb); + } +} + +int cbox_jackio_get_midi_data(struct cbox_io_impl *impl, struct cbox_midi_buffer *destination) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + if (!jii->enable_common_midi_input) + { + cbox_midi_buffer_clear(destination); + return 1; + } + + return copy_midi_data_to_buffer(jii->midi, jii->ioi.pio->io_env.buffer_size, destination); +} + +void cbox_jackio_destroy(struct cbox_io_impl *impl) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + struct cbox_io *io = impl->pio; + if (jii->client) + { + if (jii->error_str) + { + g_free(jii->error_str); + jii->error_str = NULL; + } + else + { + for (uint32_t i = 0; i < io->io_env.input_count; i++) + jack_port_unregister(jii->client, jii->inputs[i]); + free(jii->inputs); + for (uint32_t i = 0; i < io->io_env.output_count; i++) + jack_port_unregister(jii->client, jii->outputs[i]); + free(jii->outputs); + if (jii->midi) + jack_port_unregister(jii->client, jii->midi); + } + if (jii->client_name) + { + free(jii->client_name); + jii->client_name = NULL; + } + cbox_io_destroy_all_midi_ports(io); + + jack_ringbuffer_free(jii->rb_autoconnect); + jack_client_close(jii->client); + } + free(jii); +} + +gboolean cbox_jackio_cycle(struct cbox_io_impl *impl, struct cbox_command_target *fb, GError **error) +{ + struct cbox_io *io = impl->pio; + struct cbox_io_callbacks *cb = io->cb; + cbox_io_close(io); + + // XXXKF use params structure some day + if (!cbox_io_init_jack(io, NULL, fb, error)) + return FALSE; + + cbox_io_start(io, cb, fb); + if (cb->on_reconnected) + (cb->on_reconnected)(cb->user_data); + return TRUE; +} + + + +/////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_input *cbox_jackio_create_midi_in(struct cbox_io_impl *impl, const char *name, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + jack_port_t *port = jack_port_register(jii->client, name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + if (!port) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create input MIDI port '%s'", name); + return FALSE; + } + struct cbox_jack_midi_input *input = calloc(1, sizeof(struct cbox_jack_midi_input)); + input->hdr.name = g_strdup(name); + input->hdr.removing = FALSE; + input->port = port; + input->jii = jii; + cbox_uuid_generate(&input->hdr.uuid); + cbox_midi_buffer_init(&input->hdr.buffer); + + return (struct cbox_midi_input *)input; +} + +struct cbox_midi_output *cbox_jackio_create_midi_out(struct cbox_io_impl *impl, const char *name, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + jack_port_t *port = jack_port_register(jii->client, name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); + if (!port) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create output MIDI port '%s'", name); + return FALSE; + } + struct cbox_jack_midi_output *output = calloc(1, sizeof(struct cbox_jack_midi_output)); + output->hdr.name = g_strdup(name); + output->hdr.removing = FALSE; + output->port = port; + output->jii = jii; + cbox_uuid_generate(&output->hdr.uuid); + cbox_midi_buffer_init(&output->hdr.buffer); + cbox_midi_merger_init(&output->hdr.merger, &output->hdr.buffer); + + return (struct cbox_midi_output *)output; +} + +struct cbox_audio_output *cbox_jackio_create_audio_out(struct cbox_io_impl *impl, const char *name, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + jack_port_t *port = jack_port_register(jii->client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + if (!port) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create output audio port '%s'", name); + return FALSE; + } + struct cbox_jack_audio_output *output = calloc(1, sizeof(struct cbox_jack_audio_output)); + output->hdr.name = g_strdup(name); + output->hdr.removing = FALSE; + output->hdr.users = 0; + output->port = port; + output->jii = jii; + cbox_uuid_generate(&output->hdr.uuid); + + return (struct cbox_audio_output *)output; +} + +void cbox_jack_port_set_autoconnect(gchar **spec_ptr, const gchar *autoconnect_spec, struct cbox_jack_io_impl *jii, const gchar *port_name, gboolean is_cbox_input, gboolean is_midi) +{ + if (*spec_ptr) + g_free(*spec_ptr); + *spec_ptr = autoconnect_spec && *autoconnect_spec ? g_strdup(autoconnect_spec) : NULL; + if (*spec_ptr) + { + gchar *cbox_port = g_strdup_printf("%s:%s", jii->client_name, port_name); + autoconnect_by_spec(jii->client, cbox_port, *spec_ptr, is_cbox_input, is_midi, NULL, NULL); + g_free(cbox_port); + } +} + +void cbox_jack_midi_input_set_autoconnect(struct cbox_jack_midi_input *jmi, const gchar *autoconnect_spec) +{ + cbox_jack_port_set_autoconnect(&jmi->autoconnect_spec, autoconnect_spec, jmi->jii, jmi->hdr.name, TRUE, TRUE); +} + +void cbox_jack_midi_output_set_autoconnect(struct cbox_jack_midi_output *jmo, const gchar *autoconnect_spec) +{ + cbox_jack_port_set_autoconnect(&jmo->autoconnect_spec, autoconnect_spec, jmo->jii, jmo->hdr.name, FALSE, TRUE); +} + +void cbox_jack_audio_output_set_autoconnect(struct cbox_jack_audio_output *jao, const gchar *autoconnect_spec) +{ + cbox_jack_port_set_autoconnect(&jao->autoconnect_spec, autoconnect_spec, jao->jii, jao->hdr.name, FALSE, FALSE); +} + +void cbox_jackio_destroy_midi_in(struct cbox_io_impl *ioi, struct cbox_midi_input *midiin) +{ + cbox_jack_midi_input_destroy((struct cbox_jack_midi_input *)midiin); +} + +void cbox_jackio_destroy_midi_out(struct cbox_io_impl *ioi, struct cbox_midi_output *midiout) +{ + cbox_jack_midi_output_destroy((struct cbox_jack_midi_output *)midiout); +} + +void cbox_jackio_destroy_audio_out(struct cbox_io_impl *ioi, struct cbox_audio_output *audioout) +{ + cbox_jack_audio_output_destroy((struct cbox_jack_audio_output *)audioout); +} + +#if JACK_HAS_RENAME +#define jack_port_rename_fn jack_port_rename +#else +#define jack_port_rename_fn(client, handle, name) jack_port_set_name(handle, name) +#endif + + +static gboolean cbox_jack_io_get_jack_uuid_from_name(struct cbox_jack_io_impl *jii, const char *name, jack_uuid_t *uuid, GError **error) +{ + jack_port_t *port = NULL; + port = jack_port_by_name(jii->client, name); + if (!port) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", name); + return FALSE; + } + assert(uuid); + jack_uuid_t temp_uuid = jack_port_uuid(port); + if (!temp_uuid) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "JACK uuid for port '%s' not found", name); + return FALSE; + } + *uuid = temp_uuid; + return TRUE; +} + +static gboolean cbox_jack_io_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)ct->user_data; + struct cbox_io *io = jii->ioi.pio; + gboolean handled = FALSE; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/client_type", "s", error, "JACK") && + cbox_execute_on(fb, NULL, "/client_name", "s", error, jii->client_name) && + cbox_execute_on(fb, NULL, "/external_tempo", "i", error, jii->external_tempo) && + cbox_io_process_cmd(io, fb, cmd, error, &handled); + } + else if (!strcmp(cmd->command, "/jack_transport_position") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + jack_position_t pos; + memset(&pos, 0, sizeof(pos)); + jack_transport_state_t state = jack_transport_query(jii->client, &pos); + if (!(cbox_execute_on(fb, NULL, "/state", "i", error, (int)state) && + cbox_execute_on(fb, NULL, "/is_master", "i", error, jii->master_detect_bits & 2 ? 1 : 0) && + cbox_execute_on(fb, NULL, "/unique_lo", "i", error, (int)pos.unique_1) && + cbox_execute_on(fb, NULL, "/unique_hi", "i", error, (int)(pos.unique_1 >> 32)) && + cbox_execute_on(fb, NULL, "/usecs_lo", "i", error, (int)pos.usecs) && + cbox_execute_on(fb, NULL, "/usecs_hi", "i", error, (int)(pos.usecs >> 32)) && + cbox_execute_on(fb, NULL, "/frame_rate", "i", error, (int)(pos.frame_rate)) && + cbox_execute_on(fb, NULL, "/frame", "i", error, (int)(pos.frame)))) + return FALSE; + if ((pos.valid & JackPositionBBT) && !( + cbox_execute_on(fb, NULL, "/bar", "i", error, (int)pos.bar) && + cbox_execute_on(fb, NULL, "/beat", "i", error, (int)pos.beat) && + cbox_execute_on(fb, NULL, "/tick", "i", error, (int)pos.tick) && + cbox_execute_on(fb, NULL, "/bar_start_tick", "f", error, (int)pos.bar_start_tick) && + cbox_execute_on(fb, NULL, "/beats_per_bar", "f", error, (double)pos.beats_per_bar) && + cbox_execute_on(fb, NULL, "/beat_type", "f", error, (double)pos.beat_type) && + cbox_execute_on(fb, NULL, "/ticks_per_beat", "f", error, (double)pos.ticks_per_beat) && + cbox_execute_on(fb, NULL, "/beats_per_minute", "f", error, (double)pos.beats_per_minute))) + return FALSE; + if ((pos.valid & JackBBTFrameOffset) && !( + cbox_execute_on(fb, NULL, "/bbt_frame_offset", "i", error, (int)pos.bbt_offset))) + return FALSE; + return TRUE; + } + else if (!strcmp(cmd->command, "/transport_mode") && !strcmp(cmd->arg_types, "i")) + { + return cbox_jackio_set_master_mode(jii, CBOX_ARG_I(cmd, 0), error); + } + else if (!strcmp(cmd->command, "/jack_transport_locate") && !strcmp(cmd->arg_types, "i")) + { + jack_transport_locate(jii->client, (uint32_t)CBOX_ARG_I(cmd, 0)); + return TRUE; + } + else if (!strcmp(cmd->command, "/rename_midi_port") && !strcmp(cmd->arg_types, "ss")) + { + const char *uuidstr = CBOX_ARG_S(cmd, 0); + const char *new_name = CBOX_ARG_S(cmd, 1); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + struct cbox_midi_output *midiout = cbox_io_get_midi_output(io, NULL, &uuid); + if (!midiout && !midiin) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + jack_port_t *port = midiout ? ((struct cbox_jack_midi_output *)midiout)->port + : ((struct cbox_jack_midi_input *)midiin)->port; + char **pname = midiout ? &midiout->name : &midiin->name; + if (0 != jack_port_rename_fn(jii->client, port, new_name)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot set port name to '%s'", new_name); + return FALSE; + } + g_free(*pname); + *pname = g_strdup(new_name); + return TRUE; + } + else if (!strcmp(cmd->command, "/rename_audio_port") && !strcmp(cmd->arg_types, "ss")) + { + const char *uuidstr = CBOX_ARG_S(cmd, 0); + const char *new_name = CBOX_ARG_S(cmd, 1); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_audio_output *audioout = cbox_io_get_audio_output(io, NULL, &uuid); + if (!audioout) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + if (0 != jack_port_rename_fn(jii->client, ((struct cbox_jack_audio_output *)audioout)->port, new_name)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot set port name to '%s'", new_name); + return FALSE; + } + g_free(audioout->name); + audioout->name = g_strdup(new_name); + return TRUE; + } + else if (!strcmp(cmd->command, "/autoconnect") && !strcmp(cmd->arg_types, "ss")) + { + const char *uuidstr = CBOX_ARG_S(cmd, 0); + const char *spec = CBOX_ARG_S(cmd, 1); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_output *midiout = cbox_io_get_midi_output(io, NULL, &uuid); + if (midiout) + { + cbox_jack_midi_output_set_autoconnect((struct cbox_jack_midi_output *)midiout, spec); + return TRUE; + } + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + if (midiin) + { + cbox_jack_midi_input_set_autoconnect((struct cbox_jack_midi_input *)midiin, spec); + return TRUE; + } + struct cbox_audio_output *audioout = cbox_io_get_audio_output(io, NULL, &uuid); + if (audioout) + { + cbox_jack_audio_output_set_autoconnect((struct cbox_jack_audio_output *)audioout, spec); + return TRUE; + } + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + else if (!strcmp(cmd->command, "/disconnect_audio_output") && !strcmp(cmd->arg_types, "s")) + { + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_audio_output *audioout = cbox_io_get_audio_output(io, NULL, &uuid); + if (!audioout) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + jack_port_disconnect(jii->client, ((struct cbox_jack_audio_output *)audioout)->port); + return TRUE; + } + else if (!strncmp(cmd->command, "/disconnect_midi_", 17) && !strcmp(cmd->arg_types, "s")) + { + bool is_both = !strcmp(cmd->command + 17, "port"); + bool is_in = is_both || !strcmp(cmd->command + 17, "input"); + bool is_out = is_both || !strcmp(cmd->command + 17, "output"); + if (is_in || is_out) { + const char *uuidstr = CBOX_ARG_S(cmd, 0); + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, uuidstr, error)) + return FALSE; + struct cbox_midi_input *midiin = is_in ? cbox_io_get_midi_input(io, NULL, &uuid) : NULL; + struct cbox_midi_output *midiout = is_out ? cbox_io_get_midi_output(io, NULL, &uuid) : NULL; + if (!midiout && !midiin) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", uuidstr); + return FALSE; + } + jack_port_t *port = midiout ? ((struct cbox_jack_midi_output *)midiout)->port + : ((struct cbox_jack_midi_input *)midiin)->port; + jack_port_disconnect(jii->client, port); + return TRUE; + } + } + + if (!strcmp(cmd->command, "/port_connect") && !strcmp(cmd->arg_types, "ss")) + { + const char *port_from = CBOX_ARG_S(cmd, 0); + const char *port_to = CBOX_ARG_S(cmd, 1); + int res = jack_connect(jii->client, port_from, port_to); + if (res == EEXIST) + res = 0; + if (res) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot connect port '%s' to '%s'", port_from, port_to); + return res == 0; + } + else if (!strcmp(cmd->command, "/port_disconnect") && !strcmp(cmd->arg_types, "ss")) + { + const char *port_from = CBOX_ARG_S(cmd, 0); + const char *port_to = CBOX_ARG_S(cmd, 1); + int res = jack_disconnect(jii->client, port_from, port_to); + if (res) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot disconnect port '%s' from '%s'", port_from, port_to); + return res == 0; + } + else if (!strcmp(cmd->command, "/get_connected_ports") && !strcmp(cmd->arg_types, "s")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + const char *name = CBOX_ARG_S(cmd, 0); + jack_port_t *port = NULL; + if (!strchr(name, ':')) { + // try UUID + struct cbox_uuid uuid; + if (!cbox_uuid_fromstring(&uuid, name, error)) + return FALSE; + struct cbox_midi_output *midiout = cbox_io_get_midi_output(io, NULL, &uuid); + if (midiout) + port = ((struct cbox_jack_midi_output *)midiout)->port; + else { + struct cbox_midi_input *midiin = cbox_io_get_midi_input(io, NULL, &uuid); + if (midiin) + port = ((struct cbox_jack_midi_input *)midiin)->port; + } + } + else + port = jack_port_by_name(jii->client, name); + if (!port) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' not found", name); + return FALSE; + } + const char** ports = jack_port_get_all_connections(jii->client, port); + for (int i = 0; ports && ports[i]; i++) + { + if (!cbox_execute_on(fb, NULL, "/port", "s", error, ports[i])) + return FALSE; + } + jack_free(ports); + return TRUE; + } + else if (!strcmp(cmd->command, "/get_ports") && !strcmp(cmd->arg_types, "ssi")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + const char *mask = CBOX_ARG_S(cmd, 0); + const char *type = CBOX_ARG_S(cmd, 1); + uint32_t flags = CBOX_ARG_I(cmd, 2); + const char** ports = jack_get_ports(jii->client, mask, type, flags); + for (int i = 0; ports && ports[i]; i++) + { + if (!cbox_execute_on(fb, NULL, "/port", "s", error, ports[i])) + return FALSE; + } + jack_free(ports); + return TRUE; + } + else if (!strcmp(cmd->command, "/external_tempo") && !strcmp(cmd->arg_types, "i")) + { + jii->external_tempo = CBOX_ARG_I(cmd, 0); + return TRUE; + } + + //Metadata + + else if (!strcmp(cmd->command, "/set_property") && !strcmp(cmd->arg_types, "ssss")) + //parameters: "client:port", key, value, type according to jack_property_t (empty or NULL for string) + { + const char *name = CBOX_ARG_S(cmd, 0); + const char *key = CBOX_ARG_S(cmd, 1); + const char *value = CBOX_ARG_S(cmd, 2); + const char *type = CBOX_ARG_S(cmd, 3); + + jack_uuid_t subject; + if (!cbox_jack_io_get_jack_uuid_from_name(jii, name, &subject, error)) //error message set inside + return FALSE; + + if (jack_set_property(jii->client, subject, key, value, type)) // 0 on success + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Set property key:'%s' value: '%s' to port '%s' was not successful", key, value, name); + return FALSE; + } + return TRUE; + } + else if (!strcmp(cmd->command, "/get_property") && !strcmp(cmd->arg_types, "ss")) + //parameters: "client:port", key + //returns python key, value and type as strings + { + const char *name = CBOX_ARG_S(cmd, 0); + const char *key = CBOX_ARG_S(cmd, 1); + + jack_uuid_t subject; + if (!cbox_jack_io_get_jack_uuid_from_name(jii, name, &subject, error)) //error message set inside + return FALSE; + + char* value = NULL; + char* type = NULL; + + if (jack_get_property(subject, key, &value, &type)) // 0 on success, -1 if the subject has no key property. + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' does not have key '%s'", name, key); + return FALSE; + } + + char* returntype; //We need to call jack_free on type in any case so it can't be our own data. + if (type == NULL) + returntype = ""; + else + returntype = type; + + if (!cbox_execute_on(fb, NULL, "/value", "ss", error, value, returntype)) //send return values to Python. + return FALSE; + + jack_free(value); + jack_free(type); + return TRUE; + } + else if (!strcmp(cmd->command, "/get_properties") && !strcmp(cmd->arg_types, "s")) + //parameters: "client:port" + { + const char *name = CBOX_ARG_S(cmd, 0); + + jack_uuid_t subject; + if (!cbox_jack_io_get_jack_uuid_from_name(jii, name, &subject, error)) //error message set inside + return FALSE; + + jack_description_t desc; + if (!jack_get_properties(subject, &desc)) // 0 on success, -1 if no subject with any properties exists. + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Port '%s' with uuid '%lli' does not have any properties", name, (long long)subject); + return FALSE; + } + + const char *returntype; + for (uint32_t i = 0; icommand, "/get_all_properties") && !strcmp(cmd->arg_types, "")) + { + jack_description_t *descs; + int counter; + counter = jack_get_all_properties(&descs); + const char *returntype; + for (int j = 0; j < counter; j++) + { + jack_description_t *one_desc = &descs[j]; + for (uint32_t i = 0; i < one_desc->property_cnt ; i++) + { + if (one_desc->properties[i].type == NULL) + returntype = ""; + else + returntype = one_desc->properties[i].type; + + /* + index = jack_uuid_to_index(one_desc->subject) + portid = jack_port_by_id(jii->client, index); + portname = jack_port_name(port); + */ + if (!cbox_execute_on(fb, NULL, "/all_properties", "ssss", + error, + jack_port_name(jack_port_by_id(jii->client, jack_uuid_to_index(one_desc->subject))), + one_desc->properties[i].key, + one_desc->properties[i].data, + returntype)) + return FALSE; + } + jack_free_description(one_desc, 0); //if non-zero one_desc will also be passed to free() + } + jack_free(descs); + return TRUE; + } + + + else if (!strcmp(cmd->command, "/remove_property") && !strcmp(cmd->arg_types, "ss")) + { + const char *name = CBOX_ARG_S(cmd, 0); + const char *key = CBOX_ARG_S(cmd, 1); + + jack_uuid_t subject; + if (!cbox_jack_io_get_jack_uuid_from_name(jii, name, &subject, error)) //error message set inside + return FALSE; + + if (jack_remove_property(jii->client, subject, key)) // 0 on success, -1 otherwise + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Could not remove port '%s' key '%s'", name, key); + return FALSE; + } + return TRUE; + } + + else if (!strcmp(cmd->command, "/remove_properties") && !strcmp(cmd->arg_types, "s")) + { + const char *name = CBOX_ARG_S(cmd, 0); + + jack_uuid_t subject; + if (!cbox_jack_io_get_jack_uuid_from_name(jii, name, &subject, error)) //error message set inside + return FALSE; + + if (jack_remove_properties(jii->client, subject) == -1) // number of removed properties returned, -1 on error + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Could not remove properties of port '%s'", name); + return FALSE; + } + return TRUE; + } + + + else if (!strcmp(cmd->command, "/remove_all_properties") && !strcmp(cmd->arg_types, "")) + { + if (jack_remove_all_properties(jii->client)) // 0 on success, -1 otherwise + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Remove all JACK properties was not successful"); + return FALSE; + } + return TRUE; + } + + else + { + gboolean result = cbox_io_process_cmd(io, fb, cmd, error, &handled); + if (!handled) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return result; + } +} + +/////////////////////////////////////////////////////////////////////////////// + +static void cbox_jackio_control_transport(struct cbox_io_impl *impl, gboolean roll, uint32_t pos) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + + if (jii->debug_transport) + g_message("JACK transport: control(op=%s, pos=%d)\n", roll ? "roll" : "stop", (int)pos); + + jack_transport_state_t state = jack_transport_query(jii->client, NULL); + if (roll && pos != (uint32_t)-1) + jack_transport_locate(jii->client, pos); + if (roll && state == JackTransportStopped) + jack_transport_start(jii->client); + if (!roll && state != JackTransportStopped) + jack_transport_stop(jii->client); + if (!roll && pos != (uint32_t)-1) + jack_transport_locate(jii->client, pos); +} + +static gboolean cbox_jackio_get_sync_completed(struct cbox_io_impl *impl) +{ + struct cbox_jack_io_impl *jii = (struct cbox_jack_io_impl *)impl; + return jack_transport_query(jii->client, NULL) != JackTransportStarting; +} + +/////////////////////////////////////////////////////////////////////////////// + +static void cbox_jackio_update_midi_in_routing(struct cbox_io_impl *impl) +{ + // XXXKF slow and wasteful, but that's okay for now + for (GSList *p = impl->pio->midi_inputs; p; p = g_slist_next(p)) + { + struct cbox_midi_input *midiin = p->data; + for (GSList *q = impl->pio->midi_outputs; q; q = g_slist_next(q)) + { + struct cbox_midi_output *midiout = q->data; + + bool add = midiin->output_set && !midiout->removing && cbox_uuid_equal(&midiout->uuid, &midiin->output); + if (add) + cbox_midi_merger_connect(&midiout->merger, &midiin->buffer, app.rt, NULL); + else + cbox_midi_merger_disconnect(&midiout->merger, &midiin->buffer, app.rt); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// + +gboolean cbox_io_init_jack(struct cbox_io *io, struct cbox_open_params *const params, struct cbox_command_target *fb, GError **error) +{ + const char *client_name = cbox_config_get_string_with_default("io", "client_name", "cbox"); + + jack_client_t *client = NULL; + jack_status_t status = 0; + client = jack_client_open(client_name, JackNoStartServer, &status); + if (client == NULL) + { + if (!cbox_hwcfg_setup_jack()) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot set up JACK server configuration based on current hardware"); + return FALSE; + } + + status = 0; + client = jack_client_open(client_name, 0, &status); + } + if (client == NULL) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create JACK instance"); + return FALSE; + } + + // XXXKF would use a callback instead + io->io_env.buffer_size = jack_get_buffer_size(client); + io->cb = NULL; + io->io_env.input_count = cbox_config_get_int("io", "inputs", 0); + io->input_buffers = malloc(sizeof(float *) * io->io_env.input_count); + io->io_env.output_count = cbox_config_get_int("io", "outputs", 2); + io->output_buffers = malloc(sizeof(float *) * io->io_env.output_count); + + struct cbox_jack_io_impl *jii = malloc(sizeof(struct cbox_jack_io_impl)); + io->impl = &jii->ioi; + jii->enable_common_midi_input = cbox_config_get_int("io", "enable_common_midi_input", 1); + jii->debug_transport = cbox_config_get_int("debug", "jack_transport", 0); + jii->last_transport_state = JackTransportStopped; + jii->external_tempo = FALSE; + + cbox_command_target_init(&io->cmd_target, cbox_jack_io_process_cmd, jii); + jii->ioi.pio = io; + jii->ioi.getsampleratefunc = cbox_jackio_get_sample_rate; + jii->ioi.startfunc = cbox_jackio_start; + jii->ioi.stopfunc = cbox_jackio_stop; + jii->ioi.getstatusfunc = cbox_jackio_get_status; + jii->ioi.pollfunc = cbox_jackio_poll_ports; + jii->ioi.cyclefunc = cbox_jackio_cycle; + jii->ioi.getmidifunc = cbox_jackio_get_midi_data; + jii->ioi.createmidiinfunc = cbox_jackio_create_midi_in; + jii->ioi.destroymidiinfunc = cbox_jackio_destroy_midi_in; + jii->ioi.createmidioutfunc = cbox_jackio_create_midi_out; + jii->ioi.destroymidioutfunc = cbox_jackio_destroy_midi_out; + jii->ioi.updatemidiinroutingfunc = cbox_jackio_update_midi_in_routing; + jii->ioi.createaudiooutfunc = cbox_jackio_create_audio_out; + jii->ioi.destroyaudiooutfunc = cbox_jackio_destroy_audio_out; + jii->ioi.controltransportfunc = cbox_jackio_control_transport; + jii->ioi.getsynccompletedfunc = cbox_jackio_get_sync_completed; + jii->ioi.destroyfunc = cbox_jackio_destroy; + + jii->client_name = g_strdup(jack_get_client_name(client)); + jii->client = client; + jii->rb_autoconnect = jack_ringbuffer_create(sizeof(jack_port_t *) * 128); + jii->error_str = NULL; + io->io_env.srate = jack_get_sample_rate(client); + + jii->inputs = malloc(sizeof(jack_port_t *) * io->io_env.input_count); + jii->outputs = malloc(sizeof(jack_port_t *) * io->io_env.output_count); + for (uint32_t i = 0; i < io->io_env.input_count; i++) + jii->inputs[i] = NULL; + for (uint32_t i = 0; i < io->io_env.output_count; i++) + jii->outputs[i] = NULL; + for (uint32_t i = 0; i < io->io_env.input_count; i++) + { + gchar *name = g_strdup_printf("in_%d", 1 + i); + jii->inputs[i] = jack_port_register(jii->client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + if (!jii->inputs[i]) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create input port %d (%s)", i, name); + g_free(name); + goto cleanup; + } + g_free(name); + } + for (uint32_t i = 0; i < io->io_env.output_count; i++) + { + gchar *name = g_strdup_printf("out_%d", 1 + i); + jii->outputs[i] = jack_port_register(jii->client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + if (!jii->outputs[i]) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create output port %d (%s)", i, name); + g_free(name); + goto cleanup; + } + g_free(name); + } + if (jii->enable_common_midi_input) + { + jii->midi = jack_port_register(jii->client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + if (!jii->midi) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create MIDI port"); + return FALSE; + } + } + else + jii->midi = NULL; + + if (fb) + cbox_execute_on(fb, NULL, "/io/jack_client_name", "s", NULL, jii->client_name); + + cbox_io_poll_ports(io, fb); + + return TRUE; + +cleanup: + if (jii->inputs) + { + for (uint32_t i = 0; i < io->io_env.input_count; i++) + free(jii->inputs[i]); + free(jii->inputs); + } + if (jii->outputs) + { + for (uint32_t i = 0; i < io->io_env.output_count; i++) + free(jii->outputs[i]); + free(jii->outputs); + } + cbox_io_destroy_all_midi_ports(io); + if (jii->client_name) + free(jii->client_name); + jack_client_close(jii->client); + free(jii); + io->impl = NULL; + return FALSE; +}; + +#endif diff --git a/template/calfbox/layer.c b/template/calfbox/layer.c new file mode 100644 index 0000000..304e4d6 --- /dev/null +++ b/template/calfbox/layer.c @@ -0,0 +1,293 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "errors.h" +#include "instr.h" +#include "layer.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "scene.h" +#include + +gboolean cbox_layer_load(struct cbox_layer *layer, const char *name, GError **error) +{ + const char *cv = NULL; + struct cbox_instrument *instr = NULL; + gchar *section = g_strdup_printf("layer:%s", name); + + if (!cbox_config_has_section(section)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Missing section for layer %s", name); + goto error; + } + + cv = cbox_config_get_string(section, "instrument"); + if (cv) + { + instr = cbox_scene_get_instrument_by_name(layer->scene, cv, TRUE, error); + if (!instr) + { + cbox_force_error(error); + g_prefix_error(error, "Cannot get instrument %s for layer %s: ", cv, name); + goto error; + } + } + + layer->enabled = cbox_config_get_int(section, "enabled", TRUE); + layer->low_note = 0; + layer->high_note = 127; + + cv = cbox_config_get_string(section, "low_note"); + if (cv) + layer->low_note = note_from_string(cv); + + cv = cbox_config_get_string(section, "high_note"); + if (cv) + layer->high_note = note_from_string(cv); + + layer->transpose = cbox_config_get_int(section, "transpose", 0); + layer->fixed_note = cbox_config_get_int(section, "fixed_note", -1); + layer->in_channel = cbox_config_get_int(section, "in_channel", 0) - 1; + layer->out_channel = cbox_config_get_int(section, "out_channel", 0) - 1; + layer->disable_aftertouch = !cbox_config_get_int(section, "aftertouch", TRUE); + layer->invert_sustain = cbox_config_get_int(section, "invert_sustain", FALSE); + layer->consume = cbox_config_get_int(section, "consume", FALSE); + layer->ignore_scene_transpose = cbox_config_get_int(section, "ignore_scene_transpose", FALSE); + layer->ignore_program_changes = cbox_config_get_int(section, "ignore_program_changes", FALSE); + layer->external_output_set = FALSE; + g_free(section); + + cbox_layer_set_instrument(layer, instr); + + return 1; + +error: + if (instr) + cbox_instrument_destroy_if_unused(instr); + + g_free(section); + return 0; +} + +void cbox_layer_set_instrument(struct cbox_layer *layer, struct cbox_instrument *instrument) +{ + if (layer->instrument) + { + layer->instrument->refcount--; + cbox_instrument_destroy_if_unused(layer->instrument); + layer->instrument = NULL; + } + layer->instrument = instrument; + if (layer->instrument) + layer->instrument->refcount++; +} + +static gboolean cbox_layer_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +static void cbox_layer_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_layer *layer = CBOX_H2O(objhdr); + if (layer->instrument && !--(layer->instrument->refcount)) + { + if (layer->instrument->scene) + cbox_scene_remove_instrument(layer->instrument->scene, layer->instrument); + + cbox_instrument_destroy_if_unused(layer->instrument); + } + if (layer->external_merger) { + cbox_midi_merger_disconnect(layer->external_merger, &layer->output_buffer, layer->scene->rt); + } + free(layer); +} + +gboolean cbox_layer_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_layer *layer = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (!(cbox_execute_on(fb, NULL, "/enable", "i", error, (int)layer->enabled) && + (layer->instrument + ? cbox_execute_on(fb, NULL, "/instrument_name", "s", error, layer->instrument->module->instance_name) && + cbox_execute_on(fb, NULL, "/instrument_uuid", "o", error, layer->instrument) + : (layer->external_output_set + ? cbox_uuid_report_as(&layer->external_output, "/external_output", fb, error) + : TRUE)) && + cbox_execute_on(fb, NULL, "/consume", "i", error, (int)layer->consume) && + cbox_execute_on(fb, NULL, "/ignore_scene_transpose", "i", error, (int)layer->ignore_scene_transpose) && + cbox_execute_on(fb, NULL, "/ignore_program_changes", "i", error, (int)layer->ignore_program_changes) && + cbox_execute_on(fb, NULL, "/disable_aftertouch", "i", error, (int)layer->disable_aftertouch) && + cbox_execute_on(fb, NULL, "/transpose", "i", error, (int)layer->transpose) && + cbox_execute_on(fb, NULL, "/fixed_note", "i", error, (int)layer->fixed_note) && + cbox_execute_on(fb, NULL, "/low_note", "i", error, (int)layer->low_note) && + cbox_execute_on(fb, NULL, "/high_note", "i", error, (int)layer->high_note) && + cbox_execute_on(fb, NULL, "/in_channel", "i", error, layer->in_channel + 1) && + cbox_execute_on(fb, NULL, "/out_channel", "i", error, layer->out_channel + 1) && + CBOX_OBJECT_DEFAULT_STATUS(layer, fb, error))) + return FALSE; + return TRUE; + } + else if (!strcmp(cmd->command, "/enable") && !strcmp(cmd->arg_types, "i")) + { + layer->enabled = 0 != CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/consume") && !strcmp(cmd->arg_types, "i")) + { + layer->consume = 0 != CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/ignore_scene_transpose") && !strcmp(cmd->arg_types, "i")) + { + layer->ignore_scene_transpose = 0 != CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/ignore_program_changes") && !strcmp(cmd->arg_types, "i")) + { + layer->ignore_program_changes = 0 != CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/disable_aftertouch") && !strcmp(cmd->arg_types, "i")) + { + layer->disable_aftertouch = 0 != CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/transpose") && !strcmp(cmd->arg_types, "i")) + { + layer->transpose = CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/fixed_note") && !strcmp(cmd->arg_types, "i")) + { + layer->fixed_note = CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/low_note") && !strcmp(cmd->arg_types, "i")) + { + layer->low_note = CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/high_note") && !strcmp(cmd->arg_types, "i")) + { + layer->high_note = CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/in_channel") && !strcmp(cmd->arg_types, "i")) + { + layer->in_channel = CBOX_ARG_I(cmd, 0) - 1; + return TRUE; + } + else if (!strcmp(cmd->command, "/out_channel") && !strcmp(cmd->arg_types, "i")) + { + layer->out_channel = CBOX_ARG_I(cmd, 0) - 1; + return TRUE; + } + else if (!strcmp(cmd->command, "/external_output") && !strcmp(cmd->arg_types, "s")) + { + if (*CBOX_ARG_S(cmd, 0)) + { + if (cbox_uuid_fromstring(&layer->external_output, CBOX_ARG_S(cmd, 0), error)) { + layer->external_output_set = TRUE; + } + } + else { + layer->external_output_set = FALSE; + } + cbox_scene_update_connected_outputs(layer->scene); + return TRUE; + } + else // otherwise, treat just like an command on normal (non-aux) output + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +CBOX_CLASS_DEFINITION_ROOT(cbox_layer) + +struct cbox_layer *cbox_layer_new(struct cbox_scene *scene) +{ + struct cbox_document *doc = CBOX_GET_DOCUMENT(scene); + struct cbox_layer *l = malloc(sizeof(struct cbox_layer)); + + CBOX_OBJECT_HEADER_INIT(l, cbox_layer, doc); + cbox_command_target_init(&l->cmd_target, cbox_layer_process_cmd, l); + l->enabled = TRUE; + l->instrument = NULL; + l->low_note = 0; + l->high_note = 127; + + l->transpose = 0; + l->fixed_note = -1; + l->in_channel = -1; + l->out_channel = -1; + l->disable_aftertouch = FALSE; + l->invert_sustain = FALSE; + l->consume = FALSE; + l->ignore_scene_transpose = FALSE; + l->ignore_program_changes = FALSE; + l->scene = scene; + cbox_uuid_clear(&l->external_output); + l->external_output_set = FALSE; + l->external_merger = NULL; + CBOX_OBJECT_REGISTER(l); + return l; +} + +struct cbox_layer *cbox_layer_new_with_instrument(struct cbox_scene *scene, const char *instrument_name, GError **error) +{ + struct cbox_layer *layer = cbox_layer_new(scene); + struct cbox_instrument *instr = NULL; + if (!layer) goto error; + instr = cbox_scene_get_instrument_by_name(scene, instrument_name, TRUE, error); + if (!instr) + { + cbox_force_error(error); + g_prefix_error(error, "Cannot get instrument %s for new layer: ", instrument_name); + CBOX_DELETE(layer); + return NULL; + } + cbox_layer_set_instrument(layer, instr); + return layer; + +error: + CBOX_DELETE(layer); + if (instr) + cbox_instrument_destroy_if_unused(instr); + return NULL; +} + +struct cbox_layer *cbox_layer_new_from_config(struct cbox_scene *scene, const char *layer_name, GError **error) +{ + struct cbox_layer *layer = cbox_layer_new(scene); + if (!layer) + goto error; + + layer->scene = scene; + if (!cbox_layer_load(layer, layer_name, error)) + goto error; + + return layer; + +error: + CBOX_DELETE(layer); + return NULL; +} + + diff --git a/template/calfbox/layer.h b/template/calfbox/layer.h new file mode 100644 index 0000000..3f4c9e8 --- /dev/null +++ b/template/calfbox/layer.h @@ -0,0 +1,63 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_LAYER_H +#define CBOX_LAYER_H + +#include "dom.h" +#include "midi.h" +#include +#include + +struct cbox_module; +struct cbox_rt; + +CBOX_EXTERN_CLASS(cbox_layer) + +struct cbox_layer +{ + CBOX_OBJECT_HEADER() + struct cbox_scene *scene; + struct cbox_instrument *instrument; + struct cbox_command_target cmd_target; + gboolean enabled; + int8_t in_channel; // -1 for Omni + int8_t out_channel; // -1 for Omni + uint8_t low_note; + uint8_t high_note; + int8_t transpose; + int8_t fixed_note; + gboolean disable_aftertouch; + gboolean invert_sustain; + gboolean consume; + gboolean ignore_scene_transpose; + gboolean ignore_program_changes; + gboolean external_output_set; + struct cbox_uuid external_output; + struct cbox_midi_buffer output_buffer; + struct cbox_midi_merger *external_merger; +}; + +extern struct cbox_layer *cbox_layer_new(struct cbox_scene *scene); +extern struct cbox_layer *cbox_layer_new_with_instrument(struct cbox_scene *scene, const char *instrument_name, GError **error); +extern struct cbox_layer *cbox_layer_new_from_config(struct cbox_scene *scene, const char *instrument_name, GError **error); +extern gboolean cbox_layer_load(struct cbox_layer *layer, const char *name, GError **error); +extern void cbox_layer_set_instrument(struct cbox_layer *layer, struct cbox_instrument *instrument); +extern void cbox_layer_destroy(struct cbox_layer *layer); + +#endif diff --git a/template/calfbox/limiter.c b/template/calfbox/limiter.c new file mode 100644 index 0000000..401dfc5 --- /dev/null +++ b/template/calfbox/limiter.c @@ -0,0 +1,154 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-float.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS limiter_params + +struct limiter_params +{ + float threshold; + float attack; + float release; +}; + +struct limiter_module +{ + struct cbox_module module; + + struct limiter_params *params, *old_params; + + double cur_gain; + double atk_coeff, rel_coeff; +}; + +gboolean limiter_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct limiter_module *m = (struct limiter_module *)ct->user_data; + + EFFECT_PARAM("/threshold", "f", threshold, double, , -100, 12) else + EFFECT_PARAM("/attack", "f", attack, double, , 1, 1000) else + EFFECT_PARAM("/release", "f", release, double, , 1, 5000) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return + cbox_execute_on(fb, NULL, "/threshold", "f", error, m->params->threshold) && + cbox_execute_on(fb, NULL, "/attack", "f", error, m->params->attack) && + cbox_execute_on(fb, NULL, "/release", "f", error, m->params->release) && + CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void limiter_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct limiter_module *m = module->user_data; +} + +void limiter_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct limiter_module *m = module->user_data; + struct limiter_params *mp = m->params; + + if (m->params != m->old_params) + { + m->atk_coeff = 1 - exp(-1000.0 / (mp->attack * m->module.srate)); + m->rel_coeff = 1 - exp(-1000.0 / (mp->release * m->module.srate)); + // update calculated values + } + const double minval = pow(2.0, -110.0); + for (int i = 0; i < CBOX_BLOCK_SIZE; ++i) + { + float left = inputs[0][i], right = inputs[1][i]; + + float level = fabs(left); + if (fabs(right) > level) + level = fabs(right); + if (level < minval) + level = minval; + level = log(level); + + float gain = 0.0; + + if (level > mp->threshold * 0.11552) + gain = mp->threshold * 0.11552 - level; + + // instantaneous attack + slow release + if (gain >= m->cur_gain) + m->cur_gain += m->rel_coeff * (gain - m->cur_gain); + else + m->cur_gain += m->atk_coeff * (gain - m->cur_gain); + + gain = exp(m->cur_gain); + //if (gain < 1) + // printf("level = %f gain = %f\n", m->cur_level, gain); + + outputs[0][i] = left * gain; + outputs[1][i] = right * gain; + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION(limiter) + +MODULE_CREATE_FUNCTION(limiter) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct limiter_module *m = malloc(sizeof(struct limiter_module)); + CALL_MODULE_INIT(m, 2, 2, limiter); + m->module.process_event = limiter_process_event; + m->module.process_block = limiter_process_block; + struct limiter_params *p = malloc(sizeof(struct limiter_params)); + p->threshold = -1; + p->attack = 10.f; + p->release = 2000.f; + m->params = p; + m->old_params = NULL; + m->cur_gain = 0.f; + + return &m->module; +} + + +struct cbox_module_keyrange_metadata limiter_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata limiter_controllers[] = { +}; + +DEFINE_MODULE(limiter, 0, 2) + diff --git a/template/calfbox/main.c b/template/calfbox/main.c new file mode 100644 index 0000000..ed9ad48 --- /dev/null +++ b/template/calfbox/main.c @@ -0,0 +1,450 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "config.h" +#include "config-api.h" +#include "dom.h" +#include "engine.h" +#include "instr.h" +#include "io.h" +#include "layer.h" +#include "menu.h" +#include "menuitem.h" +#include "midi.h" +#include "module.h" +#include "pattern.h" +#include "rt.h" +#include "scene.h" +#include "scripting.h" +#include "song.h" +#include "tarfile.h" +#include "ui.h" +#include "wavebank.h" + +#include +#include +#include +#include +#include +#include +#if USE_NCURSES +#include +#endif +#include +#include +#include +#include + +static const char *short_options = "i:c:" +#if USE_PYTHON +"r:" +#endif +"e:s:t:b:d:D:N:o:nmhpP"; + +static struct option long_options[] = { + {"help", 0, 0, 'h'}, + {"no-ui", 0, 0, 'n'}, + {"play", 0, 0, 'p'}, + {"no-io", 1, 0, 'N'}, + {"instrument", 1, 0, 'i'}, + {"scene", 1, 0, 's'}, + {"effect", 1, 0, 'e'}, + {"config", 1, 0, 'c'}, + {"metronome", 0, 0, 'm'}, + {"tempo", 1, 0, 't'}, + {"beats", 1, 0, 'b'}, + {"drum-pattern", 1, 0, 'd'}, + {"drum-track", 1, 0, 'D'}, +#if USE_PYTHON + {"run-script", 1, 0, 'r'}, +#endif + {"output", 1, 0, 'o'}, + {0,0,0,0}, +}; + +void print_help(char *progname) +{ + printf("Usage: %s [options]\n" + "\n" + "Options:\n" + " -h | --help Show this help text\n" + " -m | --metronome Create a simple metronome pattern\n" +#if USE_NCURSES + " -n | --no-ui Do not start the user interface\n" +#endif + " -p | --play Start pattern playback (default for -d/-D)\n" + " -P | --no-play Don't start pattern playback\n" + " -N | --no-io Use off-line processing instead of JACK I/O\n" + " -d | --drum-pattern

Load drum pattern with a given name\n" + " -D | --drum-track Load drum track with a given name\n" + " -t | --tempo Use given tempo (specified in beats/min)\n" + " -b | --beats Use given beats/bar\n" + " -e | --effect Override master effect with preset \n" + " -i | --instrument Load instrument as a single-instrument scene\n" + " -s | --scene Load a scene \n" + " -c | --config Use specified config file instead of default\n" +#if USE_PYTHON + " -r | --run-script Run a Python script from a given file\n" +#endif + " -o | --output Write the first stereo output to a WAV file\n" + "\n", + progname); + exit(0); +} + +#if USE_PYTHON + +// This is a workaround for what I consider a defect in pyconfig.h +#undef _XOPEN_SOURCE +#undef _POSIX_C_SOURCE + +#include + +static gboolean set_error_from_python(GError **error) +{ + PyObject *ptype = NULL, *pvalue = NULL, *ptraceback = NULL; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + PyObject *ptypestr = PyObject_Str(ptype); + PyObject *pvaluestr = PyObject_Str(pvalue); + PyObject *ptypestr_unicode = PyUnicode_AsUTF8String(ptypestr); + PyObject *pvaluestr_unicode = PyUnicode_AsUTF8String(pvaluestr); + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "%s: %s", PyBytes_AsString(ptypestr_unicode), PyBytes_AsString(pvaluestr_unicode)); + Py_DECREF(pvaluestr_unicode); + Py_DECREF(ptypestr_unicode); + //g_error("%s:%s", PyString_AsString(ptypestr), PyString_AsString(pvaluestr)); + Py_DECREF(ptypestr); + Py_DECREF(pvaluestr); + Py_DECREF(ptype); + Py_XDECREF(pvalue); + Py_XDECREF(ptraceback); + return FALSE; +} + +void cbox_script_run(const char *name) +{ + FILE *fp = fopen(name, "rb"); + if (!fp) + { + g_warning("Cannot open script file '%s': %s", name, strerror(errno)); + return; + } + // (_cbox module is discontinued, use _cbox2) + // PyImport_AppendInittab("_cbox", &PyInit_cbox); + Py_Initialize(); +#if 0 + if (PyType_Ready(&CboxCallbackType) < 0) + { + g_warning("Cannot install the C callback type"); + return; + } + Py_INCREF(&CboxCallbackType); + engine_initialised = TRUE; +#endif + if (PyRun_SimpleFile(fp, name) == 1) + { + GError *error = NULL; + set_error_from_python(&error); + cbox_print_error(error); + } + Py_Finalize(); +} + +#endif + +#if USE_NCURSES + +static int (*old_menu_on_idle)(struct cbox_ui_page *page); + +static int on_idle_with_ui_poll(struct cbox_ui_page *page) +{ + cbox_app_on_idle(NULL, NULL); + + if (old_menu_on_idle) + return old_menu_on_idle(page); + else + return 0; +} + +void run_ui() +{ + struct cbox_menu_state *st = NULL; + struct cbox_menu_page *page = cbox_menu_page_new(); + cbox_ui_start(); + old_menu_on_idle = page->page.on_idle; + page->page.on_idle = on_idle_with_ui_poll; + + struct cbox_menu *main_menu = create_main_menu(); + + st = cbox_menu_state_new(page, main_menu, stdscr, NULL); + page->state = st; + + cbox_ui_run(&page->page); + cbox_ui_stop(); + cbox_menu_state_destroy(st); + cbox_menu_page_destroy(page); + cbox_menu_destroy(main_menu); +} + +#endif + +void run_no_ui() +{ + printf("Ready. Press ENTER to exit.\n"); + fcntl(0, F_SETFL, fcntl(0, F_GETFL, 0) | O_NONBLOCK); + do { + int ch = getchar(); + if (ch == 10 || (ch == -1 && errno != EWOULDBLOCK)) + break; + usleep(100000); + cbox_app_on_idle(NULL, NULL); + } while(1); + fcntl(0, F_SETFL, fcntl(0, F_GETFL, 0) &~ O_NONBLOCK); +} + +int main(int argc, char *argv[]) +{ + struct cbox_open_params params; + struct cbox_layer *layer; + const char *config_name = NULL; + const char *instrument_name = NULL; + const char *scene_name = NULL; + const char *effect_preset_name = NULL; + const char *drum_pattern_name = NULL; + const char *drum_track_name = NULL; +#if USE_PYTHON + const char *script_name = NULL; +#endif + const char *output_name = NULL; + char *instr_section = NULL; + struct cbox_scene *scene = NULL; + int metronome = 0; + int bpb = 0; + float tempo = 0; + GError *error = NULL; +#if USE_NCURSES + gboolean no_ui = FALSE; +#endif + gboolean no_io = FALSE; + int play_immediately = 0; + int no_io_srate = 0; + + cbox_dom_init(); + + while(1) + { + int option_index; + int c = getopt_long(argc, argv, short_options, long_options, &option_index); + if (c == -1) + break; + switch (c) + { + case 'c': + config_name = optarg; + break; + case 'i': + instrument_name = optarg; + break; + case 'o': + output_name = optarg; + break; + case 's': + scene_name = optarg; + break; + case 'e': + effect_preset_name = optarg; + break; + case 'd': + drum_pattern_name = optarg; + break; + case 'D': + drum_track_name = optarg; + break; +#if USE_PYTHON + case 'r': + script_name = optarg; + break; +#endif + case 'm': + metronome = 1; + break; + case 'n': +#if USE_NCURSES + no_ui = TRUE; +#endif + break; + case 'N': + no_io = TRUE; + no_io_srate = atoi(optarg); + break; + case 'p': + play_immediately = 1; + break; + case 'P': + play_immediately = -1; + break; + case 'b': + bpb = atoi(optarg); + break; + case 't': + tempo = atof(optarg); + break; + case 'h': + case '?': + print_help(argv[0]); + return 0; + } + } + + app.tarpool = cbox_tarpool_new(); + app.document = cbox_document_new(); + app.rt = cbox_rt_new(app.document); + app.engine = cbox_engine_new(app.document, app.rt); + app.rt->engine = app.engine; + + cbox_config_init(config_name); + if (tempo < 1) + tempo = cbox_config_get_float("master", "tempo", 120); + if (bpb < 1) + bpb = cbox_config_get_int("master", "beats_per_bar", 4); + + if (no_io) + { + cbox_rt_set_offline(app.rt, no_io_srate, 1024); + } + else + { + GError *error = NULL; + if (!cbox_io_init(&app.io, ¶ms, NULL, &error)) + { + fprintf(stderr, "Cannot initialise sound I/O: %s\n", (error && error->message) ? error->message : "Unknown error"); + return 1; + } + cbox_rt_set_io(app.rt, &app.io); + } + cbox_wavebank_init(); + + if (!scene_name && !instrument_name) + { + scene_name = cbox_config_get_string("init", "scene"); + instrument_name = cbox_config_get_string("init", "instrument"); + if (!scene_name && !instrument_name) + { + if (cbox_config_has_section("scene:default")) + scene_name = "default"; + else + if (cbox_config_has_section("instrument:default")) + instrument_name = "default"; + } + } + + scene = cbox_scene_new(app.document, app.engine); + if (!scene) + goto fail; + if (scene_name) + { + app.current_scene_name = g_strdup_printf("scene:%s", scene_name); + if (!cbox_scene_load(scene, scene_name, &error)) + goto fail; + } + else + if (instrument_name) + { + app.current_scene_name = g_strdup_printf("instrument:%s", instrument_name); + layer = cbox_layer_new_with_instrument(scene, instrument_name, &error); + if (!layer) + goto fail; + + if (!cbox_scene_add_layer(scene, layer, &error)) + goto fail; + } + + if (!effect_preset_name) + effect_preset_name = cbox_config_get_string("master", "effect"); + + if (effect_preset_name && *effect_preset_name) + { + app.engine->effect = cbox_module_new_from_fx_preset(effect_preset_name, app.document, app.rt, app.engine, &error); + if (!app.engine->effect) + goto fail; + } + cbox_master_set_tempo(app.engine->master, tempo); + cbox_master_set_timesig(app.engine->master, bpb, 4); + + if (output_name && scene) + { + GError *error = NULL; + if (!cbox_recording_source_attach(&scene->rec_stereo_outputs[0], cbox_recorder_new_stream(app.engine, app.rt, output_name), &error)) + cbox_print_error(error); + } + cbox_rt_start(app.rt, NULL); + if (drum_pattern_name) + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_load(app.engine->master->song, drum_pattern_name, 1, app.engine->master->ppqn_factor)); + else if (drum_track_name) + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_load_track(app.engine->master->song, drum_track_name, 1, app.engine->master->ppqn_factor)); + else if (metronome) + cbox_song_use_looped_pattern(app.engine->master->song, cbox_midi_pattern_new_metronome(app.engine->master->song, app.engine->master->timesig_num, app.engine->master->ppqn_factor)); + + gboolean has_song = drum_pattern_name || drum_track_name || metronome; + if (play_immediately == 1 || (play_immediately != -1 && has_song)) + cbox_master_play(app.engine->master); +#if USE_PYTHON + if (script_name) + cbox_script_run(script_name); + else +#endif +#if USE_NCURSES + if (!no_ui) + run_ui(); + else + run_no_ui(); +#else + run_no_ui(); +#endif + cbox_rt_stop(app.rt); + if (!no_io) + cbox_io_close(&app.io); + goto ok; + +fail: + fprintf(stderr, "Cannot start: %s\n", error ? error->message : "unknown error"); +ok: + if (error) + g_error_free(error); + if (app.engine->effect) + { + CBOX_DELETE(app.engine->effect); + app.engine->effect = NULL; + } + CBOX_DELETE(app.engine); + CBOX_DELETE(app.rt); + cbox_tarpool_destroy(app.tarpool); + + if (cbox_wavebank_get_maxbytes() > 0) + g_message("Max waveform usage: %f MB", (float)(cbox_wavebank_get_maxbytes() / 1048576.0)); + + cbox_document_destroy(app.document); + cbox_wavebank_close(); + cbox_config_close(); + + g_free(instr_section); + + cbox_dom_close(); + + return 0; +} diff --git a/template/calfbox/master.c b/template/calfbox/master.c new file mode 100644 index 0000000..ee379f7 --- /dev/null +++ b/template/calfbox/master.c @@ -0,0 +1,389 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "engine.h" +#include "errors.h" +#include "master.h" +#include "seq.h" +#include "rt.h" +#include "song.h" +#include + +static gboolean master_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_master *m = ct->user_data; + if (!strcmp(cmd->command, "/status") && !*cmd->arg_types) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/sample_rate", "i", error, m->srate)) + return FALSE; + if (!m->spb) + return TRUE; + return cbox_execute_on(fb, NULL, "/tempo", "f", error, m->tempo) && + cbox_execute_on(fb, NULL, "/timesig", "ii", error, m->timesig_num, m->timesig_denom) && + cbox_execute_on(fb, NULL, "/playing", "i", error, (int)m->state) && + cbox_execute_on(fb, NULL, "/pos", "i", error, m->spb->song_pos_samples) && + cbox_execute_on(fb, NULL, "/pos_ppqn", "i", error, m->spb->song_pos_ppqn) && + cbox_execute_on(fb, NULL, "/ppqn_factor", "i", error, (int)m->ppqn_factor); + } + else + if (!strcmp(cmd->command, "/tell") && !*cmd->arg_types) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!m->spb) + return TRUE; + return cbox_execute_on(fb, NULL, "/playing", "i", error, (int)m->state) && + cbox_execute_on(fb, NULL, "/pos", "i", error, m->spb->song_pos_samples) && + cbox_execute_on(fb, NULL, "/pos_ppqn", "i", error, m->spb->song_pos_ppqn); + } + else + if (!strcmp(cmd->command, "/set_tempo") && !strcmp(cmd->arg_types, "f")) + { + cbox_master_set_tempo(m, CBOX_ARG_F(cmd, 0)); + return TRUE; + } + else + if (!strcmp(cmd->command, "/set_timesig") && !strcmp(cmd->arg_types, "ii")) + { + cbox_master_set_timesig(m, CBOX_ARG_I(cmd, 0), CBOX_ARG_I(cmd, 1)); + return TRUE; + } + else + if (!strcmp(cmd->command, "/set_ppqn_factor") && !strcmp(cmd->arg_types, "i")) + { + m->ppqn_factor = CBOX_ARG_I(cmd, 0); + return TRUE; + } + else + if (!strcmp(cmd->command, "/play") && !strcmp(cmd->arg_types, "")) + { + cbox_master_play(m); + return TRUE; + } + else + if (!strcmp(cmd->command, "/stop") && !strcmp(cmd->arg_types, "")) + { + cbox_master_stop(m); + return TRUE; + } + else + if (!strcmp(cmd->command, "/panic") && !strcmp(cmd->arg_types, "")) + { + cbox_master_panic(m); + return TRUE; + } + else + if (!strcmp(cmd->command, "/seek_samples") && !strcmp(cmd->arg_types, "i")) + { + cbox_master_seek_samples(m, CBOX_ARG_I(cmd, 0)); + return TRUE; + } + else + if (!strcmp(cmd->command, "/seek_ppqn") && !strcmp(cmd->arg_types, "i")) + { + cbox_master_seek_ppqn(m, CBOX_ARG_I(cmd, 0)); + return TRUE; + } + else + if ((!strcmp(cmd->command, "/samples_to_ppqn") || !strcmp(cmd->command, "/ppqn_to_samples")) && + !strcmp(cmd->arg_types, "i")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (m->spb) + { + if (cmd->command[1] == 's') + return cbox_execute_on(fb, NULL, "/value", "i", error, cbox_master_samples_to_ppqn(m, CBOX_ARG_I(cmd, 0))); + else + return cbox_execute_on(fb, NULL, "/value", "i", error, cbox_master_ppqn_to_samples(m, CBOX_ARG_I(cmd, 0))); + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Song playback not initialised."); + return FALSE; + } + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return FALSE; + } +} + +static void cbox_master_init(struct cbox_master *master, struct cbox_engine *engine) +{ + master->srate = engine->io_env.srate; + master->tempo = 120.0; + master->new_tempo = 120.0; + master->timesig_num = 4; + master->timesig_denom = 4; + master->state = CMTS_STOP; + master->engine = engine; + master->song = NULL; + master->spb = NULL; + master->ppqn_factor = 48; + cbox_command_target_init(&master->cmd_target, master_process_cmd, master); +} + +struct cbox_master *cbox_master_new(struct cbox_engine *engine) +{ + struct cbox_master *master = malloc(sizeof(struct cbox_master)); + cbox_master_init(master, engine); + return master; +} + + +void cbox_master_set_sample_rate(struct cbox_master *master, int srate) +{ + master->srate = srate; +} + +void cbox_master_set_tempo(struct cbox_master *master, float tempo) +{ + // XXXKF not realtime-safe; won't crash, but may lose tempo + // changes when used multiple times in rapid succession + master->new_tempo = tempo; +} + +void cbox_master_set_timesig(struct cbox_master *master, int beats, int unit) +{ + if (beats > 0) + master->timesig_num = beats; + if (unit > 0) + master->timesig_denom = unit; +} + +#define cbox_master_play_args(ARG) + +DEFINE_RT_VOID_FUNC(cbox_master, master, cbox_master_play) +{ + struct cbox_rt *rt = master->engine->rt; + if (rt && rt->io && rt->io->impl->controltransportfunc) + { + rt->io->impl->controltransportfunc(rt->io->impl, FALSE, master->spb ? master->spb->song_pos_samples : (uint32_t)-1); + if (!rt->io->impl->getsynccompletedfunc(rt->io->impl)) + RT_CALL_AGAIN_LATER(); + rt->io->impl->controltransportfunc(rt->io->impl, TRUE, master->spb ? master->spb->song_pos_samples : (uint32_t)-1); + return; + } + // wait for the notes to be released + if (master->state == CMTS_STOPPING) + { + RT_CALL_AGAIN_LATER(); + return; + } + + master->state = CMTS_ROLLING; +} + +#define cbox_master_stop_args(ARG) + +DEFINE_RT_VOID_FUNC(cbox_master, master, cbox_master_stop) +{ + struct cbox_rt *rt = master->engine->rt; + if (rt && rt->io && rt->io->impl->controltransportfunc) + { + rt->io->impl->controltransportfunc(rt->io->impl, FALSE, -1); + return; + } + if (master->state == CMTS_ROLLING) + master->state = CMTS_STOPPING; + + if (master->state != CMTS_STOP) + RT_CALL_AGAIN_LATER(); +} + +struct seek_command_arg +{ + struct cbox_master *master; + gboolean is_ppqn; + uint32_t target_pos; + gboolean was_rolling; + gboolean status_known; + gboolean seek_in_progress; +}; + +static int seek_transport_execute(void *arg_) +{ + struct seek_command_arg *arg = arg_; + + struct cbox_rt *rt = arg->master->engine->rt; + if (rt && rt->io && rt->io->impl->controltransportfunc) + { + if (!arg->seek_in_progress) + { + arg->seek_in_progress = TRUE; + uint32_t pos = arg->target_pos; + if (arg->is_ppqn) + arg->target_pos = pos = cbox_master_ppqn_to_samples(arg->master, pos); + + rt->io->impl->controltransportfunc(rt->io->impl, arg->master->state == CMTS_ROLLING, pos); + // JACK slow-sync won't be performed if unless transport is rolling + if (!arg->was_rolling) + { + if (arg->master->spb) + cbox_song_playback_seek_samples(arg->master->spb, arg->target_pos); + return 5; + } + } + if (rt->io->impl->getsynccompletedfunc(rt->io->impl)) + { + if (arg->master->spb) + cbox_song_playback_seek_samples(arg->master->spb, arg->target_pos); + return 5; + } + return 0; + } + + // On first pass, check if transport is rolling from the DSP thread + if (!arg->status_known) + { + arg->status_known = TRUE; + arg->was_rolling = arg->master->state == CMTS_ROLLING; + } + // If transport was rolling, stop, release notes, seek, then restart + if (arg->master->state == CMTS_ROLLING) + arg->master->state = CMTS_STOPPING; + + // wait until transport stopped + if (arg->master->state != CMTS_STOP) + return 0; + + if (arg->master->spb) + { + if (arg->is_ppqn) + cbox_song_playback_seek_ppqn(arg->master->spb, arg->target_pos, FALSE); + else + cbox_song_playback_seek_samples(arg->master->spb, arg->target_pos); + } + if (arg->was_rolling) + arg->master->state = CMTS_ROLLING; + return 1; +} + +void cbox_master_seek_ppqn(struct cbox_master *master, uint32_t pos_ppqn) +{ + static struct cbox_rt_cmd_definition cmd = { NULL, seek_transport_execute, NULL }; + struct seek_command_arg arg = { master, TRUE, pos_ppqn, FALSE, FALSE, FALSE }; + cbox_rt_execute_cmd_sync(master->engine->rt, &cmd, &arg); +} + +void cbox_master_seek_samples(struct cbox_master *master, uint32_t pos_samples) +{ + static struct cbox_rt_cmd_definition cmd = { NULL, seek_transport_execute, NULL }; + struct seek_command_arg arg = { master, FALSE, pos_samples, FALSE, FALSE, FALSE }; + cbox_rt_execute_cmd_sync(master->engine->rt, &cmd, &arg); +} + +void cbox_master_panic(struct cbox_master *master) +{ + cbox_master_stop(master); + struct cbox_midi_buffer buf; + cbox_midi_buffer_init(&buf); + for (int ch = 0; ch < 16; ch++) + { + cbox_midi_buffer_write_inline(&buf, ch, 0xB0 + ch, 120, 0); + cbox_midi_buffer_write_inline(&buf, ch, 0xB0 + ch, 123, 0); + cbox_midi_buffer_write_inline(&buf, ch, 0xB0 + ch, 121, 0); + } + // Send to all outputs + cbox_engine_send_events_to(master->engine, NULL, &buf); +} + +uint32_t cbox_master_ppqn_to_samples(struct cbox_master *master, uint32_t time_ppqn) +{ + double tempo = master->tempo; + int offset = 0; + if (master->spb) + { + int idx = cbox_song_playback_tmi_from_ppqn(master->spb, time_ppqn); + if (idx != -1) + { + const struct cbox_tempo_map_item *tmi = &master->spb->tempo_map_items[idx]; + tempo = tmi->tempo; + time_ppqn -= tmi->time_ppqn; + offset = tmi->time_samples; + } + } + return offset + (int)(master->srate * 60.0 * time_ppqn / (tempo * master->ppqn_factor)); +} + +uint32_t cbox_master_samples_to_ppqn(struct cbox_master *master, uint32_t time_samples) +{ + double tempo = master->tempo; + uint32_t offset = 0; + if (master->spb) + { + int idx = cbox_song_playback_tmi_from_samples(master->spb, time_samples); + if (idx != -1 && idx < master->spb->tempo_map_item_count) + { + const struct cbox_tempo_map_item *tmi = &master->spb->tempo_map_items[idx]; + tempo = tmi->tempo; + time_samples -= tmi->time_samples; + offset = tmi->time_ppqn; + } + } + return offset + (uint32_t)(tempo * master->ppqn_factor * time_samples / (master->srate * 60.0)); +} + +void cbox_master_ppqn_to_bbt(const struct cbox_master *master, struct cbox_bbt *bbt, int time_ppqn, struct cbox_master_track_item *mti) +{ + bbt->bar = 0; + bbt->beat = 0; + bbt->tick = 0; + bbt->offset_samples = 0; + + uint32_t rel_ppqn = time_ppqn; + int idx = -1; + if (master->spb) + idx = cbox_song_playback_tmi_from_ppqn(master->spb, time_ppqn); + if (idx != -1 && idx < master->spb->tempo_map_item_count) + { + struct cbox_tempo_map_item *tmi = &master->spb->tempo_map_items[idx]; + rel_ppqn = time_ppqn - tmi->time_ppqn; + cbox_bbt_add(bbt, rel_ppqn, master->ppqn_factor, tmi->timesig_num, tmi->timesig_denom); + if (mti) + { + mti->tempo = tmi->tempo; + mti->timesig_num = tmi->timesig_num; + mti->timesig_denom = tmi->timesig_denom; + } + } + else + { + cbox_bbt_add(bbt, rel_ppqn, master->ppqn_factor, master->timesig_num, master->timesig_denom); + if (mti) + { + mti->tempo = master->tempo; + mti->timesig_num = master->timesig_num; + mti->timesig_denom = master->timesig_denom; + } + } +} + +void cbox_master_destroy(struct cbox_master *master) +{ + if (master->spb) + { + cbox_song_playback_destroy(master->spb); + master->spb = NULL; + } + free(master); +} diff --git a/template/calfbox/master.h b/template/calfbox/master.h new file mode 100644 index 0000000..69438fe --- /dev/null +++ b/template/calfbox/master.h @@ -0,0 +1,100 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_MASTER_H +#define CBOX_MASTER_H + +#include +#include "cmd.h" + +extern uint64_t PPQN; + +struct cbox_song; +struct cbox_rt; + +#define GET_RT_FROM_cbox_master(ptr) ((ptr)->engine->rt) + +enum cbox_master_transport_state +{ + CMTS_STOP, + CMTS_ROLLING, + CMTS_STOPPING, +}; + +struct cbox_master +{ + int srate; + float tempo, new_tempo; + int timesig_num; + int timesig_denom; // must be 4 for now + uint64_t ppqn_factor; + enum cbox_master_transport_state state; + struct cbox_engine *engine; + struct cbox_song *song; + struct cbox_song_playback *spb; + struct cbox_command_target cmd_target; +}; + +struct cbox_bbt +{ + uint32_t bar; + uint32_t beat; + uint32_t tick; + uint32_t offset_samples; +}; + +struct cbox_master_track_item; + +static inline void cbox_bbt_add(struct cbox_bbt *accum, uint32_t ticks, uint32_t ppqn_factor, uint32_t timesig_num, uint32_t timesig_denom) +{ + uint32_t ticks_per_beat = ppqn_factor * 4 / timesig_denom; + uint32_t beats_per_bar = timesig_num; + + accum->tick += ticks % ticks_per_beat; + if (accum->tick >= ticks_per_beat) + { + accum->tick -= ticks_per_beat; + accum->beat++; + } + uint32_t inc_beats = ticks / ticks_per_beat; + accum->beat += inc_beats % beats_per_bar; + if (accum->beat >= beats_per_bar) + { + accum->beat -= beats_per_bar; + accum->bar++; + } + accum->bar += inc_beats / beats_per_bar; +} + +extern struct cbox_master *cbox_master_new(struct cbox_engine *engine); +extern void cbox_master_set_sample_rate(struct cbox_master *master, int srate); +extern void cbox_master_set_tempo(struct cbox_master *master, float tempo); +extern void cbox_master_set_timesig(struct cbox_master *master, int beats, int unit); +extern void cbox_master_ppqn_to_bbt(const struct cbox_master *master, struct cbox_bbt *bbt, int time_ppqn, struct cbox_master_track_item *mti); +//extern uint32_t cbox_master_song_pos_from_bbt(struct cbox_master *master, const struct cbox_bbt *bbt); +extern void cbox_master_play(struct cbox_master *master); +extern void cbox_master_stop(struct cbox_master *master); +extern void cbox_master_panic(struct cbox_master *master); +extern void cbox_master_seek_ppqn(struct cbox_master *master, uint32_t pos_ppqn); +extern void cbox_master_seek_samples(struct cbox_master *master, uint32_t pos_samples); +extern void cbox_master_destroy(struct cbox_master *master); + +uint32_t cbox_master_ppqn_to_samples(struct cbox_master *master, uint32_t time_ppqn); +uint32_t cbox_master_samples_to_ppqn(struct cbox_master *master, uint32_t time_samples); + +#endif diff --git a/template/calfbox/menu.c b/template/calfbox/menu.c new file mode 100644 index 0000000..e80ca0c --- /dev/null +++ b/template/calfbox/menu.c @@ -0,0 +1,288 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#include "menu.h" +#include "menuitem.h" +#include "ui.h" + +#if USE_NCURSES + +#include +#include +#include +#include +#include + +struct cbox_menu +{ + GPtrArray *items; + GStringChunk *strings; +}; + +static int cbox_menu_page_on_key(struct cbox_ui_page *p, int ch); +static int cbox_menu_page_on_idle(struct cbox_ui_page *p); + +struct cbox_menu *cbox_menu_new() +{ + struct cbox_menu *menu = malloc(sizeof(struct cbox_menu)); + + menu->items = g_ptr_array_new(); + menu->strings = g_string_chunk_new(100); + return menu; +} + +struct cbox_menu_item *cbox_menu_add_item(struct cbox_menu *menu, struct cbox_menu_item *item) +{ + g_ptr_array_add(menu->items, item); + return item; +} + +void cbox_menu_destroy(struct cbox_menu *menu) +{ + guint i; + + for (i = 0; i < menu->items->len; i++) + cbox_menu_item_destroy(g_ptr_array_index(menu->items, i)); + + g_ptr_array_free(menu->items, TRUE); + g_string_chunk_free(menu->strings); + free(menu); +} + + +/* +gchar *cbox_menu_item_value_format(const struct cbox_menu_item *item, void *context) +{ + switch(item->type) + { + case menu_item_static: + if (item->extras) + return ((struct cbox_menu_item_extras_static *)(item->extras))->format_value(item, context); + else + return g_strdup_printf(""); + case menu_item_submenu: + return g_strdup_printf("..."); + case menu_item_command: + return g_strdup_printf(""); + default: + if (!item->value) + return g_strdup_printf("(null)"); + switch(item->type) + { + case menu_item_value_int: + return g_strdup_printf(((struct cbox_menu_item_extras_int *)(item->extras))->fmt, *(int *)item->value); + case menu_item_value_double: + return g_strdup_printf(((struct cbox_menu_item_extras_double *)(item->extras))->fmt, *(double *)item->value); + case menu_item_value_enum: + return g_strdup_printf("%d", *(int *)item->value); + default: + return g_strdup_printf(""); + } + } + assert(0); + return NULL; +} +*/ + +void cbox_menu_state_size(struct cbox_menu_state *menu_state) +{ + struct cbox_menu *menu = menu_state->menu; + guint i; + menu_state->size.label_width = 0; + menu_state->size.value_width = 0; + menu_state->size.height = 0; + menu_state->yspace = getmaxy(menu_state->window) - 2; + + for (i = 0; i < menu->items->len; i++) + { + struct cbox_menu_item *item = g_ptr_array_index(menu->items, i); + + item->x = 1; + item->y = 1 + menu_state->size.height; + item->item_class->measure(item, menu_state); + } +} + +void cbox_menu_state_draw(struct cbox_menu_state *menu_state) +{ + struct cbox_menu *menu = menu_state->menu; + guint i; + + werase(menu_state->window); + box(menu_state->window, 0, 0); + for (i = 0; i < menu->items->len; i++) + { + struct cbox_menu_item *item = g_ptr_array_index(menu->items, i); + gchar *str = item->item_class->format_value(item, menu_state); + item->item_class->draw(item, menu_state, str, menu_state->cursor == i); + g_free(str); + } + wrefresh(menu_state->window); +} + +static void cbox_menu_page_draw(struct cbox_ui_page *p) +{ + struct cbox_menu_page *mp = p->user_data; + struct cbox_menu_state *st = mp->state; + cbox_menu_state_size(st); + cbox_menu_state_draw(st); +} + +static int cbox_menu_is_item_enabled(struct cbox_menu *menu, unsigned int item) +{ + assert(item < menu->items->len); + + return ((struct cbox_menu_item *)g_ptr_array_index(menu->items, item))->item_class->on_key != NULL; +} + +struct cbox_menu_state *cbox_menu_state_new(struct cbox_menu_page *page, struct cbox_menu *menu, WINDOW *window, void *context) +{ + struct cbox_menu_state *st = malloc(sizeof(struct cbox_menu_state)); + st->page = page; + st->menu = menu; + st->cursor = 0; + st->yoffset = 0; + st->window = window; + st->context = context; + st->caller = NULL; + st->menu_is_temporary = 0; + + while(st->cursor < menu->items->len - 1 && !cbox_menu_is_item_enabled(menu, st->cursor)) + st->cursor++; + + return st; +} + +int cbox_menu_page_on_idle(struct cbox_ui_page *p) +{ + struct cbox_menu_page *mp = p->user_data; + struct cbox_menu_state *st = mp->state; + cbox_menu_state_size(st); + cbox_menu_state_draw(st); + return 0; +} + +int cbox_menu_page_on_key(struct cbox_ui_page *p, int ch) +{ + struct cbox_menu_page *mp = p->user_data; + struct cbox_menu_state *st = mp->state; + struct cbox_menu *menu = st->menu; + struct cbox_menu_item *item = NULL; + int pos = st->cursor; + int res = 0; + if (st->cursor >= 0 && st->cursor < menu->items->len) + item = g_ptr_array_index(menu->items, st->cursor); + + if (ch == 27) + return ch; + + if (item->item_class->on_key) + { + res = item->item_class->on_key(item, st, ch); + st = mp->state; + if (res < 0) + { + cbox_menu_state_size(st); + cbox_menu_state_draw(st); + return 0; + } + } + + if (res > 0) + return res; + + switch(ch) + { + case 12: + wclear(st->window); + return 0; + case 27: + return ch; + case KEY_UP: + case KEY_END: + pos = ch == KEY_END ? menu->items->len - 1 : st->cursor - 1; + while(pos >= 0 && !cbox_menu_is_item_enabled(menu, pos)) + pos--; + if (pos >= 0) + st->cursor = pos; + if (ch == KEY_END) + { + st->yoffset = st->size.height - st->yspace; + if (st->yoffset < 0) + st->yoffset = 0; + } + else + if (pos >= 0 && (guint)pos < menu->items->len) + { + int npos = st->cursor; + int count = 0; + // show up to 2 disabled items above + while(npos >= 1 && !cbox_menu_is_item_enabled(menu, npos - 1) && count < 2) + { + npos--; + count++; + } + item = g_ptr_array_index(menu->items, npos); + if (item->y < 1 + st->yoffset) + st->yoffset = item->y - 1; + } + cbox_menu_state_draw(st); + return 0; + case KEY_HOME: + case KEY_DOWN: + pos = ch == KEY_HOME ? 0 : st->cursor + 1; + while(pos < (int)menu->items->len && !cbox_menu_is_item_enabled(menu, pos)) + pos++; + if (pos < (int)menu->items->len) + st->cursor = pos; + if (ch == KEY_HOME) + st->yoffset = 0; + else if (pos >= 0 && pos < (int)menu->items->len) + { + item = g_ptr_array_index(menu->items, st->cursor); + if (item->y - 1 - st->yoffset >= st->yspace) + st->yoffset = item->y - st->yspace; + } + cbox_menu_state_draw(st); + return 0; + } + return 0; +} + +void cbox_menu_state_destroy(struct cbox_menu_state *st) +{ + free(st); +} + +struct cbox_menu_page *cbox_menu_page_new() +{ + struct cbox_menu_page *page = malloc(sizeof(struct cbox_menu_page)); + page->state = NULL; + page->page.user_data = page; + page->page.draw = cbox_menu_page_draw; + page->page.on_key = cbox_menu_page_on_key; + page->page.on_idle = cbox_menu_page_on_idle; + return page; +} + +void cbox_menu_page_destroy(struct cbox_menu_page *p) +{ + free(p); +} + +#endif diff --git a/template/calfbox/menu.h b/template/calfbox/menu.h new file mode 100644 index 0000000..bfe35e6 --- /dev/null +++ b/template/calfbox/menu.h @@ -0,0 +1,67 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_MENU_H +#define CBOX_MENU_H + +#include "config.h" + +#if USE_NCURSES + +#include +#include + +#include "menuitem.h" +#include "ui.h" + +struct cbox_menu; +struct cbox_menu_item; +struct cbox_menu_page; + +struct cbox_menu_state +{ + struct cbox_menu_page *page; + struct cbox_menu *menu; + guint cursor; + int yoffset, yspace; + struct cbox_menu_measure size; + WINDOW *window; + void *context; + struct cbox_menu_state *caller; + int menu_is_temporary; +}; + +extern struct cbox_menu *cbox_menu_new(void); +extern struct cbox_menu_item *cbox_menu_add_item(struct cbox_menu *menu, struct cbox_menu_item *item); +extern void cbox_menu_destroy(struct cbox_menu *menu); + +extern struct cbox_menu_state *cbox_menu_state_new(struct cbox_menu_page *page, struct cbox_menu *menu, WINDOW *window, void *context); +extern void cbox_menu_state_destroy(struct cbox_menu_state *st); + +struct cbox_menu_page +{ + struct cbox_ui_page page; + struct cbox_menu_state *state; +}; + +extern struct cbox_menu_page *cbox_menu_page_new(void); +extern void cbox_menu_page_destroy(struct cbox_menu_page *st); + +#endif + +#endif diff --git a/template/calfbox/menuitem.c b/template/calfbox/menuitem.c new file mode 100644 index 0000000..97274c0 --- /dev/null +++ b/template/calfbox/menuitem.c @@ -0,0 +1,303 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "menu.h" +#if USE_NCURSES +#include "menuitem.h" + +#include +#include + +/*******************************************************************/ + +static void item_measure(struct cbox_menu_item *item, struct cbox_menu_state *state) +{ + struct cbox_menu_measure *m = &state->size; + gchar *value = item->item_class->format_value(item, state); + + int len = strlen(item->label); + int len2 = value ? strlen(value) : 0; + if (len > m->label_width) + m->label_width = len; + if (len2 > m->value_width) + m->value_width = len2; + m->height++; + + g_free(value); +} + +static void item_destroy(struct cbox_menu_item *item) +{ + if (item->flags & mif_free_label) + g_free(item->label); + if (item->flags & mif_free_context) + free(item->item_context); + if (item->flags & mif_context_is_struct) + { + struct cbox_menu_item_context *ctx = item->item_context; + ctx->destroy_func(ctx); + } + g_free(item); +} + +static void item_draw(struct cbox_menu_item *item, struct cbox_menu_state *state, gchar *value, int hilited) +{ + int y = item->y - state->yoffset; + if (y < 1 || y > state->yspace) + return; + if (hilited) + wattron(state->window, A_REVERSE); + mvwprintw(state->window, item->y - state->yoffset, item->x, "%-*s %*s", state->size.label_width, item->label, state->size.value_width, value); + wattroff(state->window, A_REVERSE); +} + +/*******************************************************************/ + +static gchar *command_format(const struct cbox_menu_item *item, struct cbox_menu_state *state) +{ + return g_strdup("*"); +} + +static int command_on_key(struct cbox_menu_item *item, struct cbox_menu_state *state, int key) +{ + if (key == 10) + { + struct cbox_menu_item_command *citem = (struct cbox_menu_item_command *)item; + return citem->execute(citem, state->context); + } + return 0; +} + +struct cbox_menu_item_class menu_item_class_command = { + .measure = item_measure, + .draw = item_draw, + .format_value = command_format, + .on_idle = NULL, + .on_key = command_on_key, + .destroy = item_destroy +}; + +/*******************************************************************/ + +void static_draw(struct cbox_menu_item *item, struct cbox_menu_state *state, gchar *value, int hilited) +{ + int y = item->y - state->yoffset; + if (y < 1 || y > state->yspace) + return; + if (!value) + { + wattron(state->window, A_BOLD); + mvwprintw(state->window, y, item->x, "%-*s", state->size.label_width + state->size.value_width, item->label); + wattroff(state->window, A_BOLD); + } + else + mvwprintw(state->window, y, item->x, "%-*s %*s", state->size.label_width, item->label, state->size.value_width, value); +} + +static gchar *static_format(const struct cbox_menu_item *item, struct cbox_menu_state *state) +{ + struct cbox_menu_item_static *sitem = (struct cbox_menu_item_static *)item; + if (!sitem->format_value) + return NULL; + return sitem->format_value(sitem, state->context); +} + +struct cbox_menu_item_class menu_item_class_static = { + .measure = item_measure, + .draw = static_draw, + .format_value = static_format, + .on_idle = NULL, + .on_key = NULL, + .destroy = item_destroy +}; + +/*******************************************************************/ + +static gchar *intvalue_format(const struct cbox_menu_item *item, struct cbox_menu_state *state) +{ + struct cbox_menu_item_int *iitem = (struct cbox_menu_item_int *)item; + + return g_strdup_printf("%d", *iitem->value); +} + +static int intvalue_on_key(struct cbox_menu_item *item, struct cbox_menu_state *state, int key) +{ + struct cbox_menu_item_int *iitem = (struct cbox_menu_item_int *)item; + int *pv; + + switch(key) + { + case KEY_LEFT: + pv = iitem->value; + if (*pv > iitem->vmin) + { + (*pv)--; + if (iitem->on_change) + iitem->on_change(iitem, state->context); + return -1; + } + return 0; + case KEY_RIGHT: + pv = iitem->value; + if (*pv < iitem->vmax) + { + (*pv)++; + if (iitem->on_change) + iitem->on_change(iitem, state->context); + return -1; + } + return 0; + } + return 0; +} + +struct cbox_menu_item_class menu_item_class_int = { + .measure = item_measure, + .draw = item_draw, + .format_value = intvalue_format, + .on_idle = NULL, + .on_key = intvalue_on_key, + .destroy = item_destroy +}; + + +/*******************************************************************/ + +static gchar *menu_format(const struct cbox_menu_item *item, struct cbox_menu_state *state) +{ + struct cbox_menu_item_menu *mitem = (struct cbox_menu_item_menu *)item; + + return g_strdup((mitem->menu || mitem->create_menu) ? "->" : "<-"); +} + +static int menu_on_key(struct cbox_menu_item *item, struct cbox_menu_state *state, int key) +{ + struct cbox_menu_page *page = state->page; + if (key == 10) + { + struct cbox_menu_item_menu *mitem = (struct cbox_menu_item_menu *)item; + if (mitem->create_menu) + { + struct cbox_menu_state *new_state = cbox_menu_state_new(page, mitem->create_menu(mitem, state->context), state->window, state->context); + new_state->caller = state; + new_state->menu_is_temporary = 1; + + page->state = new_state; + } + else + if (mitem->menu) + { + struct cbox_menu_state *new_state = cbox_menu_state_new(page, mitem->menu, state->window, state->context); + new_state->caller = state; + + page->state = new_state; + } + else + { + struct cbox_menu_state *caller_state = state->caller; + if (state->menu_is_temporary) + cbox_menu_destroy(state->menu); + cbox_menu_state_destroy(state); + + page->state = caller_state; + return -1; + } + return 0; + } + return 0; +} + +struct cbox_menu_item_class menu_item_class_menu = { + .measure = item_measure, + .draw = item_draw, + .format_value = menu_format, + .on_idle = NULL, + .on_key = menu_on_key, + .destroy = item_destroy +}; + +/*******************************************************************/ + +#define TREAT_LABEL(label) ((flags & mif_dup_label) == mif_dup_label ? g_strdup(label) : (char *)(label)) + +struct cbox_menu_item *cbox_menu_item_new_command(const char *label, cbox_menu_item_execute_func exec, void *item_context, uint32_t flags) +{ + struct cbox_menu_item_command *item = calloc(1, sizeof(struct cbox_menu_item_command)); + item->item.label = TREAT_LABEL(label); + item->item.flags = flags; + item->item.item_class = &menu_item_class_command; + item->item.item_context = item_context; + item->execute = exec; + return &item->item; +} + +struct cbox_menu_item *cbox_menu_item_new_static(const char *label, cbox_menu_item_format_value fmt, void *item_context, uint32_t flags) +{ + struct cbox_menu_item_static *item = calloc(1, sizeof(struct cbox_menu_item_static)); + item->item.label = TREAT_LABEL(label); + item->item.flags = flags; + item->item.item_class = &menu_item_class_static; + item->item.item_context = item_context; + item->format_value = fmt; + return &item->item; +} + +struct cbox_menu_item *cbox_menu_item_new_int(const char *label, int *value, int vmin, int vmax, void *item_context, uint32_t flags) +{ + struct cbox_menu_item_int *item = calloc(1, sizeof(struct cbox_menu_item_int)); + item->item.label = TREAT_LABEL(label); + item->item.flags = flags; + item->item.item_class = &menu_item_class_int; + item->item.item_context = item_context; + item->value = value; + item->vmin = vmin; + item->vmax = vmax; + item->on_change = NULL; + return &item->item; +} + +struct cbox_menu_item *cbox_menu_item_new_menu(const char *label, struct cbox_menu *menu, void *item_context, uint32_t flags) +{ + struct cbox_menu_item_menu *item = calloc(1, sizeof(struct cbox_menu_item_menu)); + item->item.label = TREAT_LABEL(label); + item->item.flags = flags; + item->item.item_class = &menu_item_class_menu; + item->item.item_context = item_context; + item->menu = menu; + item->create_menu = NULL; + return &item->item; +} + +struct cbox_menu_item *cbox_menu_item_new_dynamic_menu(const char *label, create_menu_func func, void *item_context, uint32_t flags) +{ + struct cbox_menu_item_menu *item = calloc(1, sizeof(struct cbox_menu_item_menu)); + item->item.label = TREAT_LABEL(label); + item->item.flags = flags; + item->item.item_class = &menu_item_class_menu; + item->item.item_context = item_context; + item->menu = NULL; + item->create_menu = func; + return &item->item; +} + +void cbox_menu_item_destroy(struct cbox_menu_item *item) +{ + item->item_class->destroy(item); +} + +#endif diff --git a/template/calfbox/menuitem.h b/template/calfbox/menuitem.h new file mode 100644 index 0000000..e209745 --- /dev/null +++ b/template/calfbox/menuitem.h @@ -0,0 +1,133 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_MENUITEM_H +#define CBOX_MENUITEM_H + +#if USE_NCURSES + +#include +#include +#include + +struct cbox_menu; +struct cbox_menu_item; +struct cbox_menu_item_command; +struct cbox_menu_item_static; +struct cbox_menu_item_menu; +struct cbox_menu_state; + +struct cbox_menu_measure +{ + int label_width; + int value_width; + int height; +}; + +struct cbox_menu_item_class +{ + void (*measure)(struct cbox_menu_item *item, struct cbox_menu_state *state); + void (*draw)(struct cbox_menu_item *item, struct cbox_menu_state *state, gchar *value, int hilited); + gchar *(*format_value)(const struct cbox_menu_item *item, struct cbox_menu_state *state); + int (*on_key)(struct cbox_menu_item *item, struct cbox_menu_state *state, int key); + int (*on_idle)(struct cbox_menu_item *item, struct cbox_menu_state *state); + void (*destroy)(struct cbox_menu_item *item); + +}; + +typedef int (*cbox_menu_item_execute_func)(struct cbox_menu_item_command *item, void *context); +typedef char *(*cbox_menu_item_format_value)(const struct cbox_menu_item_static *item, void *context); + +struct cbox_menu_item +{ + gchar *label; + struct cbox_menu_item_class *item_class; + void *item_context; + uint32_t flags; + int x, y; + /* TODO: is_active? */ +}; + +struct cbox_menu_item_command +{ + struct cbox_menu_item item; + int (*execute)(struct cbox_menu_item_command *item, void *menu_context); +}; + +struct cbox_menu_item_int +{ + struct cbox_menu_item item; + int *value; + int vmin, vmax; + const char *fmt; + int (*on_change)(struct cbox_menu_item_int *item, void *menu_context); +}; + +struct cbox_menu_item_double +{ + struct cbox_menu_item item; + double *value; + double vmin, vmax; + const char *fmt; + double step_arg; + double (*step)(struct cbox_menu_item_double *item, int where); + int (*on_change)(struct cbox_menu_item_double *item, void *menu_context); +}; + +struct cbox_menu_item_static +{ + struct cbox_menu_item item; + char *(*format_value)(const struct cbox_menu_item_static *item, void *menu_context); +}; + +typedef struct cbox_menu *(*create_menu_func)(struct cbox_menu_item_menu *item, void *menu_context); + +struct cbox_menu_item_context +{ + void (*destroy_func)(void *menu_context); +}; + +struct cbox_menu_item_menu +{ + struct cbox_menu_item item; + struct cbox_menu *menu; + create_menu_func create_menu; +}; + +enum { + mif_free_label = 1, // release the label on destroy + mif_free_context = 2, // release the context on destroy + mif_dup_label = 4 | mif_free_label, // clone the label, release the clone on destroy + mif_context_is_struct = 8, // cast context to cbox_menu_item_context and call destroy_func on destroy (it may or may not free() itself) +}; + +extern struct cbox_menu_item *cbox_menu_item_new_command(const char *label, cbox_menu_item_execute_func exec, void *item_context, uint32_t flags); +extern struct cbox_menu_item *cbox_menu_item_new_static(const char *label, cbox_menu_item_format_value fmt, void *item_context, uint32_t flags); +extern struct cbox_menu_item *cbox_menu_item_new_int(const char *label, int *value, int vmin, int vmax, void *item_context, uint32_t flags); +extern struct cbox_menu_item *cbox_menu_item_new_menu(const char *label, struct cbox_menu *menu, void *item_context, uint32_t flags); +extern struct cbox_menu_item *cbox_menu_item_new_dynamic_menu(const char *label, create_menu_func func, void *item_context, uint32_t flags); +extern void cbox_menu_item_destroy(struct cbox_menu_item *); + +static inline struct cbox_menu_item *cbox_menu_item_new_ok(void) +{ + return cbox_menu_item_new_menu("OK", NULL, NULL, 0); +} + +#endif + +#endif diff --git a/template/calfbox/meter.c b/template/calfbox/meter.c new file mode 100644 index 0000000..688aa42 --- /dev/null +++ b/template/calfbox/meter.c @@ -0,0 +1,137 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "dspmath.h" +#include "errors.h" +#include "meter.h" +#include +#include +#include + +static void clear_meter(struct cbox_meter *m) +{ + for (int i = 0; i < 2; i++) + { + m->volume[i] = 0.f; + m->peak[i] = 0.f; + m->last_peak[i] = 0.f; + } + m->smpcounter = 0; +} + +gboolean cbox_meter_attach(struct cbox_recorder *handler, struct cbox_recording_source *src, GError **error) +{ + struct cbox_meter *m = handler->user_data; + m->channels = src->channels; + clear_meter(m); + return TRUE; +} + +void cbox_meter_record_block(struct cbox_recorder *handler, const float **buffers, uint32_t offset, uint32_t numsamples) +{ + struct cbox_meter *m = handler->user_data; + for (int c = 0; c < m->channels; c++) + { + float peak = m->peak[c]; + float volume = m->volume[c]; + for (uint32_t i = 0; i < numsamples; i++) + { + float s = buffers[c][i]; + if (fabs(s) > peak) + peak = fabs(s); + volume += (s * s - volume) * 0.01; // XXXKF this is too simplistic, needs sample rate and proper time constant + } + m->peak[c] = peak; + m->volume[c] = sanef(volume); + } + m->smpcounter += numsamples; + if (m->smpcounter > m->srate) + { + for (int c = 0; c < m->channels; c++) + { + m->last_peak[c] = m->peak[c]; + m->peak[c] = 0; + } + m->smpcounter = 0; + } +} + +gboolean cbox_meter_detach(struct cbox_recorder *handler, GError **error) +{ + struct cbox_meter *m = handler->user_data; + m->channels = 0; + clear_meter(m); + return TRUE; +} + +void cbox_meter_destroy(struct cbox_recorder *handler) +{ + struct cbox_meter *m = handler->user_data; + free(m); +} + +static gboolean cbox_meter_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_meter *m = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + return CBOX_OBJECT_DEFAULT_STATUS(&m->recorder, fb, error); + } + if (!strcmp(cmd->command, "/get_peak") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + float peak[2]; + for (int c = 0; c < 2; c++) + { + float v = m->peak[c], w = m->last_peak[c]; + if (v < w) + v = w; + peak[c] = v; + } + + return cbox_execute_on(fb, NULL, "/peak", "ff", error, peak[0], peak[1]); + } + else + if (!strcmp(cmd->command, "/get_rms") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + return cbox_execute_on(fb, NULL, "/rms", "ff", error, sqrt(m->volume[0]), sqrt(m->volume[1])); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +struct cbox_meter *cbox_meter_new(struct cbox_document *document, int srate) +{ + struct cbox_meter *m = malloc(sizeof(struct cbox_meter)); + CBOX_OBJECT_HEADER_INIT(&m->recorder, cbox_recorder, document); + m->recorder.user_data = m; + cbox_command_target_init(&m->recorder.cmd_target, cbox_meter_process_cmd, m); + m->recorder.attach = cbox_meter_attach; + m->recorder.detach = cbox_meter_detach; + m->recorder.record_block = cbox_meter_record_block; + m->recorder.destroy = cbox_meter_destroy; + m->srate = srate; + clear_meter(m); + CBOX_OBJECT_REGISTER(&m->recorder); + return m; +} \ No newline at end of file diff --git a/template/calfbox/meter.h b/template/calfbox/meter.h new file mode 100644 index 0000000..c2bdf3d --- /dev/null +++ b/template/calfbox/meter.h @@ -0,0 +1,38 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_METER_H +#define CBOX_METER_H + +#include "recsrc.h" + +struct cbox_meter +{ + struct cbox_recorder recorder; + + float volume[2]; // lowpassed squared + float peak[2]; + float last_peak[2]; + int srate; + int channels; + int smpcounter; +}; + +extern struct cbox_meter *cbox_meter_new(struct cbox_document *document, int srate); + +#endif diff --git a/template/calfbox/midi.c b/template/calfbox/midi.c new file mode 100644 index 0000000..6e5371e --- /dev/null +++ b/template/calfbox/midi.c @@ -0,0 +1,113 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "midi.h" +#include + +int cbox_midi_buffer_write_inline(struct cbox_midi_buffer *buffer, uint32_t time, ...) +{ + uint8_t buf[4]; + va_list va; + va_start(va, time); + buf[0] = va_arg(va, int); + int size = midi_cmd_size(buf[0]); + for (int i = 1; i < size; i++) + buf[i] = va_arg(va, int); + return cbox_midi_buffer_write_event(buffer, time, buf, size); +} + +int cbox_midi_buffer_write_event(struct cbox_midi_buffer *buffer, uint32_t time, uint8_t *data, uint32_t size) +{ + struct cbox_midi_event *evt; + + if (buffer->count >= CBOX_MIDI_MAX_EVENTS) + return 0; + if (size > 4 && size > CBOX_MIDI_MAX_LONG_DATA - buffer->long_data_size) + return 0; + evt = &buffer->events[buffer->count++]; + evt->time = time; + evt->size = size; + if (size <= 4) + { + memcpy(evt->data_inline, data, size); + } + else + { + evt->data_ext = buffer->long_data + buffer->long_data_size; + memcpy(evt->data_ext, data, size); + buffer->long_data_size += size; + } + return 1; +} + +int cbox_midi_buffer_copy_event(struct cbox_midi_buffer *buffer, const struct cbox_midi_event *event, int new_time) +{ + struct cbox_midi_event *evt; + + if (buffer->count >= CBOX_MIDI_MAX_EVENTS) + return 0; + if (event->size > 4 && event->size > CBOX_MIDI_MAX_LONG_DATA - buffer->long_data_size) + return 0; + evt = &buffer->events[buffer->count++]; + evt->time = new_time; + evt->size = event->size; + if (event->size <= 4) + { + memcpy(evt->data_inline, event->data_inline, event->size); + } + else + { + evt->data_ext = buffer->long_data + buffer->long_data_size; + memcpy(evt->data_ext, event->data_ext, event->size); + buffer->long_data_size += event->size; + } + return 1; +} + +int note_from_string(const char *note) +{ + static const int semis[] = {9, 11, 0, 2, 4, 5, 7}; + int pos; + int nn = tolower(note[0]); + int nv; + if (nn >= '0' && nn <= '9') + return atoi(note); + if (nn < 'a' && nn > 'g') + return -1; + nv = semis[nn - 'a']; + + for (pos = 1; note[pos] == 'b' || note[pos] == '#'; pos++) + nv += (note[pos] == 'b') ? -1 : +1; + + if ((note[pos] == '-' && note[pos + 1] >= '1' && note[pos + 1] <= '2' && note[pos + 2] == '\0') || (note[pos] >= '0' && note[pos] <= '9' && note[pos + 1] == '\0')) + { + return nv + 12 * (2 + atoi(note + pos)); + } + + return -1; +} + +int cbox_config_get_note(const char *cfg_section, const char *key, int def_value) +{ + const char *cv = cbox_config_get_string(cfg_section, key); + if (cv) + return note_from_string(cv); + return def_value; +} + diff --git a/template/calfbox/midi.h b/template/calfbox/midi.h new file mode 100644 index 0000000..ec4ef8c --- /dev/null +++ b/template/calfbox/midi.h @@ -0,0 +1,125 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_MIDI_H +#define CBOX_MIDI_H + +#include +#include +#include +#include + +struct cbox_midi_event +{ + uint32_t time; + uint32_t size; + union { + uint8_t data_inline[4]; /* up to 4 bytes */ + uint8_t *data_ext; /* if larger than 4 bytes */ + }; +}; + +#define CBOX_MIDI_MAX_EVENTS 256 +#define CBOX_MIDI_MAX_LONG_DATA 256 + +struct cbox_midi_buffer +{ + uint32_t count; + uint32_t long_data_size; + struct cbox_midi_event events[CBOX_MIDI_MAX_EVENTS]; + uint8_t long_data[CBOX_MIDI_MAX_LONG_DATA]; +}; + +static inline void cbox_midi_buffer_init(struct cbox_midi_buffer *buffer) +{ + buffer->count = 0; + buffer->long_data_size = 0; +} + +static inline void cbox_midi_buffer_clear(struct cbox_midi_buffer *buffer) +{ + buffer->count = 0; + buffer->long_data_size = 0; +} + +static inline void cbox_midi_buffer_copy(struct cbox_midi_buffer *dst, const struct cbox_midi_buffer *src) +{ + dst->count = src->count; + dst->long_data_size = src->long_data_size; + memcpy(dst->events, src->events, src->count * sizeof(struct cbox_midi_event)); + memcpy(dst->long_data, src->long_data, src->long_data_size); + // for any long events, update data pointers + for (uint32_t i = 0; i < src->count; i++) + { + if (dst->events[i].size > 4) + dst->events[i].data_ext += &dst->long_data[0] - &src->long_data[0]; + } +} + +static inline uint32_t cbox_midi_buffer_get_count(struct cbox_midi_buffer *buffer) +{ + return buffer->count; +} + +static inline uint32_t cbox_midi_buffer_get_last_event_time(struct cbox_midi_buffer *buffer) +{ + if (!buffer->count) + return 0; + return buffer->events[buffer->count - 1].time; +} + +static inline int cbox_midi_buffer_can_store_msg(struct cbox_midi_buffer *buffer, int size) +{ + if (buffer->count >= CBOX_MIDI_MAX_EVENTS) + return 0; + if (size < 4) + return 1; + return buffer->long_data_size + size <= CBOX_MIDI_MAX_LONG_DATA; +} + +static inline const struct cbox_midi_event *cbox_midi_buffer_get_event(const struct cbox_midi_buffer *buffer, uint32_t pos) +{ + if (pos >= buffer->count) + return NULL; + return &buffer->events[pos]; +} + +static inline const uint8_t *cbox_midi_event_get_data(const struct cbox_midi_event *evt) +{ + return evt->size > 4 ? evt->data_ext : evt->data_inline; +} + +static inline int midi_cmd_size(uint8_t cmd) +{ + static const int sizes[] = { 3, 3, 3, 3, 2, 2, 3, 1 }; + if (cmd < 128) + return 0; + return sizes[(cmd >> 4) - 8]; +} + +extern int cbox_midi_buffer_write_event(struct cbox_midi_buffer *buffer, uint32_t time, uint8_t *data, uint32_t size); + +extern int cbox_midi_buffer_write_inline(struct cbox_midi_buffer *buffer, uint32_t time, ...); + +extern int cbox_midi_buffer_copy_event(struct cbox_midi_buffer *buffer, const struct cbox_midi_event *event, int new_time); + +extern int note_from_string(const char *note); + +extern int cbox_config_get_note(const char *cfg_section, const char *key, int def_value); + +#endif diff --git a/template/calfbox/mididest.c b/template/calfbox/mididest.c new file mode 100644 index 0000000..f9b6a6b --- /dev/null +++ b/template/calfbox/mididest.c @@ -0,0 +1,254 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "blob.h" +#include "mididest.h" +#include "rt.h" +#include "stm.h" + +void cbox_midi_merger_init(struct cbox_midi_merger *dest, struct cbox_midi_buffer *output) +{ + dest->inputs = NULL; + dest->output = output; + if (dest->output) + cbox_midi_buffer_clear(dest->output); +} + +// void cbox_midi_buffer_merge(struct cbox_midi_buffer *output, struct cbox_midi_buffer **inputs, int count, int *positions) +void cbox_midi_merger_render_to(struct cbox_midi_merger *dest, struct cbox_midi_buffer *output) +{ + if (!output) + return; + cbox_midi_buffer_clear(output); + for (struct cbox_midi_source *p = dest->inputs; p; p = p->next) + { + if (p->streaming) + p->bpos = 0; + } + + struct cbox_midi_source *first = dest->inputs; + struct cbox_midi_source *first_not = NULL; + while(first) + { + struct cbox_midi_source *earliest_source = NULL; + uint32_t earliest_time = (uint32_t)-1; + + for (struct cbox_midi_source *p = first; p != first_not; p = p->next) + { + struct cbox_midi_buffer *data = p->data; + if (p->bpos < data->count) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(data, p->bpos); + if (event->time < earliest_time) + { + earliest_source = p; + earliest_time = event->time; + } + } + else + { + // Narrow down the range from top and bottom + if (p == first) + first = p->next; + if (p->next == first_not) + { + first_not = p; + break; + } + } + } + if (earliest_source) + { + cbox_midi_buffer_copy_event(output, cbox_midi_buffer_get_event(earliest_source->data, earliest_source->bpos), earliest_time); + earliest_source->bpos++; + } + else + break; + } +} + +struct cbox_midi_source **cbox_midi_merger_find_source(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer) +{ + for (struct cbox_midi_source **pp = &dest->inputs; *pp; pp = &((*pp)->next)) + if ((*pp)->data == buffer) + return pp; + return NULL; +} + +void cbox_midi_merger_connect(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer, struct cbox_rt *rt, struct cbox_midi_merger **dest_ptr) +{ + if (cbox_midi_merger_find_source(dest, buffer) != NULL) + return; + + struct cbox_midi_source *src = calloc(1, sizeof(struct cbox_midi_source)); + src->data = buffer; + src->bpos = 0; + src->streaming = TRUE; + src->next = NULL; // will be updated by the swap + src->merger_ptr = dest_ptr; + if (src->merger_ptr) + *src->merger_ptr = dest; + cbox_rt_swap_pointers_into(rt, (void **)&dest->inputs, src, (void **)&src->next); +} + +void cbox_midi_merger_disconnect(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer, struct cbox_rt *rt) +{ + // Make sure there are no old commands that could modify the chain + // between find_source and swap_pointers. + cbox_rt_handle_cmd_queue(rt); + + struct cbox_midi_source **pp = cbox_midi_merger_find_source(dest, buffer); + if (!pp) + return; + + struct cbox_midi_source *ms = *pp; + void *old_ptr = cbox_rt_swap_pointers(rt, (void **)pp, ms->next); + assert(old_ptr == ms); + if (ms->merger_ptr) + *ms->merger_ptr = NULL; + free(ms); +} + +void cbox_midi_merger_push(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer, struct cbox_rt *rt) +{ + if (!buffer->count) + return; + assert(!cbox_midi_merger_find_source(dest, buffer)); + struct cbox_midi_source src; + src.data = buffer; + src.bpos = 0; + src.streaming = FALSE; + src.next = dest->inputs; + src.merger_ptr = NULL; + cbox_rt_swap_pointers_into(rt, (void **)&dest->inputs, &src, (void **)&src.next); + while(src.bpos < buffer->count) + cbox_rt_handle_cmd_queue(rt); + + struct cbox_midi_source **pp = cbox_midi_merger_find_source(dest, buffer); + if (!pp) + return; + assert(*pp == &src); + void *old_ptr = cbox_rt_swap_pointers(rt, (void **)pp, src.next); + assert(old_ptr == &src); +} + +void cbox_midi_merger_close(struct cbox_midi_merger *dest, struct cbox_rt *rt) +{ + struct cbox_midi_source *ms = cbox_rt_swap_pointers(rt, (void **)&dest->inputs, NULL); + while(ms) + { + struct cbox_midi_source *p = ms; + ms = p->next; + if (p->merger_ptr) + *p->merger_ptr = NULL; + free(p); + } +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_midi_appsink_init(struct cbox_midi_appsink *appsink, struct cbox_rt *rt, struct cbox_time_mapper *tmap) +{ + appsink->rt = rt; + appsink->tmap = tmap; + cbox_midi_buffer_init(&appsink->midibufs[0]); + cbox_midi_buffer_init(&appsink->midibufs[1]); + appsink->current_buffer = 0; +} + +void cbox_midi_appsink_supply(struct cbox_midi_appsink *appsink, struct cbox_midi_buffer *buffer, uint32_t time_offset) +{ + struct cbox_midi_buffer *sinkbuf = &appsink->midibufs[appsink->current_buffer]; + for (uint32_t i = 0; i < buffer->count; i++) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(buffer, i); + if (event) + { + if (!cbox_midi_buffer_can_store_msg(sinkbuf, event->size)) + break; + uint32_t abs_time_samples = time_offset + event->time; + uint32_t etime = abs_time_samples; + if (appsink->tmap) + etime = appsink->tmap->map_time(appsink->tmap, etime); + cbox_midi_buffer_copy_event(sinkbuf, event, etime); + } + } +} + +#define cbox_midi_appsink_get_input_midi_data__args(ARG) + +DEFINE_RT_FUNC(const struct cbox_midi_buffer *, cbox_midi_appsink, appsink, cbox_midi_appsink_get_input_midi_data_) +{ + const struct cbox_midi_buffer *ret = NULL; + if (appsink->midibufs[appsink->current_buffer].count) + { + // return the current buffer, switch to the new, empty one + ret = &appsink->midibufs[appsink->current_buffer]; + appsink->current_buffer = 1 - appsink->current_buffer; + cbox_midi_buffer_clear(&appsink->midibufs[appsink->current_buffer]); + } + + return ret; +} + +const struct cbox_midi_buffer *cbox_midi_appsink_get_input_midi_data(struct cbox_midi_appsink *appsink) +{ + // This checks the counter from the 'wrong' thread, but that's OK, it's + // just to avoid doing any RT work when input buffer is completely empty. + // Any further access/manipulation is done via RT cmd. + if (!appsink->midibufs[appsink->current_buffer].count) + return NULL; + return cbox_midi_appsink_get_input_midi_data_(appsink); +} + +gboolean cbox_midi_appsink_send_to(struct cbox_midi_appsink *appsink, struct cbox_command_target *fb, GError **error) +{ + const struct cbox_midi_buffer *midi_in = cbox_midi_appsink_get_input_midi_data(appsink); + // If no feedback, the input events are lost - probably better than if + // they filled up the input buffer needlessly. + if (fb && midi_in) + { + for (uint32_t i = 0; i < midi_in->count; i++) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(midi_in, i); + const uint8_t *data = cbox_midi_event_get_data(event); + uint32_t time = event->time & 0x7FFFFFFF; + uint32_t time_type = event->time >> 31; + if (time_type == 0 && !cbox_execute_on(fb, NULL, "/io/midi/event_time_samples", "i", error, time)) + return FALSE; + if (time_type == 1 && !cbox_execute_on(fb, NULL, "/io/midi/event_time_ppqn", "i", error, time)) + return FALSE; + // XXXKF doesn't handle SysEx properly yet, only 3-byte values + if (event->size <= 3) + { + if (!cbox_execute_on(fb, NULL, "/io/midi/simple_event", "iii" + (3 - event->size), error, data[0], data[1], data[2])) + return FALSE; + } + else + { + struct cbox_blob blob; + blob.data = (uint8_t *)data; + blob.size = event->size; + if (!cbox_execute_on(fb, NULL, "/io/midi/long_event", "b", error, &blob)) + return FALSE; + } + } + } + return TRUE; +} + diff --git a/template/calfbox/mididest.h b/template/calfbox/mididest.h new file mode 100644 index 0000000..079a43e --- /dev/null +++ b/template/calfbox/mididest.h @@ -0,0 +1,76 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_MIDIDEST_H +#define CBOX_MIDIDEST_H + +#include "midi.h" +#include + +struct cbox_command_target; +struct cbox_rt; + +struct cbox_midi_source +{ + struct cbox_midi_source *next; + struct cbox_midi_buffer *data; + uint32_t bpos; + gboolean streaming; + struct cbox_midi_merger **merger_ptr; +}; + +struct cbox_midi_merger +{ + struct cbox_midi_source *inputs; + struct cbox_midi_buffer *output; +}; + +void cbox_midi_merger_init(struct cbox_midi_merger *dest, struct cbox_midi_buffer *output); +void cbox_midi_merger_render_to(struct cbox_midi_merger *dest, struct cbox_midi_buffer *output); +static inline void cbox_midi_merger_render(struct cbox_midi_merger *dest) +{ + if (dest->output) + cbox_midi_merger_render_to(dest, dest->output); +} +struct cbox_midi_source **cbox_midi_merger_find_source(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer); +void cbox_midi_merger_connect(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer, struct cbox_rt *rt, struct cbox_midi_merger **dest_ptr); +void cbox_midi_merger_disconnect(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer, struct cbox_rt *rt); +void cbox_midi_merger_push(struct cbox_midi_merger *dest, struct cbox_midi_buffer *buffer, struct cbox_rt *rt); +void cbox_midi_merger_close(struct cbox_midi_merger *dest, struct cbox_rt *rt); + +struct cbox_time_mapper +{ + uint32_t (*map_time)(struct cbox_time_mapper *, uint32_t free_running_counter); +}; + +#define GET_RT_FROM_cbox_midi_appsink(appsink) ((appsink)->rt) + +struct cbox_midi_appsink +{ + struct cbox_rt *rt; + struct cbox_time_mapper *tmap; + struct cbox_midi_buffer midibufs[2]; + int current_buffer; +}; + +extern void cbox_midi_appsink_init(struct cbox_midi_appsink *appsink, struct cbox_rt *rt, struct cbox_time_mapper *tmap); +extern void cbox_midi_appsink_supply(struct cbox_midi_appsink *appsink, struct cbox_midi_buffer *buffer, uint32_t time_offset); +extern const struct cbox_midi_buffer *cbox_midi_appsink_get_input_midi_data(struct cbox_midi_appsink *appsink); +extern gboolean cbox_midi_appsink_send_to(struct cbox_midi_appsink *appsink, struct cbox_command_target *fb, GError **error); + +#endif diff --git a/template/calfbox/module.c b/template/calfbox/module.c new file mode 100644 index 0000000..5c5986c --- /dev/null +++ b/template/calfbox/module.c @@ -0,0 +1,270 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "cmd.h" +#include "config-api.h" +#include "engine.h" +#include "module.h" +#include "rt.h" + +#include +#include +#include +#include +#include +#include + +extern struct cbox_module_manifest sampler_module; +extern struct cbox_module_manifest fluidsynth_module; +extern struct cbox_module_manifest tonewheel_organ_module; +extern struct cbox_module_manifest stream_player_module; +extern struct cbox_module_manifest tone_control_module; +extern struct cbox_module_manifest delay_module; +extern struct cbox_module_manifest reverb_module; +extern struct cbox_module_manifest parametric_eq_module; +extern struct cbox_module_manifest phaser_module; +extern struct cbox_module_manifest chorus_module; +extern struct cbox_module_manifest fxchain_module; +extern struct cbox_module_manifest jack_input_module; +extern struct cbox_module_manifest feedback_reducer_module; +extern struct cbox_module_manifest compressor_module; +extern struct cbox_module_manifest gate_module; +extern struct cbox_module_manifest distortion_module; +extern struct cbox_module_manifest fuzz_module; +extern struct cbox_module_manifest limiter_module; + +struct cbox_module_manifest *cbox_module_list[] = { + &tonewheel_organ_module, +#if USE_FLUIDSYNTH + &fluidsynth_module, +#endif + &stream_player_module, + &tone_control_module, + &delay_module, + &reverb_module, + ¶metric_eq_module, + &phaser_module, + &chorus_module, + &sampler_module, + &fxchain_module, +#if USE_JACK + &jack_input_module, +#endif + &feedback_reducer_module, + &compressor_module, + &gate_module, + &distortion_module, + &fuzz_module, + &limiter_module, + NULL +}; + +CBOX_CLASS_DEFINITION_ROOT(cbox_module) + +void cbox_module_manifest_dump(struct cbox_module_manifest *manifest) +{ + static const char *ctl_classes[] = { "Switch CC#", "Continuous CC#", "Cont. Param", "Discrete Param", "Enum" }; + int i = 0; + printf("Module: %s\n", manifest->name); + printf("Audio I/O: min %d inputs, min %d outputs\n", manifest->min_inputs, manifest->min_outputs); + + printf("Live controllers:\n"); + printf("Ch# Type Number Name \n"); + printf("---- --------------- ------ ------------------------------\n"); + for (i = 0; i < manifest->num_live_controllers; i++) + { + struct cbox_module_livecontroller_metadata *lc = &manifest->live_controllers[i]; + if (lc->channel == 255) + printf("ALL "); + else + if (!lc->channel) + printf("ANY "); + else + printf("%-4d ", lc->channel); + printf("%15s %-6d %-30s\n", ctl_classes[lc->controller_class], lc->controller, lc->name); + } +} + +struct cbox_module_manifest *cbox_module_manifest_get_by_name(const char *name) +{ + struct cbox_module_manifest **mptr; + + for (mptr = cbox_module_list; *mptr; mptr++) + { + if (!strcmp((*mptr)->name, name)) + return *mptr; + } + return NULL; +} + +struct cbox_module *cbox_module_manifest_create_module(struct cbox_module_manifest *manifest, const char *cfg_section, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, const char *instance_name, GError **error) +{ + g_clear_error(error); + struct cbox_module *module = manifest->create(manifest->user_data, cfg_section, doc, rt, engine, error); + if (!module) + return NULL; + + module->instance_name = g_strdup(instance_name); + module->input_samples = malloc(sizeof(float) * CBOX_BLOCK_SIZE * module->inputs); + module->output_samples = malloc(sizeof(float) * CBOX_BLOCK_SIZE * module->outputs); + module->engine_name = manifest->name; + cbox_midi_buffer_init(&module->midi_input); + + return module; +} + +void cbox_module_init(struct cbox_module *module, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, void *user_data, int inputs, int outputs, cbox_process_cmd cmd_handler, void (*destroy)(struct cbox_module *module)) +{ + CBOX_OBJECT_HEADER_INIT(module, cbox_module, doc); + module->user_data = user_data; + module->rt = rt; + module->engine = engine; + module->instance_name = NULL; + module->input_samples = NULL; + module->output_samples = NULL; + module->inputs = inputs; + module->outputs = outputs; + module->aux_offset = outputs; + module->bypass = 0; + module->srate = engine->io_env.srate; + module->srate_inv = 1.0 / module->srate; + + cbox_command_target_init(&module->cmd_target, cmd_handler, module); + module->process_event = NULL; + module->process_block = NULL; + module->destroy = destroy; + CBOX_OBJECT_REGISTER(module); +} + +struct cbox_module *cbox_module_new_from_fx_preset(const char *name, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, GError **error) +{ + gchar *section = g_strdup_printf("fxpreset:%s", name); + const char *engine_name; + struct cbox_module_manifest *mptr; + struct cbox_module *effect; + + if (!cbox_config_has_section(section)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No FX preset called '%s'", name); + goto fxpreset_error; + } + engine_name = cbox_config_get_string(section, "engine"); + if (!engine_name) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "FX engine not specified for preset '%s'", name); + goto fxpreset_error; + } + mptr = cbox_module_manifest_get_by_name(engine_name); + if (!mptr) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "FX preset '%s' refers to non-existing engine '%s'", name, engine_name); + goto fxpreset_error; + } + effect = cbox_module_manifest_create_module(mptr, section, doc, rt, engine, name, error); + if (!effect) + { + cbox_force_error(error); + g_prefix_error(error, "Could not instantiate FX preset '%s': ", name); + goto fxpreset_error; + } + g_free(section); + return effect; + +fxpreset_error: + g_free(section); + return NULL; +} + +gboolean cbox_module_slot_process_cmd(struct cbox_module **psm, + struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, + struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, GError **error) +{ + struct cbox_module *sm = *psm; + if (!strcmp(subcmd, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (!(cbox_execute_on(fb, NULL, "/insert_engine", "s", error, sm ? sm->engine_name : "") && + cbox_execute_on(fb, NULL, "/insert_preset", "s", error, sm ? sm->instance_name : "") && + cbox_execute_on(fb, NULL, "/bypass", "i", error, sm ? sm->bypass : 0))) + return FALSE; + return TRUE; + } + if (!strcmp(subcmd, "/insert_preset") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_module *effect = cbox_module_new_from_fx_preset(CBOX_ARG_S(cmd, 0), doc, rt, engine, error); + if (!effect) + return FALSE; + cbox_rt_swap_pointers(rt, (void **)psm, effect); + return TRUE; + } + if (!strcmp(subcmd, "/insert_engine") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_module *effect = NULL; + if (*CBOX_ARG_S(cmd, 0)) + { + struct cbox_module_manifest *manifest = cbox_module_manifest_get_by_name(CBOX_ARG_S(cmd, 0)); + if (!manifest) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No effect engine '%s'", CBOX_ARG_S(cmd, 0)); + return FALSE; + } + effect = cbox_module_manifest_create_module(manifest, NULL, doc, rt, engine, "unnamed", error); + if (!effect) + return FALSE; + } + cbox_rt_swap_pointers(rt, (void **)psm, effect); + return TRUE; + } + if (!sm) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No engine on module in path '%s'", cmd->command); + return FALSE; + } + if (!strncmp(subcmd, "/engine/", 8)) + { + if (!sm->cmd_target.process_cmd) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "The engine %s has no command target defined", sm->engine_name); + return FALSE; + } + return cbox_execute_sub(&sm->cmd_target, fb, cmd, subcmd + 7, error); + } + if (!strcmp(subcmd, "/set_bypass") && !strcmp(cmd->arg_types, "i")) + { + sm->bypass = CBOX_ARG_I(cmd, 0); + return TRUE; + } + return cbox_object_default_process_cmd(&sm->cmd_target, fb, cmd, error); +} + +void cbox_module_swap_pointers_and_free(struct cbox_module *sm, void **pptr, void *value) +{ + free(cbox_rt_swap_pointers(sm->rt, pptr, value)); +} + +void cbox_module_destroyfunc(struct cbox_objhdr *hdr) +{ + struct cbox_module *module = CBOX_H2O(hdr); + g_free(module->instance_name); + free(module->input_samples); + free(module->output_samples); + if (module->destroy) + module->destroy(module); + free(module); +} diff --git a/template/calfbox/module.h b/template/calfbox/module.h new file mode 100644 index 0000000..37b8134 --- /dev/null +++ b/template/calfbox/module.h @@ -0,0 +1,181 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_MODULE_H +#define CBOX_MODULE_H + +#include "dom.h" +#include "dspmath.h" +#include "errors.h" +#include "midi.h" + +#include + +CBOX_EXTERN_CLASS(cbox_module) + +#define CBOX_MAX_AUDIO_PORTS 36 + +struct cbox_engine; +struct cbox_rt; + +struct cbox_module_keyrange_metadata +{ + uint8_t channel; // 0 = omni + uint8_t low_key; + uint8_t high_key; + const char *name; +}; + +enum cbox_module_livecontroller_class +{ + cmlc_onoffcc, + cmlc_continuouscc, + cmlc_continuous, + cmlc_discrete, + cmlc_enum +}; + +struct cbox_module_livecontroller_metadata +{ + uint8_t channel; + enum cbox_module_livecontroller_class controller_class:8; + uint16_t controller; + const char *name; + void *extra_info; +}; + +struct cbox_module_voicingparam_metadata +{ +}; + +struct cbox_module +{ + CBOX_OBJECT_HEADER() + void *user_data; + struct cbox_rt *rt; + struct cbox_engine *engine; + const char *engine_name; + gchar *instance_name; + cbox_sample_t *input_samples; + cbox_sample_t *output_samples; + struct cbox_midi_buffer midi_input; + uint32_t inputs, outputs, aux_offset; + int bypass; + int srate; + double srate_inv; + + struct cbox_command_target cmd_target; + + void (*process_event)(struct cbox_module *module, const uint8_t *data, uint32_t len); + void (*process_block)(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs); + void (*destroy)(struct cbox_module *module); +}; + +struct cbox_module_manifest +{ + void *user_data; + const char *name; + int min_inputs; + int min_outputs; + + struct cbox_module_keyrange_metadata *keyranges; + int num_keyranges; + + struct cbox_module_livecontroller_metadata *live_controllers; + int num_live_controllers; + + struct cbox_module_voicingparam_metadata *voicing_params; + int num_voicing_params; + + struct cbox_module *(*create)(void *user_data, const char *cfg_section, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, GError **error); +}; + +#define DEFINE_MODULE(modname, ninputs, noutputs) \ + struct cbox_module_manifest modname##_module = { \ + NULL, \ + .name = #modname, \ + .min_inputs = ninputs, \ + .min_outputs = noutputs, \ + .keyranges = modname##_keyranges, \ + .num_keyranges = sizeof(modname##_keyranges)/sizeof(modname##_keyranges[0]), \ + .live_controllers = modname##_controllers, \ + .num_live_controllers = sizeof(modname##_controllers)/sizeof(modname##_controllers[0]), \ + .create = modname##_create \ + }; + +extern struct cbox_module_manifest *cbox_module_list[]; + +extern void cbox_module_manifest_dump(struct cbox_module_manifest *manifest); +extern struct cbox_module_manifest *cbox_module_manifest_get_by_name(const char *name); +extern struct cbox_module *cbox_module_manifest_create_module(struct cbox_module_manifest *manifest, const char *cfg_section, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, const char *instance_name, GError **error); + +extern struct cbox_module *cbox_module_new_from_fx_preset(const char *name, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, GError **error); + +extern void cbox_module_init(struct cbox_module *module, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, void *user_data, int inputs, int outputs, cbox_process_cmd cmd_handler, void (*destroy)(struct cbox_module *module)); +extern void cbox_module_swap_pointers_and_free(struct cbox_module *sm, void **pptr, void *value); + +extern gboolean cbox_module_slot_process_cmd(struct cbox_module **psm, struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, GError **error); + +#define EFFECT_PARAM_CLONE(res) \ + struct MODULE_PARAMS *res = malloc(sizeof(struct MODULE_PARAMS)); \ + memcpy(res, m->params, sizeof(struct MODULE_PARAMS)); \ + +#define EFFECT_PARAM(path, type, field, ctype, expr, minv, maxv) \ + if (!strcmp(cmd->command, path) && !strcmp(cmd->arg_types, type)) \ + { \ + ctype value = *(ctype *)cmd->arg_values[0]; \ + if (value < minv || value > maxv) \ + return cbox_set_range_error(error, path, minv, maxv);\ + EFFECT_PARAM_CLONE(pp); \ + pp->field = expr(value); \ + cbox_module_swap_pointers_and_free(&m->module, (void **)&m->params, pp); \ + } \ + +#define EFFECT_PARAM_ARRAY(path, type, array, field, ctype, expr, minv, maxv) \ + if (!strcmp(cmd->command, path) && !strcmp(cmd->arg_types, "i" type)) \ + { \ + int pos = *(int *)cmd->arg_values[0]; \ + ctype value = *(ctype *)cmd->arg_values[1]; \ + if (value < minv || value > maxv) \ + return cbox_set_range_error(error, path, minv, maxv);\ + EFFECT_PARAM_CLONE(pp); \ + pp->array[pos].field = expr(value); \ + cbox_module_swap_pointers_and_free(&m->module, (void **)&m->params, pp); \ + } \ + +#define MODULE_CREATE_FUNCTION(module) \ + struct cbox_module *module##_create(void *user_data, const char *cfg_section, struct cbox_document *doc, struct cbox_rt *rt, struct cbox_engine *engine, GError **error) + +#define MODULE_PROCESSCMD_FUNCTION(module) \ + gboolean module##_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) + +#define MODULE_SIMPLE_DESTROY_FUNCTION(module) \ + static void module##_destroyfunc(struct cbox_module *module_) \ + { \ + struct module##_module *m = (struct module##_module *)module_; \ + free(m->params); \ + } + +#define CALL_MODULE_INIT(m, inputs, outputs, name) \ + cbox_module_init(&(m)->module, doc, rt, engine, (m), inputs, outputs, name##_process_cmd, name##_destroyfunc); + +#define CALL_MODULE_INIT_SIMPLE(m, inputs, outputs) \ + cbox_module_init(&(m)->module, doc, rt, engine, (m), inputs, outputs, NULL, NULL); + + +#endif diff --git a/template/calfbox/novabox.py b/template/calfbox/novabox.py new file mode 100644 index 0000000..e0e23e3 --- /dev/null +++ b/template/calfbox/novabox.py @@ -0,0 +1,107 @@ +# A primitive drum machine interface for Novation Nocturn. +# +# Usage: +# - make sure that Novation Nocturn is connected *and* that the USB device +# that corresponds to it can be opened by the current user +# - create an ini file containing a scene with a single instrument using +# a sampler or fluidsynth engine + some drum mappings in SFZ or SF2 +# - ensure that the ini file contains 7 drum patterns, pat1..pat7, these +# can be copied from cboxrc-example +# - user buttons 1..7 start drum patterns +# - user button 8 stops the playback +# - encoder knob 1 adjusts the volume +# - mixer button exits the application + +import math +import sys + +sys.path = ["./py"] + sys.path + +import nocturn +import cbox + +quit = False + +instr_name = cbox.Document.get_scene().status().instruments.keys()[0] + +def clamp(val, min, max): + if val < min: + val = min + elif val > max: + val = max + return val + +class NovaBox: + def __init__(self): + self.nocturn = nocturn.Nocturn() + self.cur_pattern = None + self.handlers = {} + self.handlers[83] = self.on_xfade_touch + for i in range(7): + self.handlers[112 + i] = lambda cmd, val: self.on_buttonN_press(cmd - 112) if val > 0 else None + self.handlers[112 + 7] = lambda cmd, val: self.on_button8_press() if val > 0 else None + self.handlers[64] = self.on_knob1_change + self.handlers[65] = self.on_knob2_change + self.handlers[127] = lambda cmd, val: self.on_mixer_press() if val > 0 else None + + def on_knob1_change(self, cmd, val): + scene = cbox.Document.get_scene() + instr = scene.status().instruments[instr_name][1] + gain = instr.get_things('/output/1/status', ['gain']).gain + if val > 63: + val = -128 + val + instr.cmd('/output/1/gain', None, gain + val * 0.5) + + def on_knob2_change(self, cmd, val): + tempo = cbox.GetThings("/master/status", ['tempo'], []).tempo + if val > 63: + val = -128 + val + tempo = clamp(tempo + val * 0.5, 30, 300) + cbox.do_cmd('/master/set_tempo', None, [tempo]) + + def on_buttonN_press(self, button): + cbox.do_cmd("/master/stop", None, []) + song = cbox.Document.get_song() + song.loop_single_pattern(lambda: song.load_drum_pattern('pat%d' % (button + 1))) + cbox.do_cmd("/master/seek_ppqn", None, [0]) + cbox.do_cmd("/master/play", None, []) + self.cur_pattern = button + + def on_button8_press(self): + self.cur_pattern = None + cbox.do_cmd("/master/stop", None, []) + + def on_mixer_press(self): + global quit + quit = True + + def on_xfade_touch(self, cmd, val): + if val > 0: + print "Do not touch" + + def handler(self, cmd, val): + if cmd in self.handlers: + self.handlers[cmd](cmd, val) + return + + def poll(self): + self.nocturn.poll(self.handler) + + def update(self): + scene = cbox.Document.get_scene() + cmds = nocturn.NocturnCommands() + master = cbox.GetThings("/master/status", ['playing'], []) + for i in range(7): + cmds.setModeButtonLight(i, self.cur_pattern == i) + gain = scene.status().instruments[instr_name][1].get_things('/output/1/status', ['gain']).gain + cmds.setEncoderMode(0, 0) + cmds.setEncoderValue(0, clamp(int(gain * 2 + 64), 0, 127)) + + cmds.setModeButtonLight(7, self.cur_pattern is None) + self.nocturn.execute(cmds) + +nb = NovaBox() +while not quit: + nb.poll() + nb.update() +nb.nocturn.reset() diff --git a/template/calfbox/onepole-float.h b/template/calfbox/onepole-float.h new file mode 100644 index 0000000..291ce72 --- /dev/null +++ b/template/calfbox/onepole-float.h @@ -0,0 +1,205 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_ONEPOLE_FLOAT_H +#define CBOX_ONEPOLE_FLOAT_H + +#include "dspmath.h" + +struct cbox_onepolef_state +{ + float x1; + float y1; +}; + +struct cbox_onepolef_coeffs +{ + float a0; + float a1; + float b1; +}; + +static inline void cbox_onepolef_reset(struct cbox_onepolef_state *state) +{ + state->x1 = state->y1 = 0.f; +} + +static inline void cbox_onepolef_set_lowpass(struct cbox_onepolef_coeffs *coeffs, float w) +{ + float x = tan (w * 0.5f); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + + coeffs->a0 = a01; + coeffs->a1 = a01; + coeffs->b1 = b1; +} + +static inline void cbox_onepolef_set_highpass(struct cbox_onepolef_coeffs *coeffs, float w) +{ + float x = tan (w * 0.5f); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + + coeffs->a0 = q; + coeffs->a1 = -q; + coeffs->b1 = b1; +} + +static inline void cbox_onepolef_set_highshelf_tonectl(struct cbox_onepolef_coeffs *coeffs, float w, float g0) +{ + float x = tan (w * 0.5f); + float q = 1 / (1 + x); + float b1 = x * q - q; + + coeffs->a0 = 0.5 * (1 + b1 + g0 - b1 * g0); + coeffs->a1 = 0.5 * (1 + b1 - g0 + b1 * g0); + coeffs->b1 = b1; +} + +static inline void cbox_onepolef_set_highshelf_setgain(struct cbox_onepolef_coeffs *coeffs, float g0) +{ + coeffs->a0 = 0.5 * (1 + coeffs->b1 + g0 - coeffs->b1 * g0); + coeffs->a1 = 0.5 * (1 + coeffs->b1 - g0 + coeffs->b1 * g0); +} + +static inline void cbox_onepolef_set_allpass(struct cbox_onepolef_coeffs *coeffs, float w) +{ + float x = tan (w * 0.5f); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + + coeffs->a0 = b1; + coeffs->a1 = 1; + coeffs->b1 = b1; +} + +static inline float cbox_onepolef_process_sample(struct cbox_onepolef_state *state, struct cbox_onepolef_coeffs *coeffs, float in) +{ + float out = sanef(coeffs->a0 * in + coeffs->a1 * state->x1 - coeffs->b1 * state->y1); + + state->x1 = in; + state->y1 = out; + return out; +} + +#if USE_NEON_NOTREALLYFASTER + +#include + +static inline void cbox_onepolef_process(struct cbox_onepolef_state *state, struct cbox_onepolef_coeffs *coeffs, float *buffer) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float b1 = coeffs->b1; + float32x2_t a00 = {1, a0}; + float32x2_t ab1 = {a1, -b1}; + float32x2_t xy = {state->x1, state->y1}; + float32x2_t zero = {0, 0}; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float32x2_t inin = vdup_n_f32(buffer[i]); // {in, in} + float32x2_t xymul = vmul_f32(ab1, xy); // {x1 * a1, y1 * b1} + xymul = vpadd_f32(zero, xymul); // {0, x1 * a1 + y1 * b1} + xy = vmla_f32(xymul, inin, a00); // {in, a0 * in + a1 * x1 + b1 * y1} + + buffer[i] = xy[1]; + } + state->x1 = xy[0]; + state->y1 = sanef(xy[1]); +} + +#else +static inline void cbox_onepolef_process(struct cbox_onepolef_state *state, struct cbox_onepolef_coeffs *coeffs, float *buffer) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float b1 = coeffs->b1; + float x1 = state->x1; + float y1 = state->y1; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float in = buffer[i]; + double out = a0 * in + a1 * x1 - b1 * y1; + + buffer[i] = out; + x1 = in; + y1 = out; + } + state->x1 = x1; + state->y1 = sanef(y1); +} +#endif + +static inline void cbox_onepolef_process_stereo(struct cbox_onepolef_state *lstate, struct cbox_onepolef_state *rstate, struct cbox_onepolef_coeffs *coeffs, float *buffer) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float b1 = coeffs->b1; + float lx1 = lstate->x1; + float ly1 = lstate->y1; + float rx1 = rstate->x1; + float ry1 = rstate->y1; + + for (i = 0; i < 2 * CBOX_BLOCK_SIZE; i += 2) + { + float inl = buffer[i], inr = buffer[i + 1]; + double outl = a0 * inl + a1 * lx1 - b1 * ly1; + double outr = a0 * inr + a1 * rx1 - b1 * ry1; + + buffer[i] = outl; + buffer[i + 1] = outr; + lx1 = inl; + ly1 = outl; + rx1 = inr; + ry1 = outr; + } + lstate->x1 = lx1; + lstate->y1 = sanef(ly1); + rstate->x1 = rx1; + rstate->y1 = sanef(ry1); +} + +static inline void cbox_onepolef_process_to(struct cbox_onepolef_state *state, struct cbox_onepolef_coeffs *coeffs, float *buffer_in, float *buffer_out) +{ + int i; + float a0 = coeffs->a0; + float a1 = coeffs->a1; + float b1 = coeffs->b1; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float in = buffer_in[i]; + double out = a0 * in + a1 * state->x1 - b1 * state->y1; + + buffer_out[i] = out; + state->x1 = in; + state->y1 = out; + } + state->y1 = sanef(state->y1); +} + +#endif diff --git a/template/calfbox/onepole-int.h b/template/calfbox/onepole-int.h new file mode 100644 index 0000000..41ebf27 --- /dev/null +++ b/template/calfbox/onepole-int.h @@ -0,0 +1,120 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_ONEPOLE_INT_H +#define CBOX_ONEPOLE_INT_H + +#include "dspmath.h" + +struct cbox_onepole_state +{ + int32_t x1; + int32_t y1; +}; + +struct cbox_onepole_coeffs +{ + int32_t a0; + int32_t a1; + int32_t b1; + int shift; +}; + +static inline void cbox_onepole_reset(struct cbox_onepole_state *state) +{ + state->x1 = state->y1 = 0; +} + +static inline void cbox_onepole_set_lowpass(struct cbox_onepole_coeffs *coeffs, float w) +{ + float x = tan (w); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + int shift = 28; + float scaler = (1 << shift); + + coeffs->a1 = coeffs->a0 = (int32_t)(a01 * scaler); + coeffs->b1 = (int32_t)(b1 * scaler); + coeffs->shift = shift; +} + +static inline void cbox_onepole_set_highpass(struct cbox_onepole_coeffs *coeffs, float w) +{ + float x = tan (w); + float q = 1 / (1 + x); + float a01 = x*q; + float b1 = a01 - q; + int shift = 28; + float scaler = (1 << shift)-1; + + coeffs->a0 = (int32_t)(a01 * scaler); + coeffs->a1 = -coeffs->a0; + coeffs->b1 = (int32_t)(b1 * scaler); + coeffs->shift = shift; +} + +static inline void cbox_onepole_process(struct cbox_onepole_state *state, struct cbox_onepole_coeffs *coeffs, int32_t *buffer) +{ + int i; + int64_t a0 = coeffs->a0; + int64_t a1 = coeffs->a1; + int64_t b1 = coeffs->b1; + int shift = coeffs->shift; + int64_t maxint = ((int64_t)0x7FFFFFF) << shift; + int32_t round = 1 << (shift - 1); + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + int32_t in = buffer[i]; + int64_t v = a0 * in + a1 * state->x1 - b1 * state->y1 + round; + int32_t out = (llabs(v) >= maxint) ? (v > 0 ? 0x7FFFFFFF : -0x7FFFFFFF) : (v >> shift); + + buffer[i] = out; + state->x1 = in; + state->y1 = out; + } + if (state->y1 > 0 && state->y1 < round) + state->y1--; + if (state->y1 < 0 && state->y1 > -round) + state->y1++; +} + +static inline void cbox_onepole_process_to(struct cbox_onepole_state *state, struct cbox_onepole_coeffs *coeffs, int32_t *buffer_in, int32_t *buffer_out) +{ + int i; + int64_t a0 = coeffs->a0; + int64_t a1 = coeffs->a1; + int64_t b1 = coeffs->b1; + int shift = coeffs->shift; + int64_t maxint = ((int64_t)0x7FFFFFF) << shift; + int64_t round = 1 << (shift - 1); + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + int32_t in = buffer_in[i]; + int64_t v = a0 * in + a1 * state->x1 - b1 * state->y1 + round; + int32_t out = (llabs(v) >= maxint) ? (v > 0 ? 0x7FFFFFFF : -0x7FFFFFFF) : (v >> shift); + + buffer_out[i] = out; + state->x1 = in; + state->y1 = out; + } +} + +#endif diff --git a/template/calfbox/pattern-maker.c b/template/calfbox/pattern-maker.c new file mode 100644 index 0000000..3b884c6 --- /dev/null +++ b/template/calfbox/pattern-maker.c @@ -0,0 +1,183 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "errors.h" +#include "pattern.h" +#include "pattern-maker.h" +#include "song.h" +#include +#if USE_LIBSMF +#include +#endif + +struct event_entry +{ + uint32_t time; + uint8_t data[4]; +}; + +static gint event_entry_compare(gconstpointer a, gconstpointer b, gpointer unused) +{ + const struct event_entry *ea = a, *eb = b; + // Event ordering - it's to ensure bank changes are emitted before program + // changes and program changes are emitted before notes. + static const char event_class[8] = { + 8, // Note Off + 9, // Note On + 20, // Poly Pressure + 4, // Control Change + 6, // Program Change + 16, // Mono Pressure + 18, // Pitch Wheel + 0, // SysEx/Realtime + }; + + if (ea->time < eb->time) + return -1; + if (ea->time == eb->time && event_class[(ea->data[0] >> 4) & 7] < event_class[(eb->data[0] >> 4) & 7]) + return -1; + if (ea->time == eb->time && (ea->data[0] & 15) < (eb->data[0] & 15)) + return -1; + if (ea->time == eb->time && ea->data[0] == eb->data[0] && ea->data[1] < eb->data[1]) + return -1; + if (ea->time == eb->time && ea->data[0] == eb->data[0] && ea->data[1] == eb->data[1]) + return 0; + return +1; +} + +static void event_entry_destroy(gpointer p) +{ + struct event_entry *e = p; + free(e); +} + +struct cbox_midi_pattern_maker +{ + CBOX_OBJECT_HEADER() + GTree *events; + uint64_t ppqn_factor; +}; + +struct cbox_midi_pattern_maker *cbox_midi_pattern_maker_new(uint64_t ppqn_factor) +{ + struct cbox_midi_pattern_maker *maker = malloc(sizeof(struct cbox_midi_pattern_maker)); + maker->events = g_tree_new_full(event_entry_compare, NULL, event_entry_destroy, NULL); + maker->ppqn_factor = ppqn_factor; + return maker; +} + + +void cbox_midi_pattern_maker_add(struct cbox_midi_pattern_maker *maker, uint32_t time, uint8_t cmd, uint8_t val1, uint8_t val2) +{ + struct event_entry *e = malloc(sizeof(struct event_entry)); + e->time = time; + e->data[0] = cmd; + e->data[1] = val1; + e->data[2] = val2; + + g_tree_insert(maker->events, e, NULL); +} + +void cbox_midi_pattern_maker_add_mem(struct cbox_midi_pattern_maker *maker, uint32_t time, const uint8_t *src, uint32_t len) +{ + if (len > 3) + { + g_warning("Event size %d not supported yet, ignoring", (int)len); + return; + } + struct event_entry *e = malloc(sizeof(struct event_entry)); + e->time = time; + memcpy(e->data, src, len); + + g_tree_insert(maker->events, e, NULL); +} + +struct traverse_state +{ + struct cbox_midi_event *events; + int pos; +}; + +static gboolean traverse_func(gpointer key, gpointer value, gpointer pstate) +{ + struct traverse_state *state = pstate; + struct event_entry *e = key; + struct cbox_midi_event *event = &state->events[state->pos++]; + event->time = e->time; + event->size = midi_cmd_size(e->data[0]); + memcpy(event->data_inline, &e->data[0], 3); + return FALSE; +} + +extern void cbox_song_add_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern); + +struct cbox_midi_pattern *cbox_midi_pattern_maker_create_pattern(struct cbox_midi_pattern_maker *maker, struct cbox_song *song, gchar *name) +{ + struct cbox_midi_pattern *p = malloc(sizeof(struct cbox_midi_pattern)); + CBOX_OBJECT_HEADER_INIT(p, cbox_midi_pattern, CBOX_GET_DOCUMENT(song)); + cbox_command_target_init(&p->cmd_target, cbox_midi_pattern_process_cmd, p); + p->owner = NULL; + p->name = name; + p->event_count = g_tree_nnodes(maker->events); + p->events = malloc(sizeof(struct cbox_midi_event[1]) * p->event_count); + + struct traverse_state st = { p->events, 0 }; + + g_tree_foreach(maker->events, traverse_func, &st); + + CBOX_OBJECT_REGISTER(p); + + cbox_song_add_pattern(song, p); + + return p; +} + +#if USE_LIBSMF +gboolean cbox_midi_pattern_maker_load_smf(struct cbox_midi_pattern_maker *maker, const char *filename, int *length, GError **error) +{ + smf_t *smf = smf_load(filename); + if (!smf) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot load SMF file '%s'", filename); + return FALSE; + } + + int ppqn = smf->ppqn; + smf_event_t *event = NULL; + while ((event = smf_get_next_event(smf)) != NULL) { + if (smf_event_is_metadata(event)) + continue; + + cbox_midi_pattern_maker_add_mem(maker, event->time_pulses * 1.0 * maker->ppqn_factor / ppqn, event->midi_buffer, event->midi_buffer_length); + } + if (length) + *length = smf_get_length_pulses(smf) * 1.0 * maker->ppqn_factor / ppqn; + smf_delete(smf); + + return TRUE; +} +#endif + +void cbox_midi_pattern_maker_destroy(struct cbox_midi_pattern_maker *maker) +{ + g_tree_destroy(maker->events); + free(maker); +} + + diff --git a/template/calfbox/pattern-maker.h b/template/calfbox/pattern-maker.h new file mode 100644 index 0000000..64bd2f8 --- /dev/null +++ b/template/calfbox/pattern-maker.h @@ -0,0 +1,40 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_PATTERN_MAKER_H +#define CBOX_PATTERN_MAKER_H + +#include + +CBOX_EXTERN_CLASS(cbox_midi_pattern_maker) + +struct cbox_midi_pattern; +struct cbox_midi_pattern_maker; +struct cbox_song; + +extern struct cbox_midi_pattern_maker *cbox_midi_pattern_maker_new(uint64_t ppqn_factor); +extern void cbox_midi_pattern_maker_destroy(struct cbox_midi_pattern_maker *maker); + +extern gboolean cbox_midi_pattern_maker_load_smf(struct cbox_midi_pattern_maker *maker, const char *filename, int *length, GError **error); + +extern void cbox_midi_pattern_maker_add(struct cbox_midi_pattern_maker *maker, uint32_t time, uint8_t cmd, uint8_t val1, uint8_t val2); +extern void cbox_midi_pattern_maker_add_mem(struct cbox_midi_pattern_maker *maker, uint32_t time, const uint8_t *src, uint32_t len); + +extern struct cbox_midi_pattern *cbox_midi_pattern_maker_create_pattern(struct cbox_midi_pattern_maker *maker, struct cbox_song *owner, gchar *name); + +#endif diff --git a/template/calfbox/pattern.c b/template/calfbox/pattern.c new file mode 100644 index 0000000..a8ad1af --- /dev/null +++ b/template/calfbox/pattern.c @@ -0,0 +1,461 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "blob.h" +#include "config.h" +#include "config-api.h" +#include "pattern.h" +#include "pattern-maker.h" +#include "song.h" + +#include + +extern void cbox_song_remove_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern); + +CBOX_CLASS_DEFINITION_ROOT(cbox_midi_pattern) + +struct cbox_midi_pattern *cbox_midi_pattern_new_metronome(struct cbox_song *song, int ts, uint64_t ppqn_factor) +{ + struct cbox_midi_pattern_maker *m = cbox_midi_pattern_maker_new(ppqn_factor); + + int length = (int)ppqn_factor; + int channel = cbox_config_get_int("metronome", "channel", 10); + int accnote = cbox_config_get_note("metronome", "note_accent", 37); + int note = cbox_config_get_note("metronome", "note", 37); + + for (int i = 0; i < ts; i++) + { + int accent = !i && ts != 1; + cbox_midi_pattern_maker_add(m, length * i, 0x90 + channel - 1, accent ? accnote : note, accent ? 127 : 100); + cbox_midi_pattern_maker_add(m, length * i + 1, 0x80 + channel - 1, accent ? accnote : note, 0); + } + + struct cbox_midi_pattern *p = cbox_midi_pattern_maker_create_pattern(m, song, g_strdup_printf("click-%d", ts)); + p->loop_end = length * ts; + + cbox_midi_pattern_maker_destroy(m); + + return p; +} + +void cbox_midi_pattern_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_midi_pattern *pattern = CBOX_H2O(objhdr); + if (pattern->owner) + cbox_song_remove_pattern(pattern->owner, pattern); + g_free(pattern->name); + if (pattern->events != NULL) + free(pattern->events); + free(pattern); +} + +#if USE_LIBSMF +static int cbox_midi_pattern_load_smf_into(struct cbox_midi_pattern_maker *m, const char *smf) +{ + int length = 0; + if (!cbox_midi_pattern_maker_load_smf(m, smf, &length, NULL)) + { + g_error("Cannot load SMF file %s", smf); + return -1; + } + return length; +} +#endif + +static int cbox_midi_pattern_load_melodic_into(struct cbox_midi_pattern_maker *m, const char *name, int start_pos, int transpose, int transpose_to_note, uint64_t ppqn_factor) +{ + gchar *cfg_section = g_strdup_printf("pattern:%s", name); + + if (!cbox_config_has_section(cfg_section)) + { + g_error("Melodic pattern '%s' not found", name); + g_free(cfg_section); + return -1; + } + + gchar *smf = cbox_config_get_string(cfg_section, "smf"); +#if USE_LIBSMF + if (smf) + return cbox_midi_pattern_load_smf_into(m, smf); +#else + if (smf) + g_warning("libsmf disabled at build time, MIDI import functionality not available."); +#endif + + int length = ppqn_factor * cbox_config_get_int(cfg_section, "beats", 4); + int gchannel = cbox_config_get_int(cfg_section, "channel", 1); + int gswing = cbox_config_get_int(cfg_section, "swing", 0); + int gres = cbox_config_get_int(cfg_section, "resolution", 4); + int orignote = cbox_config_get_note(cfg_section, "base_note", 24); + if (transpose_to_note != -1) + transpose += transpose_to_note - orignote; + + for (int t = 1; ; t++) + { + gchar *tname = g_strdup_printf("track%d", t); + char *trkname = cbox_config_get_string(cfg_section, tname); + g_free(tname); + if (trkname) + { + tname = g_strdup_printf("%s_vel", trkname); + int vel = cbox_config_get_note(cfg_section, tname, 100); + g_free(tname); + tname = g_strdup_printf("%s_res", trkname); + int res = cbox_config_get_note(cfg_section, tname, gres); + g_free(tname); + tname = g_strdup_printf("%s_channel", trkname); + int channel = cbox_config_get_note(cfg_section, tname, gchannel); + g_free(tname); + tname = g_strdup_printf("%s_swing", trkname); + int swing = cbox_config_get_int(cfg_section, tname, gswing); + g_free(tname); + tname = g_strdup_printf("%s_notes", trkname); + const char *notes = cbox_config_get_string(cfg_section, tname); + g_free(tname); + if (!notes) + { + g_error("Invalid track %s", trkname); + } + const char *s = notes; + int t = 0; + while(1) + { + if (!*s) + break; + + gchar *note; + const char *comma = strchr(s, ','); + if (comma) + { + note = g_strndup(s, comma - s); + s = comma + 1; + } + else + { + note = g_strdup(s); + s += strlen(s); + } + + if (*note) + { + int pitch = note_from_string(note); + + int pos = t * ppqn_factor / res + start_pos; + if (t & 1) + pos += ppqn_factor * swing / (res * 24); + + int pos2 = (t + 1) * ppqn_factor / res + start_pos; + if (t & 1) + pos2 += ppqn_factor * swing / (res * 24); + + pitch += transpose; + + cbox_midi_pattern_maker_add(m, pos, 0x90 + channel - 1, pitch, vel); + cbox_midi_pattern_maker_add(m, pos2 - 1, 0x80 + channel - 1, pitch, 0); + } + t++; + } + } + else + break; + } + + g_free(cfg_section); + + return length; +} + +static int cbox_midi_pattern_load_drum_into(struct cbox_midi_pattern_maker *m, const char *name, int start_pos, uint64_t ppqn_factor) +{ + gchar *cfg_section = g_strdup_printf("drumpattern:%s", name); + + if (!cbox_config_has_section(cfg_section)) + { + g_error("Drum pattern '%s' not found", name); + g_free(cfg_section); + return -1; + } + + gchar *smf = cbox_config_get_string(cfg_section, "smf"); +#if USE_LIBSMF + if (smf) + return cbox_midi_pattern_load_smf_into(m, smf); +#else + if (smf) + g_warning("libsmf disabled at build time, MIDI import functionality not available."); +#endif + + int length = ppqn_factor * cbox_config_get_int(cfg_section, "beats", 4); + int channel = cbox_config_get_int(cfg_section, "channel", 10); + int gswing = cbox_config_get_int(cfg_section, "swing", 0); + int gres = cbox_config_get_int(cfg_section, "resolution", 4); + + for (int t = 1; ; t++) + { + gchar *tname = g_strdup_printf("track%d", t); + char *trkname = cbox_config_get_string(cfg_section, tname); + g_free(tname); + if (trkname) + { + tname = g_strdup_printf("%s_note", trkname); + int note = cbox_config_get_note(cfg_section, tname, -1); + g_free(tname); + tname = g_strdup_printf("%s_res", trkname); + int res = cbox_config_get_note(cfg_section, tname, gres); + g_free(tname); + tname = g_strdup_printf("%s_swing", trkname); + int swing = cbox_config_get_int(cfg_section, tname, gswing); + g_free(tname); + tname = g_strdup_printf("%s_trigger", trkname); + const char *trigger = cbox_config_get_string(cfg_section, tname); + g_free(tname); + if (!trigger || note == -1) + { + g_error("Invalid track %s", trkname); + } + int t = 0; + for (int i = 0; trigger[i]; i++) + { + int pos = t * ppqn_factor / res + start_pos; + if (t & 1) + pos += ppqn_factor * swing / (res * 24); + if (trigger[i] >= '1' && trigger[i] <= '9') + { + int amt = (trigger[i] - '0') * 127 / 9; + cbox_midi_pattern_maker_add(m, pos, 0x90 + channel - 1, note, amt); + cbox_midi_pattern_maker_add(m, pos + 1, 0x80 + channel - 1, note, 0); + t++; + } + if (trigger[i] == 'F') // flam + { + int dflam = ppqn_factor / 4; + int rnd = rand() & 7; + dflam += rnd / 2; + cbox_midi_pattern_maker_add(m, pos - dflam, 0x90 + channel - 1, note, 90+rnd); + cbox_midi_pattern_maker_add(m, pos - dflam + 1, 0x80 + channel - 1, note, 0); + cbox_midi_pattern_maker_add(m, pos , 0x90 + channel - 1, note, 120 + rnd); + cbox_midi_pattern_maker_add(m, pos + 1, 0x80 + channel - 1, note, 0); + t++; + } + if (trigger[i] == 'D') // drag + { + pos = (t + 1) * ppqn_factor / res + start_pos; + //if (!(t & 1)) + // pos += ppqn_factor * swing / (res * 24); + float dflam = ppqn_factor/8.0; + int rnd = rand() & 7; + cbox_midi_pattern_maker_add(m, pos - dflam*2, 0x90 + channel - 1, note, 70+rnd); + cbox_midi_pattern_maker_add(m, pos - dflam*2 + 1, 0x80 + channel - 1, note, 0); + cbox_midi_pattern_maker_add(m, pos - dflam, 0x90 + channel - 1, note, 60+rnd); + cbox_midi_pattern_maker_add(m, pos - dflam + 1, 0x80 + channel - 1, note, 0); + t++; + } + else if (trigger[i] == '.') + t++; + } + } + else + break; + } + + g_free(cfg_section); + + return length; +} + +struct cbox_midi_pattern *cbox_midi_pattern_load(struct cbox_song *song, const char *name, int is_drum, uint64_t ppqn_factor) +{ + struct cbox_midi_pattern_maker *m = cbox_midi_pattern_maker_new(ppqn_factor); + + int length = 0; + if (is_drum) + length = cbox_midi_pattern_load_drum_into(m, name, 0, ppqn_factor); + else + length = cbox_midi_pattern_load_melodic_into(m, name, 0, 0, -1, ppqn_factor); + struct cbox_midi_pattern *p = cbox_midi_pattern_maker_create_pattern(m, song, g_strdup(name)); + p->loop_end = length; + + cbox_midi_pattern_maker_destroy(m); + + return p; +} + +struct cbox_midi_pattern *cbox_midi_pattern_load_track(struct cbox_song *song, const char *name, int is_drum, uint64_t ppqn_factor) +{ + int length = 0; + struct cbox_midi_pattern_maker *m = cbox_midi_pattern_maker_new(ppqn_factor); + + gchar *cfg_section = g_strdup_printf(is_drum ? "drumtrack:%s" : "track:%s", name); + + if (!cbox_config_has_section(cfg_section)) + { + g_error("Drum track '%s' not found", name); + g_free(cfg_section); + return NULL; + } + + for (int p = 1; ; p++) + { + gchar *pname = g_strdup_printf("pos%d", p); + char *patname = cbox_config_get_string(cfg_section, pname); + g_free(pname); + if (patname) + { + int tplen = 0; + char *comma = strchr(patname, ','); + while(*patname) + { + char *v = comma ? g_strndup(patname, comma - patname) : g_strdup(patname); + patname = comma ? comma + 1 : patname + strlen(patname); + + int xpval = 0, xpnote = -1; + if (!is_drum) + { + char *xp = strchr(v, '+'); + if (xp) + { + *xp = '\0'; + xpval = atoi(xp + 1); + } + else + { + xp = strchr(v, '='); + if (xp) + { + *xp = '\0'; + xpnote = note_from_string(xp + 1); + } + } + } + int plen = 0; + int is_drum_pat = is_drum; + int nofs = 0; + if (*v == '@') + { + nofs = 1; + is_drum_pat = !is_drum_pat; + } + if (is_drum_pat) + plen = cbox_midi_pattern_load_drum_into(m, v + nofs, length, ppqn_factor); + else + plen = cbox_midi_pattern_load_melodic_into(m, v + nofs, length, xpval, xpnote, ppqn_factor); + g_free(v); + if (plen < 0) + { + cbox_midi_pattern_maker_destroy(m); + return NULL; + } + if (plen > tplen) + tplen = plen; + if (*patname) + comma = strchr(patname, ','); + } + length += tplen; + } + else + break; + } + + g_free(cfg_section); + + struct cbox_midi_pattern *p = cbox_midi_pattern_maker_create_pattern(m, song, g_strdup(name)); + p->loop_end = length; + + cbox_midi_pattern_maker_destroy(m); + + return p; +} + +struct cbox_midi_pattern *cbox_midi_pattern_new_from_blob(struct cbox_song *song, const struct cbox_blob *blob, int length, uint64_t ppqn_factor) +{ + struct cbox_midi_pattern_maker *m = cbox_midi_pattern_maker_new(ppqn_factor); + + struct cbox_blob_serialized_event event; + for (size_t i = 0; i < blob->size; i += sizeof(event)) + { + // not sure about alignment guarantees of Python buffers + memcpy(&event, ((uint8_t *)blob->data) + i, sizeof(event)); + cbox_midi_pattern_maker_add(m, event.time, event.cmd, event.byte1, event.byte2); + } + + struct cbox_midi_pattern *p = cbox_midi_pattern_maker_create_pattern(m, song, g_strdup("unnamed-blob")); + p->loop_end = length; + + cbox_midi_pattern_maker_destroy(m); + + return p; +} + +struct cbox_blob *cbox_midi_pattern_to_blob(struct cbox_midi_pattern *pat, int *length) +{ + if (length) + *length = pat->loop_end; + + struct cbox_blob_serialized_event event; + int size = 0; + for (uint32_t i = 0; i < pat->event_count; i++) + { + // currently sysex events and the like are not supported + if (pat->events[i].size < 4) + size += sizeof(event); + } + + struct cbox_blob *blob = cbox_blob_new(size); + + size = 0; + uint8_t *data = blob->data; + for (uint32_t i = 0; i < pat->event_count; i++) + { + // currently sysex events and the like are not supported + const struct cbox_midi_event *src = &pat->events[i]; + if (src->size < 4) + { + event.time = src->time; + event.len = src->size; + memcpy(&event.cmd, &src->data_inline[0], event.len); + memcpy(data + size, &event, sizeof(event)); + size += sizeof(event); + } + } + return blob; +} + +gboolean cbox_midi_pattern_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_midi_pattern *p = ct->user_data; + + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + return cbox_execute_on(fb, NULL, "/event_count", "i", error, (int)p->event_count) && + cbox_execute_on(fb, NULL, "/loop_end", "i", error, (int)p->loop_end) && + cbox_execute_on(fb, NULL, "/name", "s", error, p->name) && + CBOX_OBJECT_DEFAULT_STATUS(p, fb, error) + ; + } + else if (!strcmp(cmd->command, "/name") && !strcmp(cmd->arg_types, "s")) + { + char *old_name = p->name; + p->name = g_strdup(CBOX_ARG_S(cmd, 0)); + g_free(old_name); + return TRUE; + } + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} diff --git a/template/calfbox/pattern.h b/template/calfbox/pattern.h new file mode 100644 index 0000000..7cd1982 --- /dev/null +++ b/template/calfbox/pattern.h @@ -0,0 +1,57 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_PATTERN_H +#define CBOX_PATTERN_H + +#include "dom.h" +#include "master.h" +#include "midi.h" + +CBOX_EXTERN_CLASS(cbox_midi_pattern) + +struct cbox_blob; +struct cbox_song; + +struct cbox_midi_pattern +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + struct cbox_song *owner; + gchar *name; + struct cbox_midi_event *events; + uint32_t event_count; + int loop_end; +}; + +struct cbox_blob_serialized_event +{ + int32_t time; + unsigned char len, cmd, byte1, byte2; +}; + +extern struct cbox_midi_pattern *cbox_midi_pattern_new_metronome(struct cbox_song *song, int ts, uint64_t ppqn_factor); +extern struct cbox_midi_pattern *cbox_midi_pattern_load(struct cbox_song *song, const char *name, int is_drum, uint64_t ppqn_factor); +extern struct cbox_midi_pattern *cbox_midi_pattern_load_track(struct cbox_song *song, const char *name, int is_drum, uint64_t ppqn_factor); +extern struct cbox_midi_pattern *cbox_midi_pattern_new_from_blob(struct cbox_song *song, const struct cbox_blob *blob, int length, uint64_t ppqn_factor); + +extern struct cbox_blob *cbox_midi_pattern_to_blob(struct cbox_midi_pattern *pat, int *length); + +extern gboolean cbox_midi_pattern_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +#endif diff --git a/template/calfbox/phaser.c b/template/calfbox/phaser.c new file mode 100644 index 0000000..4d1196f --- /dev/null +++ b/template/calfbox/phaser.c @@ -0,0 +1,251 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-float.h" +#include +#include +#include +#include +#include +#include +#include + +#define NO_STAGES 12 + +#define MODULE_PARAMS phaser_params + +struct phaser_params +{ + float center; + float mdepth; + float fb_amt; + float lfo_freq; + float sphase; + float wet_dry; + int stages; +}; + +#if USE_NEON +#include + +struct cbox_onepolef_stereo_state_neon { + float32x2_t x1; + float32x2_t y1; +}; +#endif + +struct phaser_module +{ + struct cbox_module module; + +#if USE_NEON + struct cbox_onepolef_stereo_state_neon state[NO_STAGES]; +#else + struct cbox_onepolef_state state[NO_STAGES][2]; +#endif + struct cbox_onepolef_coeffs coeffs[2]; + float fb[2]; + float tpdsr; + struct phaser_params *params; + + float phase; +}; + +gboolean phaser_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct phaser_module *m = (struct phaser_module *)ct->user_data; + + EFFECT_PARAM("/center_freq", "f", center, double, , 10, 20000) else + EFFECT_PARAM("/mod_depth", "f", mdepth, double, , 0, 10000) else + EFFECT_PARAM("/fb_amt", "f", fb_amt, double, , -1, 1) else + EFFECT_PARAM("/lfo_freq", "f", lfo_freq, double, , 0, 20) else + EFFECT_PARAM("/stereo_phase", "f", sphase, double, deg2rad, 0, 360) else + EFFECT_PARAM("/wet_dry", "f", wet_dry, double, , 0, 1) else + EFFECT_PARAM("/stages", "i", stages, int, , 1, 12) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/center_freq", "f", error, m->params->center) && + cbox_execute_on(fb, NULL, "/mod_depth", "f", error, m->params->mdepth) && + cbox_execute_on(fb, NULL, "/fb_amt", "f", error, m->params->fb_amt) && + cbox_execute_on(fb, NULL, "/lfo_freq", "f", error, m->params->lfo_freq) && + cbox_execute_on(fb, NULL, "/stereo_phase", "f", error, rad2deg(m->params->sphase)) && + cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry) && + cbox_execute_on(fb, NULL, "/stages", "i", error, m->params->stages) && + CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void phaser_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct phaser_module *m = (struct phaser_module *)module; +} + +static inline float clip_w(float w) +{ + if (w > 0.9 * M_PI) + return 0.9 * M_PI; + return w; +} + +void phaser_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct phaser_module *m = (struct phaser_module *)module; + struct phaser_params *p = m->params; + int s, i; + int stages = p->stages; + float fb_amt = p->fb_amt; + if (stages < 0 || stages > NO_STAGES) + stages = 0; + + if (p->mdepth) + { + cbox_onepolef_set_allpass(&m->coeffs[0], clip_w(m->tpdsr * p->center * cent2factor(p->mdepth * sin(m->phase)))); + cbox_onepolef_set_allpass(&m->coeffs[1], clip_w(m->tpdsr * p->center * cent2factor(p->mdepth * sin(m->phase + p->sphase)))); + } + else + { + cbox_onepolef_set_allpass(&m->coeffs[0], m->tpdsr * p->center); + cbox_onepolef_set_allpass(&m->coeffs[1], m->tpdsr * p->center); + } + m->phase += p->lfo_freq * CBOX_BLOCK_SIZE * m->tpdsr; + +#if USE_NEON + float *__restrict input1 = inputs[0]; + float *__restrict input2 = inputs[1]; + float *__restrict output1 = outputs[0]; + float *__restrict output2 = outputs[1]; + float32x2_t wetdry = {p->wet_dry, p->wet_dry}; + float32x2_t fb_amt2 = {fb_amt, fb_amt}; + float32x2_t fb = {m->fb[0], m->fb[1]}; + float32x2_t a0 = {m->coeffs[0].a0, m->coeffs[1].a0}; + float32x2_t a1 = {m->coeffs[0].a1, m->coeffs[1].a1}; + float32x2_t b1 = {m->coeffs[0].b1, m->coeffs[1].b1}; + float32x2_t zero = {0.f, 0.f}; + const float32x2_t thresh = {(1.0 / (65536.0 * 65536.0)), (1.0 / (65536.0 * 65536.0))}; + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float32x2_t dry = {input1[i], input2[i]}; + float32x2_t wet = vsub_f32(dry, vmul_f32(fb, fb_amt2)); + for (s = 0; s < (stages & ~1); s += 2) + { + // wet = sanef(coeffs->a0 * wet + coeffs->a1 * state->x1 - coeffs->b1 * state->y1); + float32x2_t pre = wet; + wet = vmla_f32(vmls_f32(vmul_f32(a1, m->state[s].x1), b1, m->state[s].y1), a0, wet); + m->state[s].x1 = pre; + m->state[s].y1 = wet; + pre = wet; + wet = vmla_f32(vmls_f32(vmul_f32(a1, m->state[s + 1].x1), b1, m->state[s + 1].y1), a0, wet); + m->state[s + 1].x1 = pre; + m->state[s + 1].y1 = wet; + } + for (; s < stages; s++) + { + // wet = sanef(coeffs->a0 * wet + coeffs->a1 * state->x1 - coeffs->b1 * state->y1); + float32x2_t pre = wet; + wet = vmla_f32(vmls_f32(vmul_f32(a1, m->state[s].x1), b1, m->state[s].y1), a0, wet); + m->state[s].x1 = pre; + m->state[s].y1 = wet; + } + fb = wet; + wet = vadd_f32(dry, vmul_f32(vsub_f32(wet, dry), wetdry)); + output1[i] = wet[0]; + output2[i] = wet[1]; + } + // set values < threshold to zero + for (s = 0; s < stages; s++) + m->state[s].y1 = vbsl_f32(vcage_f32(m->state[s].y1, thresh), m->state[s].y1, zero); + m->fb[0] = fb[0]; + m->fb[1] = fb[1]; +#else + for (int c = 0; c < 2; c++) + { + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float dry = inputs[c][i]; + float wet = dry - m->fb[c] * fb_amt; + for (s = 0; s < stages; s++) + wet = cbox_onepolef_process_sample(&m->state[s][c], &m->coeffs[c], wet); + m->fb[c] = wet; + outputs[c][i] = dry + (wet - dry) * p->wet_dry; + } + } +#endif +} + +MODULE_SIMPLE_DESTROY_FUNCTION(phaser) + +MODULE_CREATE_FUNCTION(phaser) +{ + int b; + + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct phaser_module *m = malloc(sizeof(struct phaser_module)); + CALL_MODULE_INIT(m, 2, 2, phaser); + m->module.process_event = phaser_process_event; + m->module.process_block = phaser_process_block; + m->tpdsr = 2.0 * M_PI / m->module.srate; + m->phase = 0; + struct phaser_params *p = malloc(sizeof(struct phaser_params)); + m->params = p; + p->sphase = deg2rad(cbox_config_get_float(cfg_section, "stereo_phase", 90.f)); + p->lfo_freq = cbox_config_get_float(cfg_section, "lfo_freq", 1.f); + p->center = cbox_config_get_float(cfg_section, "center_freq", 1500.f); + p->mdepth = cbox_config_get_float(cfg_section, "mod_depth", 1200.f); + p->fb_amt = cbox_config_get_float(cfg_section, "feedback", 0.f); + p->wet_dry = cbox_config_get_float(cfg_section, "wet_dry", 0.5f); + p->stages = cbox_config_get_int(cfg_section, "stages", NO_STAGES); + +#if USE_NEON + for (b = 0; b < NO_STAGES; b++) + { + float32x2_t zero = {0.f, 0.f}; + m->state[b].x1 = zero; + m->state[b].y1 = zero; + } +#else + for (b = 0; b < NO_STAGES; b++) + for (int c = 0; c < 2; c++) + cbox_onepolef_reset(&m->state[b][c]); +#endif + + return &m->module; +} + + +struct cbox_module_keyrange_metadata phaser_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata phaser_controllers[] = { +}; + +DEFINE_MODULE(phaser, 2, 2) + diff --git a/template/calfbox/prefetch_pipe.c b/template/calfbox/prefetch_pipe.c new file mode 100644 index 0000000..13e397a --- /dev/null +++ b/template/calfbox/prefetch_pipe.c @@ -0,0 +1,309 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "prefetch_pipe.h" +#include "tarfile.h" +#include "wavebank.h" +#include +#include +#include +#include +#include + +// Don't bother fetching less than 4 (mono) or 8 KB (stereo) + +void cbox_prefetch_pipe_init(struct cbox_prefetch_pipe *pipe, uint32_t buffer_size, uint32_t min_buffer_frames) +{ + pipe->data = malloc(buffer_size); + pipe->buffer_size = buffer_size; + pipe->min_buffer_frames = min_buffer_frames; + pipe->sndfile = NULL; + pipe->state = pps_free; +} + +gboolean cbox_prefetch_pipe_openfile(struct cbox_prefetch_pipe *pipe) +{ + if (pipe->waveform->taritem) + pipe->sndfile = cbox_tarfile_opensndfile(pipe->waveform->tarfile, pipe->waveform->taritem, &pipe->sndstream, &pipe->info); + else + pipe->sndfile = sf_open(pipe->waveform->canonical_name, SFM_READ, &pipe->info); + if (!pipe->sndfile) + return FALSE; + pipe->file_pos_frame = sf_seek(pipe->sndfile, pipe->waveform->preloaded_frames, SEEK_SET); + if (pipe->file_loop_end > pipe->info.frames) + pipe->file_loop_end = pipe->info.frames; + pipe->buffer_loop_end = pipe->buffer_size / (sizeof(int16_t) * pipe->info.channels); + pipe->produced = pipe->file_pos_frame; + pipe->write_ptr = 0; + pipe->state = pps_active; + + return TRUE; +} + +void cbox_prefetch_pipe_consumed(struct cbox_prefetch_pipe *pipe, uint32_t frames) +{ + pipe->consumed += frames; +} + +void cbox_prefetch_pipe_fetch(struct cbox_prefetch_pipe *pipe) +{ + gboolean retry; + do { + retry = FALSE; + // XXXKF take consumption rate into account + + // How many frames left to consume + int32_t supply = pipe->produced - pipe->consumed; + if (supply < 0) + { + // Overrun already occurred. Cut the losses by skipping already missed + // part. + uint32_t overrun = -supply; + + // XXXKF This may or may not be stupid. I didn't put much thought into it. + pipe->produced += overrun; + pipe->file_pos_frame = sf_seek(pipe->sndfile, overrun, SEEK_CUR); + pipe->write_ptr += overrun; + if (pipe->write_ptr >= pipe->buffer_loop_end) + pipe->write_ptr %= pipe->buffer_loop_end; + } + // + if ((uint32_t)supply >= pipe->buffer_loop_end) + return; + + // How many frames to read to fill the full prefetch size + int32_t readsize = pipe->buffer_loop_end - supply; + if (readsize < (int32_t)pipe->min_buffer_frames) + return; + + if (pipe->write_ptr == pipe->buffer_loop_end) + pipe->write_ptr = 0; + + // If reading across buffer boundary, only read the part up to buffer + // end, and then retry from start of the buffer. + if (pipe->write_ptr + readsize > pipe->buffer_loop_end) + { + readsize = pipe->buffer_loop_end - pipe->write_ptr; + retry = TRUE; + } + // If past the file loop end, restart at file loop start + if (pipe->file_pos_frame >= pipe->file_loop_end) + { + if (pipe->file_loop_start == (uint32_t)-1 || (pipe->loop_count && pipe->play_count >= pipe->loop_count - 1)) + { + pipe->finished = TRUE; + for (int i = 0; i < readsize * pipe->info.channels; i++) + pipe->data[pipe->write_ptr * pipe->info.channels + i] = rand(); + break; + } + else + { + pipe->play_count++; + pipe->file_pos_frame = pipe->file_loop_start; + sf_seek(pipe->sndfile, pipe->file_loop_start, SEEK_SET); + } + } + // If reading across file loop boundary, read up to loop end and + // retry to restart + if (pipe->file_pos_frame + readsize > pipe->file_loop_end) + { + readsize = pipe->file_loop_end - pipe->file_pos_frame; + retry = TRUE; + } + + int32_t actread = sf_readf_short(pipe->sndfile, pipe->data + pipe->write_ptr * pipe->info.channels, readsize); + pipe->produced += actread; + pipe->file_pos_frame += actread; + pipe->write_ptr += actread; + } while(retry); +} + +void cbox_prefetch_pipe_closefile(struct cbox_prefetch_pipe *pipe) +{ + assert(pipe->state == pps_closing); + assert(pipe->sndfile); + sf_close(pipe->sndfile); + pipe->sndfile = NULL; + pipe->state = pps_free; +} + +void cbox_prefetch_pipe_close(struct cbox_prefetch_pipe *pipe) +{ + if (pipe->sndfile) + cbox_prefetch_pipe_closefile(pipe); + if (pipe->data) + { + free(pipe->data); + pipe->data = NULL; + } +} + +static void *prefetch_thread(void *user_data) +{ + struct cbox_prefetch_stack *stack = user_data; + + while(!stack->finished) + { + usleep(1000); + for (int i = 0; i < stack->pipe_count; i++) + { + struct cbox_prefetch_pipe *pipe = &stack->pipes[i]; + switch(pipe->state) + { + case pps_free: + case pps_finished: + case pps_error: + break; + case pps_opening: + if (!cbox_prefetch_pipe_openfile(pipe)) + pipe->state = pps_error; + assert(pipe->state != pps_opening); + break; + case pps_active: + if (pipe->returned) + pipe->state = pps_closing; + else + cbox_prefetch_pipe_fetch(pipe); + break; + case pps_closing: + cbox_prefetch_pipe_closefile(pipe); + break; + default: + break; + } + } + } + return 0; +} + +struct cbox_prefetch_stack *cbox_prefetch_stack_new(int npipes, uint32_t buffer_size, uint32_t min_buffer_frames) +{ + struct cbox_prefetch_stack *stack = calloc(1, sizeof(struct cbox_prefetch_stack)); + stack->pipes = calloc(npipes, sizeof(struct cbox_prefetch_pipe)); + stack->next_free_pipe = calloc(npipes, sizeof(int)); + + for (int i = 0; i < npipes; i++) + { + cbox_prefetch_pipe_init(&stack->pipes[i], buffer_size, min_buffer_frames); + stack->next_free_pipe[i] = i - 1; + } + stack->pipe_count = npipes; + stack->last_free_pipe = npipes - 1; + stack->finished = FALSE; + + if (pthread_create(&stack->thr_prefetch, NULL, prefetch_thread, stack)) + { + // XXXKF set thread priority + g_warning("Cannot create a prefetch thread. Exiting.\n"); + return NULL; + } + + return stack; +} + +struct cbox_prefetch_pipe *cbox_prefetch_stack_pop(struct cbox_prefetch_stack *stack, struct cbox_waveform *waveform, uint32_t file_loop_start, uint32_t file_loop_end, uint32_t loop_count) +{ + // The stack may include some pipes that are already returned but not yet + // fully prepared for opening a new file + int *ppos = &stack->last_free_pipe; + while(*ppos != -1 && stack->pipes[*ppos].state != pps_free) + ppos = &stack->next_free_pipe[*ppos]; + if (*ppos == -1) { + for (int i = 0; i < stack->pipe_count; ++i) { + printf("Pipe %d state %d next-free %d\n", i, stack->pipes[i].state, stack->next_free_pipe[i]); + } + printf("last_free_pipe %d\n", stack->last_free_pipe); + return NULL; + } + + int pos = *ppos; + struct cbox_prefetch_pipe *pipe = &stack->pipes[pos]; + + *ppos = stack->next_free_pipe[pos]; + stack->next_free_pipe[pos] = -1; + + pipe->waveform = waveform; + if (file_loop_start == (uint32_t)-1 && loop_count) + file_loop_start = 0; + pipe->file_loop_start = file_loop_start; + pipe->file_loop_end = file_loop_end; + pipe->buffer_loop_end = 0; + pipe->finished = FALSE; + pipe->returned = FALSE; + pipe->produced = waveform->preloaded_frames; + pipe->consumed = 0; + pipe->play_count = 0; + pipe->loop_count = loop_count; + + __sync_synchronize(); + pipe->state = pps_opening; + return pipe; +} + +void cbox_prefetch_stack_push(struct cbox_prefetch_stack *stack, struct cbox_prefetch_pipe *pipe) +{ + switch(pipe->state) + { + case pps_free: + assert(0); + break; + case pps_error: + case pps_closed: + pipe->state = pps_free; + break; + case pps_opening: + // Close the file as soon as open operation completes + pipe->returned = TRUE; + break; + default: + pipe->state = pps_closing; + break; + } + + __sync_synchronize(); + + int pos = pipe - stack->pipes; + assert(stack->next_free_pipe[pos] == -1); + stack->next_free_pipe[pos] = stack->last_free_pipe; + stack->last_free_pipe = pos; + + __sync_synchronize(); +} + +int cbox_prefetch_stack_get_active_pipe_count(struct cbox_prefetch_stack *stack) +{ + int count = 0; + for (int i = 0; i < stack->pipe_count; i++) + { + if (stack->pipes[i].state != pps_free) + count++; + } + return count; +} + +void cbox_prefetch_stack_destroy(struct cbox_prefetch_stack *stack) +{ + void *result = NULL; + stack->finished = TRUE; + pthread_join(stack->thr_prefetch, &result); + for (int i = 0; i < stack->pipe_count; i++) + cbox_prefetch_pipe_close(&stack->pipes[i]); + free(stack->next_free_pipe); + free(stack->pipes); + free(stack); +} diff --git a/template/calfbox/prefetch_pipe.h b/template/calfbox/prefetch_pipe.h new file mode 100644 index 0000000..8eef6f1 --- /dev/null +++ b/template/calfbox/prefetch_pipe.h @@ -0,0 +1,95 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_PREFETCH_PIPE_H +#define CBOX_PREFETCH_PIPE_H + +#include +#include +#include +#include +#include +#include + +#define PIPE_MIN_PREFETCH_SIZE_FRAMES 2048 + +struct cbox_waveform; + +enum cbox_prefetch_pipe_state +{ + pps_free, + pps_opening, + pps_active, + pps_finished, + pps_error, + pps_closing, + pps_closed, +}; + +struct cbox_prefetch_pipe +{ + union { + volatile enum cbox_prefetch_pipe_state state; + uint64_t atomic1; + }; + struct cbox_waveform *waveform; + struct cbox_tarfile_sndstream sndstream; + int16_t *data; + uint32_t buffer_size; + uint32_t min_buffer_frames; + SF_INFO info; + SNDFILE *sndfile; + uint32_t file_pos_frame; + uint32_t file_loop_start; + uint32_t file_loop_end; + uint32_t buffer_loop_end; + uint32_t play_count, loop_count; + size_t write_ptr; + size_t produced; + size_t consumed; + gboolean finished; + gboolean returned; +}; + +extern void cbox_prefetch_pipe_init(struct cbox_prefetch_pipe *pipe, uint32_t buffer_size, uint32_t min_buffer_frames); +extern void cbox_prefetch_pipe_consumed(struct cbox_prefetch_pipe *pipe, uint32_t frames); +extern void cbox_prefetch_pipe_close(struct cbox_prefetch_pipe *pipe); + +static inline uint32_t cbox_prefetch_pipe_get_remaining(struct cbox_prefetch_pipe *pipe) +{ + assert(pipe->consumed <= pipe->produced); + return pipe->produced - pipe->consumed; +} + +struct cbox_prefetch_stack +{ + struct cbox_prefetch_pipe *pipes; + int *next_free_pipe; + int pipe_count; + pthread_t thr_prefetch; + int last_free_pipe; + gboolean finished; +}; + +extern struct cbox_prefetch_stack *cbox_prefetch_stack_new(int npipes, uint32_t buffer_size, uint32_t min_buffer_frames); +extern struct cbox_prefetch_pipe *cbox_prefetch_stack_pop(struct cbox_prefetch_stack *stack, struct cbox_waveform *waveform, uint32_t file_loop_start, uint32_t file_loop_end, uint32_t loop_count); +extern void cbox_prefetch_stack_push(struct cbox_prefetch_stack *stack, struct cbox_prefetch_pipe *pipe); +extern int cbox_prefetch_stack_get_active_pipe_count(struct cbox_prefetch_stack *stack); +extern void cbox_prefetch_stack_destroy(struct cbox_prefetch_stack *stack); + +#endif diff --git a/template/calfbox/py/__init__.py b/template/calfbox/py/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/template/calfbox/py/_cbox2.py b/template/calfbox/py/_cbox2.py new file mode 100644 index 0000000..94cd182 --- /dev/null +++ b/template/calfbox/py/_cbox2.py @@ -0,0 +1,187 @@ +from ctypes import * +from ctypes.util import find_library +import numbers +import os +import logging + +def cbox_uuid_to_str(uuid_ptr): + uuid_str = create_string_buffer(40) + cb.cbox_uuid_tostring(uuid_ptr, uuid_str) + return uuid_str.value.decode() + +c_int32_p = POINTER(c_int32) +c_double_p = POINTER(c_double) +GQuark = c_int32 + +class GError(Structure): + _fields_ = [ + ( 'domain', GQuark ), + ( 'code', c_int ), + ( 'message', c_char_p ), + ] +GErrorPtr = POINTER(GError) +GErrorPtrPtr = POINTER(GErrorPtr) + +class OscCommand(Structure): + _fields_ = [ + ( 'command', c_char_p ), + ( 'arg_types', c_char_p ), + ( 'arg_values', POINTER(c_void_p) ), + ] + +class CboxBlob(Structure): + _fields_ = [ + ( 'data', c_void_p ), + ( 'size', c_ulong ), + ] + def bytes(self): + return string_at(self.data, self.size) + def __repr__(self): + return repr(self.bytes()) +CboxBlobPtr = POINTER(CboxBlob) + +class CboxObjHdr(Structure): + _fields_ = [ + ( 'class_ptr', c_void_p ), + ( 'owner', c_void_p ), + ( 'link_in_document', c_void_p ), + ( 'cbox_uuid', c_ubyte * 16 ), + ( 'stamp', c_uint64 ), + ] + +PROCESS_CMD_FUNC = CFUNCTYPE(c_bool, c_void_p, c_void_p, POINTER(OscCommand), GErrorPtrPtr) + +class CmdTarget(Structure): + _fields_ = [ + ( 'user_data', c_void_p ), + ( 'process_cmd', PROCESS_CMD_FUNC ), + ] +CmdTargetPtr = POINTER(CmdTarget) + +class PyCmdTarget(CmdTarget): + def __init__(self): + def process_cmd_func(cmd_target, fb_target, command, error_ptr_ptr): + cmd_target = CmdTargetPtr.from_address(cmd_target) + fb_target = CmdTargetPtr.from_address(fb_target) if fb_target else None + cmd = command.contents + argtypes = cmd.arg_types.decode() + data = cmd.arg_values + args = [None] * len(argtypes) + for i in range(len(argtypes)): + argtype = argtypes[i] + if argtype == 's': + args[i] = cast(data[i], c_char_p).value.decode() + elif argtype == 'i': + args[i] = cast(data[i], c_int32_p).contents.value + elif argtype == 'f': + args[i] = cast(data[i], c_double_p).contents.value + elif argtype == 'u': + args[i] = cbox_uuid_to_str(cast(data[i], c_void_p)) + elif argtype == 'b': + args[i] = cast(data[i], CboxBlobPtr).contents.bytes() + elif argtype == 'o': + args[i] = cbox_uuid_to_str(cast(data[i], POINTER(CboxObjHdr)).contents.cbox_uuid) + #elif argtype == 'N': + # args[i] = None + try: + self.process(cmd.command.decode(), args) + return True + except Exception as e: + cb.g_error_set(1, 1, str(e)) + return False + self.process_cmd = PROCESS_CMD_FUNC(process_cmd_func) + self.user_data = 0 + def process(self, cmd, args): + print ("%s(%s)" % (cmd, repr(args))) + +def find_calfbox(): + if "CALFBOXLIBABSPATH" in os.environ: + assert os.path.exists(os.environ["CALFBOXLIBABSPATH"]) + cblib = os.environ["CALFBOXLIBABSPATH"] + else: + cblib = find_library('calfbox') + logging.info("Loading calfbox shared library: %s" % (cblib)) + cb = cdll.LoadLibrary(cblib) + return cb + +cb = find_calfbox() +cb.cbox_embed_get_cmd_root.restype = CmdTargetPtr + +class CalfboxException(Exception): + pass + +def convert_exception(cls, gptr): + msg = gptr.contents.message + cb.g_error_free(gptr) + raise cls(msg.decode()) +class WrapCmdTarget(PyCmdTarget): + def __init__(self, fb): + PyCmdTarget.__init__(self) + self.fb = fb + def process(self, cmd, args): + self.fb(cmd, None, args) + +def init_engine(config=None): + gptr = GErrorPtr() + if not cb.cbox_embed_init_engine(config, byref(gptr)): + convert_exception(CalfboxException, gptr) +def start_audio(cmd_dumper=None): + gptr = GErrorPtr() + target = byref(WrapCmdTarget(cmd_dumper)) if cmd_dumper is not None else None + # XXXKF pass the callback + if not cb.cbox_embed_start_audio(target, byref(gptr)): + convert_exception(CalfboxException, gptr) +def stop_audio(): + gptr = GErrorPtr() + if not cb.cbox_embed_stop_audio(byref(gptr)): + convert_exception(CalfboxException, gptr) +def shutdown_engine(): + gptr = GErrorPtr() + if not cb.cbox_embed_shutdown_engine(byref(gptr)): + convert_exception(CalfboxException, gptr) +def do_cmd_on(target, cmd, fb, args): + gptr = GErrorPtr() + ocmd = OscCommand() + ocmd.command = cmd.encode() + acnt = len(args) + arg_types = create_string_buffer(acnt) + arg_values = (c_void_p * acnt)() + arg_space = (c_uint8 * (8 * acnt))() + # Scratch space for string encoding + tmp = [] + for i in range(len(args)): + a = args[i] + t = type(a) + if isinstance(a, numbers.Number): + if t is float: + arg_types[i] = b'f' + arg_values[i] = addressof(arg_space) + 8 * i + cast(arg_values[i], c_double_p).contents.value = a + else: + arg_types[i] = b'i' + arg_values[i] = addressof(arg_space) + 8 * i + cast(arg_values[i], c_int32_p).contents.value = a + elif t == str: + tmp.append(create_string_buffer(a.encode())) + arg_types[i] = b's' + arg_values[i] = addressof(tmp[-1]) + elif t == bytearray: + tmp.append(CboxBlob(cast((c_byte * len(a)).from_buffer(a), c_void_p), len(a))) + arg_types[i] = b'b' + arg_values[i] = addressof(tmp[-1]) + else: + arg_types[i] = b'N' + ocmd.arg_types = cast(arg_types, c_char_p) + ocmd.arg_values = arg_values + if fb is not None: + res = target.contents.process_cmd(target, byref(WrapCmdTarget(fb)), ocmd, gptr) + else: + res = target.contents.process_cmd(target, None, ocmd, gptr) + if not res: + if gptr and gptr.contents: + raise Exception(gptr.contents.message.decode()) + else: + raise Exception("Unknown error") + +def do_cmd(cmd, fb, args): + do_cmd_on(cb.cbox_embed_get_cmd_root(), cmd, fb, args) diff --git a/template/calfbox/py/cbox.py b/template/calfbox/py/cbox.py new file mode 100644 index 0000000..ac93bc6 --- /dev/null +++ b/template/calfbox/py/cbox.py @@ -0,0 +1,1147 @@ +from calfbox._cbox2 import * +from io import BytesIO +import struct +import sys +import traceback +import calfbox.metadata as metadata #local file metadata.py +type_wrapper_debug = False +is_python3 = not sys.version.startswith("2") + +############################################################################### +# Ugly internals. Please skip this section for your own sanity. +############################################################################### + +class GetUUID: + """An object that calls a C layer command, receives a /uuid callback from it + and stores the passed UUID in its uuid attribute. + + Example use: GetUUID('/command', arg1, arg2...).uuid + """ + def __init__(self, cmd, *cmd_args): + def callback(cmd, fb, args): + if cmd == "/uuid" and len(args) == 1: + self.uuid = args[0] + else: + raise ValueException("Unexpected callback: %s" % cmd) + self.callback = callback + self.uuid = None + do_cmd(cmd, self, list(cmd_args)) + def __call__(self, *args): + self.callback(*args) + +class GetThings: + """A generic callback object that receives various forms of information from + C layer and converts then into object's Python attributes. + + This is an obsolete interface, to be replaced by GetUUID or metaclass + based type-safe autoconverter. However, there are still some cases that + aren't (yet) handled by either. + """ + @staticmethod + def by_uuid(uuid, cmd, anames, args): + return GetThings(Document.uuid_cmd(uuid, cmd), anames, args) + def __init__(self, cmd, anames, args): + for i in anames: + if i.startswith("*"): + setattr(self, i[1:], []) + elif i.startswith("%"): + setattr(self, i[1:], {}) + else: + setattr(self, i, None) + anames = set(anames) + self.seq = [] + def update_callback(cmd, fb, args): + self.seq.append((cmd, fb, args)) + cmd = cmd[1:] + if cmd in anames: + if len(args) == 1: + setattr(self, cmd, args[0]) + else: + setattr(self, cmd, args) + elif "*" + cmd in anames: + if len(args) == 1: + getattr(self, cmd).append(args[0]) + else: + getattr(self, cmd).append(args) + elif "%" + cmd in anames: + if len(args) == 2: + getattr(self, cmd)[args[0]] = args[1] + else: + getattr(self, cmd)[args[0]] = args[1:] + elif "?" + cmd in anames: + setattr(self, cmd, bool(args[0])) + elif len(args) == 1: + setattr(self, cmd, args[0]) + do_cmd(cmd, update_callback, args) + def __str__(self): + return str(self.seq) + +class PropertyDecorator(object): + """Abstract property decorator.""" + def __init__(self, base): + self.base = base + def get_base(self): + return self.base + def map_cmd(self, cmd): + return cmd + +class AltPropName(PropertyDecorator): + """Command-name-changing property decorator. Binds a property to the + specified /path, different from the default one, which based on property name, + with -s and -es suffix removed for lists and dicts.""" + def __init__(self, alt_name, base): + PropertyDecorator.__init__(self, base) + self.alt_name = alt_name + def map_cmd(self, cmd): + return self.alt_name + def execute(self, property, proptype, klass): + pass + +class SettableProperty(PropertyDecorator): + """Decorator that creates a setter method for the property.""" + def execute(self, property, proptype, klass): + if type(proptype) is dict: + setattr(klass, 'set_' + property, lambda self, key, value: self.cmd('/' + property, None, key, value)) + elif type(proptype) is bool: + setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, 1 if value else 0)) + elif issubclass(proptype, DocObj): + setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, value.uuid)) + else: + setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, proptype(value))) + +def new_get_things(obj, cmd, settermap, args): + """Call C command with arguments 'args', populating a return object obj + using settermap to interpret callback commands and initialise the return + object.""" + def update_callback(cmd2, fb, args2): + try: + if cmd2 in settermap: + settermap[cmd2](obj, args2) + elif cmd2 != '/uuid': # Ignore UUID as it's usually safe to do so + print ("Unexpected command: %s" % cmd2) + except Exception as error: + traceback.print_exc() + raise + # Set initial values for the properties (None or empty dict/list) + for setterobj in settermap.values(): + setattr(obj, setterobj.property, setterobj.init_value()) + # Call command and apply callback commands via setters to the object + do_cmd(cmd, update_callback, args) + return obj + +def _error_arg_mismatch(required, passed): + raise ValueError("Types required: %s, values passed: %s" % (repr(required), repr(passed))) +def _handle_object_wrapping(t): + if issubclass(t, DocObj): + return lambda uuid: Document.map_uuid_and_check(uuid, t) + return t +def _make_args_to_type_lambda(t): + t = _handle_object_wrapping(t) + return lambda args: t(*args) +def _make_args_to_tuple_of_types_lambda(ts): + ts = list(map(_handle_object_wrapping, ts)) + return lambda args: tuple([ts[i](args[i]) for i in range(max(len(ts), len(args)))]) if len(ts) == len(args) else _error_arg_mismatch(ts, args) +def _make_args_decoder(t): + if type(t) is tuple: + return _make_args_to_tuple_of_types_lambda(t) + else: + return _make_args_to_type_lambda(t) + +def get_thing(cmd, fieldcmd, datatype, *args): + pull = False + if type(datatype) is list: + assert (len(datatype) == 1) + decoder = _make_args_decoder(datatype[0]) + value = [] + def adder(data): + value.append(decoder(data)) + elif type(datatype) is dict: + assert (len(datatype) == 1) + key_type, value_type = list(datatype.items())[0] + key_decoder = _make_args_decoder(key_type) + value_decoder = _make_args_decoder(value_type) + value = {} + def adder(data): + value[key_decoder([data[0]])] = value_decoder(data[1:]) + else: + decoder = _make_args_decoder(datatype) + def adder(data): + value[0] = decoder(data) + value = [None] + pull = True + def callback(cmd2, fb, args2): + if cmd2 == fieldcmd: + adder(args2) + else: + print ("Unexpected command %s" % cmd2) + do_cmd(cmd, callback, list(args)) + if pull: + return value[0] + else: + return value + +class SetterWithConversion: + """A setter object class that sets a specific property to a typed value or a tuple of typed value.""" + def __init__(self, property, extractor): + self.property = property + self.extractor = extractor + def init_value(self): + return None + def __call__(self, obj, args): + # print ("Setting attr %s on object %s" % (self.property, obj)) + setattr(obj, self.property, self.extractor(args)) + +class ListAdderWithConversion: + """A setter object class that adds a tuple filled with type-converted arguments of the + callback to a list. E.g. ListAdderWithConversion('foo', (int, int))(obj, [1,2]) + adds a tuple: (int(1), int(2)) to the list obj.foo""" + + def __init__(self, property, extractor): + self.property = property + self.extractor = extractor + def init_value(self): + return [] + def __call__(self, obj, args): + getattr(obj, self.property).append(self.extractor(args)) + +class DictAdderWithConversion: + """A setter object class that adds a tuple filled with type-converted + arguments of the callback to a dictionary under a key passed as first argument + i.e. DictAdderWithConversion('foo', str, (int, int))(obj, ['bar',1,2]) adds + a tuple: (int(1), int(2)) under key 'bar' to obj.foo""" + + def __init__(self, property, keytype, valueextractor): + self.property = property + self.keytype = keytype + self.valueextractor = valueextractor + def init_value(self): + return {} + def __call__(self, obj, args): + getattr(obj, self.property)[self.keytype(args[0])] = self.valueextractor(args[1:]) + +def _type_properties(base_type): + return {prop: getattr(base_type, prop) for prop in dir(base_type) if not prop.startswith("__")} + +def _create_setter(prop, t): + if type(t) in [type, tuple] or issubclass(type(t), DocObj): + if type_wrapper_debug: + print ("%s is type %s" % (prop, repr(t))) + return SetterWithConversion(prop, _make_args_decoder(t)) + elif type(t) is dict: + assert(len(t) == 1) + tkey, tvalue = list(t.items())[0] + if type_wrapper_debug: + print ("%s is type: %s -> %s" % (prop, repr(tkey), repr(tvalue))) + return DictAdderWithConversion(prop, tkey, _make_args_decoder(tvalue)) + elif type(t) is list: + assert(len(t) == 1) + if type_wrapper_debug: + print ("%s is array of %s" % (prop, repr(t[0]))) + return ListAdderWithConversion(prop, _make_args_decoder(t[0])) + else: + raise ValueError("Don't know what to do with property '%s' of type %s" % (prop, repr(t))) + +def _create_unmarshaller(name, base_type, object_wrapper = False, property_grabber = _type_properties): + all_decorators = {} + prop_types = {} + settermap = {} + if type_wrapper_debug: + print ("Wrapping type: %s" % name) + print ("-----") + for prop, proptype in property_grabber(base_type).items(): + decorators = [] + propcmd = '/' + prop + if type(proptype) in [list, dict]: + if propcmd.endswith('s'): + if propcmd.endswith('es'): + propcmd = propcmd[:-2] + else: + propcmd = propcmd[:-1] + while isinstance(proptype, PropertyDecorator): + decorators.append(proptype) + propcmd = proptype.map_cmd(propcmd) + proptype = proptype.get_base() + + settermap[propcmd] = _create_setter(prop, proptype) + all_decorators[prop] = decorators + prop_types[prop] = proptype + base_type.__str__ = lambda self: (str(name) + ":" + " ".join(["%s=%s" % (v.property, str(getattr(self, v.property))) for v in settermap.values()])) + if type_wrapper_debug: + print ("") + def exec_cmds(o): + for propname, decorators in all_decorators.items(): + for decorator in decorators: + decorator.execute(propname, prop_types[propname], o) + if object_wrapper: + return exec_cmds, lambda cmd: (lambda self, *args: new_get_things(base_type(), self.path + cmd, settermap, list(args))) + else: + return lambda cmd, *args: new_get_things(base_type(), cmd, settermap, list(args)) + +class NonDocObj(object): + """Root class for all wrapper classes that wrap objects that don't have + their own identity/UUID. + This covers various singletons and inner objects (e.g. engine in instruments).""" + class Status: + pass + def __init__(self, path): + self.path = path + def __new__(classObj, *args, **kwargs): + if is_python3: + result = object.__new__(classObj) + result.__init__(*args, **kwargs) + else: + result = object.__new__(classObj, *args, **kwargs) + name = classObj.__name__ + if getattr(classObj, 'wrapped_class', None) != name: + classfinaliser, cmdwrapper = _create_unmarshaller(name, classObj.Status, object_wrapper = True) + classfinaliser(classObj) + classObj.status = cmdwrapper('/status') + classObj.wrapped_class = name + return result + + def cmd(self, cmd, fb = None, *args): + do_cmd(self.path + cmd, fb, list(args)) + + def cmd_makeobj(self, cmd, *args): + return Document.map_uuid(GetUUID(self.path + cmd, *args).uuid) + + def get_things(self, cmd, fields, *args): + return GetThings(self.path + cmd, fields, list(args)) + + def get_thing(self, cmd, fieldcmd, type, *args): + return get_thing(self.path + cmd, fieldcmd, type, *args) + + def make_path(self, path): + return self.path + path + + def __str__(self): + return "%s<%s>" % (self.__class__.__name__, self.path) + +class DocObj(NonDocObj): + """Root class for all wrapper classes that wrap first-class document objects.""" + class Status: + pass + def __init__(self, uuid): + NonDocObj.__init__(self, Document.uuid_cmd(uuid, '')) + self.uuid = uuid + + def delete(self): + self.cmd("/delete") + + def __str__(self): + return "%s<%s>" % (self.__class__.__name__, self.uuid) + +class VarPath: + def __init__(self, path, args = []): + self.path = path + self.args = args + def plus(self, subpath, *args): + return VarPath(self.path if subpath is None else self.path + "/" + subpath, self.args + list(args)) + def set(self, *values): + do_cmd(self.path, None, self.args + list(values)) + +############################################################################### +# And those are the proper user-accessible objects. +############################################################################### + +class Config: + class KeysUnmarshaller: + keys = [str] + keys_unmarshaller = _create_unmarshaller('Config.keys()', KeysUnmarshaller) + + """INI file manipulation class.""" + @staticmethod + def sections(prefix = ""): + """Return a list of configuration sections.""" + return [CfgSection(name) for name in get_thing('/config/sections', '/section', [str], prefix)] + + @staticmethod + def keys(section, prefix = ""): + """Return a list of configuration keys in a section, with optional prefix filtering.""" + return Config.keys_unmarshaller('/config/keys', str(section), str(prefix)).keys + + @staticmethod + def get(section, key): + """Return a string value of a given key.""" + return get_thing('/config/get', '/value', str, str(section), str(key)) + + @staticmethod + def set(section, key, value): + """Set a string value for a given key.""" + do_cmd('/config/set', None, [str(section), str(key), str(value)]) + + @staticmethod + def delete(section, key): + """Delete a given key.""" + do_cmd('/config/delete', None, [str(section), str(key)]) + + @staticmethod + def save(filename = None): + """Save config, either into current INI file or some other file.""" + if filename is None: + do_cmd('/config/save', None, []) + else: + do_cmd('/config/save', None, [str(filename)]) + + @staticmethod + def add_section(section, content): + """Populate a config section based on a string with key=value lists. + This is a toy/debug function, it doesn't handle any edge cases.""" + for line in content.splitlines(): + line = line.strip() + if line == '' or line.startswith('#'): + continue + try: + key, value = line.split("=", 2) + except ValueError as err: + raise ValueError("Cannot parse config line '%s'" % line) + Config.set(section, key.strip(), value.strip()) + +class Transport: + @staticmethod + def seek_ppqn(ppqn): + do_cmd('/master/seek_ppqn', None, [int(ppqn)]) + @staticmethod + def seek_samples(samples): + do_cmd('/master/seek_samples', None, [int(samples)]) + @staticmethod + def set_tempo(tempo): + do_cmd('/master/set_tempo', None, [float(tempo)]) + @staticmethod + def set_timesig(nom, denom): + do_cmd('/master/set_timesig', None, [int(nom), int(denom)]) + @staticmethod + def set_ppqn_factor(factor): + do_cmd('/master/set_ppqn_factor', None, [int(factor)]) + @staticmethod + def play(): + do_cmd('/master/play', None, []) + @staticmethod + def stop(): + do_cmd('/master/stop', None, []) + @staticmethod + def panic(): + do_cmd('/master/panic', None, []) + @staticmethod + def status(): + return GetThings("/master/status", ['pos', 'pos_ppqn', 'tempo', 'timesig', 'sample_rate', 'playing', 'ppqn_factor'], []) + @staticmethod + def tell(): + return GetThings("/master/tell", ['pos', 'pos_ppqn', 'playing'], []) + @staticmethod + def ppqn_to_samples(pos_ppqn): + return get_thing("/master/ppqn_to_samples", '/value', int, pos_ppqn) + @staticmethod + def samples_to_ppqn(pos_samples): + return get_thing("/master/samples_to_ppqn", '/value', int, pos_samples) + +# Currently responsible for both JACK and USB I/O - not all functionality is +# supported by both. +class JackIO: + AUDIO_TYPE = "32 bit float mono audio" + MIDI_TYPE = "8 bit raw midi" + PORT_IS_SINK = 0x1 + PORT_IS_SOURCE = 0x2 + PORT_IS_PHYSICAL = 0x4 + PORT_CAN_MONITOR = 0x8 + PORT_IS_TERMINAL = 0x10 + + metadata.get_thing = get_thing #avoid circular dependency and redundant code + Metadata = metadata.Metadata #use with cbox.JackIO.Metadata.get_all_properties() + + @staticmethod + def status(): + # Some of these only make sense for JACK + return GetThings("/io/status", ['client_type', 'client_name', + 'audio_inputs', 'audio_outputs', 'buffer_size', '*midi_output', + '*midi_input', 'sample_rate', 'output_resolution', + '*usb_midi_input', '*usb_midi_output', '?external_tempo'], []) + @staticmethod + def jack_transport_position(): + # Some of these only make sense for JACK + return GetThings("/io/jack_transport_position", ['state', 'unique_lo', + 'unique_hi', 'usecs_lo', 'usecs_hi', 'frame_rate', 'frame', 'bar', + 'beat', 'tick', 'bar_start_tick', 'bbt_frame_offset', 'beats_per_bar', + 'beat_type', 'ticks_per_beat', 'beats_per_minute', 'is_master'], []) + @staticmethod + def jack_transport_locate(pos): + do_cmd("/io/jack_transport_locate", None, [pos]) + @staticmethod + def transport_mode(master = True, conditional = False): + if master: + do_cmd("/io/transport_mode", None, [1 if conditional else 2]) + else: + do_cmd("/io/transport_mode", None, [0]) + @staticmethod + def create_midi_input(name, autoconnect_spec = None): + uuid = GetUUID("/io/create_midi_input", name).uuid + if autoconnect_spec is not None and autoconnect_spec != '': + JackIO.autoconnect(uuid, autoconnect_spec) + return uuid + @staticmethod + def create_midi_output(name, autoconnect_spec = None): + uuid = GetUUID("/io/create_midi_output", name).uuid + if autoconnect_spec is not None and autoconnect_spec != '': + JackIO.autoconnect(uuid, autoconnect_spec) + return uuid + @staticmethod + def autoconnect(uuid, autoconnect_spec = None): + if autoconnect_spec is not None: + do_cmd("/io/autoconnect", None, [uuid, autoconnect_spec]) + else: + do_cmd("/io/autoconnect", None, [uuid, '']) + autoconnect_midi_input = autoconnect + autoconnect_midi_output = autoconnect + autoconnect_audio_output = autoconnect + @staticmethod + def rename_midi_output(uuid, new_name): + do_cmd("/io/rename_midi_port", None, [uuid, new_name]) + rename_midi_input = rename_midi_output + @staticmethod + def disconnect_midi_port(uuid): + do_cmd("/io/disconnect_midi_port", None, [uuid]) + @staticmethod + def disconnect_midi_output(uuid): + do_cmd("/io/disconnect_midi_output", None, [uuid]) + @staticmethod + def disconnect_midi_input(uuid): + do_cmd("/io/disconnect_midi_input", None, [uuid]) + @staticmethod + def delete_midi_input(uuid): + do_cmd("/io/delete_midi_input", None, [uuid]) + @staticmethod + def delete_midi_output(uuid): + do_cmd("/io/delete_midi_output", None, [uuid]) + @staticmethod + def route_midi_input(input_uuid, scene_uuid): + do_cmd("/io/route_midi_input", None, [input_uuid, scene_uuid]) + @staticmethod + def set_appsink_for_midi_input(input_uuid, enabled): + do_cmd("/io/set_appsink_for_midi_input", None, [input_uuid, 1 if enabled else 0]) + @staticmethod + def get_new_events(input_uuid): + seq = [] + do_cmd("/io/get_new_events", (lambda cmd, fb, args: seq.append((cmd, fb, args))), [input_uuid]) + return seq + @staticmethod + def create_audio_output(name, autoconnect_spec = None): + uuid = GetUUID("/io/create_audio_output", name).uuid + if autoconnect_spec is not None and autoconnect_spec != '': + JackIO.autoconnect(uuid, autoconnect_spec) + return uuid + @staticmethod + def create_audio_output_router(uuid_left, uuid_right): + return get_thing("/io/create_audio_output_router", "/uuid", DocRecorder, uuid_left, uuid_right) + @staticmethod + def delete_audio_output(uuid): + do_cmd("/io/delete_audio_output", None, [uuid]) + @staticmethod + def rename_audio_output(uuid, new_name): + do_cmd("/io/rename_audio_port", None, [uuid, new_name]) + @staticmethod + def disconnect_audio_output(uuid): + do_cmd("/io/disconnect_audio_output", None, [uuid]) + @staticmethod + def port_connect(pfrom, pto): + do_cmd("/io/port_connect", None, [pfrom, pto]) + @staticmethod + def port_disconnect(pfrom, pto): + do_cmd("/io/port_disconnect", None, [pfrom, pto]) + @staticmethod + def get_ports(name_mask = ".*", type_mask = ".*", flag_mask = 0): + return get_thing("/io/get_ports", '/port', [str], name_mask, type_mask, int(flag_mask)) + @staticmethod + def get_connected_ports(port): + return get_thing("/io/get_connected_ports", '/port', [str], port) + @staticmethod + def external_tempo(enable): + """Enable reacting to JACK transport tempo""" + do_cmd('/io/external_tempo', None, [1 if enable else 0]) + +def call_on_idle(callback = None): + do_cmd("/on_idle", callback, []) + +def get_new_events(): + seq = [] + do_cmd("/on_idle", (lambda cmd, fb, args: seq.append((cmd, fb, args))), []) + return seq + +def send_midi_event(*data, **kwargs): + output = kwargs.get('output', None) + do_cmd('/send_event_to', None, [output if output is not None else ''] + list(data)) + +def send_sysex(data, output = None): + do_cmd('/send_sysex_to', None, [output if output is not None else '', bytearray(data)]) + +def flush_rt(): + do_cmd('/rt/flush', None, []) + +class CfgSection: + def __init__(self, name): + self.name = name + + def __getitem__(self, key): + return Config.get(self.name, key) + + def __setitem__(self, key, value): + Config.set(self.name, key, value) + + def __delitem__(self, key): + Config.delete(self.name, key) + + def keys(self, prefix = ""): + return Config.keys(self.name, prefix) + + +class Pattern: + @staticmethod + def get_pattern(): + pat_data = get_thing("/get_pattern", '/pattern', (bytes, int)) + if pat_data is not None: + pat_blob, length = pat_data + pat_data = [] + ofs = 0 + while ofs < len(pat_blob): + data = list(struct.unpack_from("iBBbb", pat_blob, ofs)) + data[1:2] = [] + pat_data.append(tuple(data)) + ofs += 8 + return pat_data, length + return None + + @staticmethod + def serialize_event(time, *data): + if len(data) >= 1 and len(data) <= 3: + return struct.pack("iBBbb"[0:2 + len(data)], int(time), len(data), *[int(v) for v in data]) + raise ValueError("Invalid length of an event (%d)" % len(data)) + +class Document: + """Document singleton.""" + classmap = {} + objmap = {} + @staticmethod + def dump(): + """Print all objects in the documents to stdout. Only used for debugging.""" + do_cmd("/doc/dump", None, []) + @staticmethod + def uuid_cmd(uuid, cmd): + """Internal: execute a given request on an object with specific UUID.""" + return "/doc/uuid/%s%s" % (uuid, cmd) + @staticmethod + def get_uuid(path): + """Internal: retrieve an UUID of an object that has specified path.""" + return GetUUID('%s/get_uuid' % path).uuid + @staticmethod + def map_path(path, *args): + """Internal: return an object corresponding to a path""" + return Document.map_uuid(Document.get_uuid(path)) + @staticmethod + def cmd_makeobj(cmd, *args): + """Internal: create an object from the UUID result of a command""" + return Document.map_uuid(GetUUID(cmd, *args).uuid) + @staticmethod + def get_obj_class(uuid): + """Internal: retrieve an internal class type of an object that has specified path.""" + return get_thing(Document.uuid_cmd(uuid, "/get_class_name"), '/class_name', str) + @staticmethod + def get_song(): + """Retrieve the current song object of a given document. Each document can + only have one current song.""" + return Document.map_path("/song") + @staticmethod + def get_scene(): + """Retrieve the first scene object of a default engine. This function + is considered obsolete-ish, because of multiple scene support.""" + return Document.map_path("/scene") + @staticmethod + def get_engine(): + """Retrieve the current RT engine object of a given document. Each document can + only have one current RT engine.""" + return Document.map_path("/rt/engine") + @staticmethod + def get_rt(): + """Retrieve the RT singleton. RT is an object used to communicate between + realtime and user thread, and is currently also used to access the audio + engine.""" + return Document.map_path("/rt") + @staticmethod + def new_engine(srate, bufsize): + """Create a new off-line engine object. This new engine object cannot be used for + audio playback - that's only allowed for default engine.""" + return Document.cmd_makeobj('/new_engine', int(srate), int(bufsize)) + @staticmethod + def map_uuid(uuid): + """Create or retrieve a Python-side accessor proxy for a C-side object.""" + if uuid is None: + return None + if uuid in Document.objmap: + return Document.objmap[uuid] + try: + oclass = Document.get_obj_class(uuid) + except Exception as e: + print ("Note: Cannot get class for " + uuid) + Document.dump() + raise + o = Document.classmap[oclass](uuid) + Document.objmap[uuid] = o + if hasattr(o, 'init_object'): + o.init_object() + return o + @staticmethod + def map_uuid_and_check(uuid, t): + o = Document.map_uuid(uuid) + if not isinstance(o, t): + raise TypeError("UUID %s is of type %s, expected %s" % (uuid, o.__class__, t)) + return o + +class DocPattern(DocObj): + class Status: + event_count = int + loop_end = int + name = str + def __init__(self, uuid): + DocObj.__init__(self, uuid) + def set_name(self, name): + self.cmd("/name", None, name) +Document.classmap['cbox_midi_pattern'] = DocPattern + +class ClipItem: + def __init__(self, pos, offset, length, pattern, clip): + self.pos = pos + self.offset = offset + self.length = length + self.pattern = Document.map_uuid(pattern) + self.clip = Document.map_uuid(clip) + def __str__(self): + return "pos=%d offset=%d length=%d pattern=%s clip=%s" % (self.pos, self.offset, self.length, self.pattern.uuid, self.clip.uuid) + def __eq__(self, other): + return str(self) == str(other) + +class DocTrackClip(DocObj): + class Status: + pos = SettableProperty(int) + offset = SettableProperty(int) + length = SettableProperty(int) + pattern = SettableProperty(DocPattern) + def __init__(self, uuid): + DocObj.__init__(self, uuid) +Document.classmap['cbox_track_item'] = DocTrackClip + +class DocTrack(DocObj): + class Status: + clips = [ClipItem] + name = SettableProperty(str) + external_output = SettableProperty(str) + mute = SettableProperty(int) + def add_clip(self, pos, offset, length, pattern): + return self.cmd_makeobj("/add_clip", int(pos), int(offset), int(length), pattern.uuid) + def clear_clips(self): + return self.cmd_makeobj("/clear_clips") +Document.classmap['cbox_track'] = DocTrack + +class TrackItem: + def __init__(self, name, count, track): + self.name = name + self.count = count + self.track = Document.map_uuid(track) + +class PatternItem: + def __init__(self, name, length, pattern): + self.name = name + self.length = length + self.pattern = Document.map_uuid(pattern) + +class MtiItem: + def __init__(self, pos, tempo, timesig_num, timesig_denom): + self.pos = pos + self.tempo = tempo + # Original misspelling + self.timesig_num = timesig_num + self.timesig_denom = timesig_denom + def __getattr__(self, name): + if name == 'timesig_nom': + return self.timesig_num + raise AttributeError(name) + def __setattr__(self, name, value): + if name == 'timesig_nom': + self.timesig_num = value + else: + self.__dict__[name] = value + def __eq__(self, o): + return self.pos == o.pos and self.tempo == o.tempo and self.timesig_num == o.timesig_num and self.timesig_denom == o.timesig_denom + def __repr__(self): + return ("pos: {}, bpm: {}, timesig: {}/{}".format(self.pos, self.tempo, self.timesig_num, self.timesig_denom)) + +class DocSongStatus: + tracks = None + patterns = None + +class DocSong(DocObj): + class Status: + tracks = [TrackItem] + patterns = [PatternItem] + mtis = [MtiItem] + loop_start = int + loop_end = int + def clear(self): + return self.cmd("/clear", None) + def set_loop(self, ls, le): + return self.cmd("/set_loop", None, int(ls), int(le)) + def set_mti(self, pos, tempo = None, timesig_num = None, timesig_denom = None, timesig_nom = None): + if timesig_nom is not None: + timesig_num = timesig_nom + self.cmd("/set_mti", None, int(pos), float(tempo) if tempo is not None else -1.0, int(timesig_num) if timesig_num is not None else -1, int(timesig_denom) if timesig_denom else -1) + def delete_mti(self, pos): + """Deleting works only if we set everything to exactly 0. Not None, not -1""" + self.set_mti(pos, tempo = 0, timesig_num = 0, timesig_denom = 0, timesig_nom = 0) + def add_track(self): + return self.cmd_makeobj("/add_track") + def load_drum_pattern(self, name): + return self.cmd_makeobj("/load_pattern", name, 1) + def load_drum_track(self, name): + return self.cmd_makeobj("/load_track", name, 1) + def pattern_from_blob(self, blob, length): + return self.cmd_makeobj("/load_blob", bytearray(blob), int(length)) + def loop_single_pattern(self, loader): + self.clear() + track = self.add_track() + pat = loader() + length = pat.status().loop_end + track.add_clip(0, 0, length, pat) + self.set_loop(0, length) + self.update_playback() + def update_playback(self): + # XXXKF Maybe make it a song-level API instead of global + do_cmd("/update_playback", None, []) +Document.classmap['cbox_song'] = DocSong + +class UnknownModule(NonDocObj): + class Status: + pass + +class DocRecorder(DocObj): + class Status: + filename = str + gain = SettableProperty(float) +Document.classmap['cbox_recorder'] = DocRecorder + +class RecSource(NonDocObj): + class Status: + handler = [DocRecorder] + def attach(self, recorder): + self.cmd('/attach', None, recorder.uuid) + def detach(self, recorder): + self.cmd('/detach', None, recorder.uuid) + +class EffectSlot(NonDocObj): + class Status: + insert_preset = SettableProperty(str) + insert_engine = SettableProperty(str) + bypass = SettableProperty(bool) + def init_object(self): + # XXXKF add wrapper classes for effect engines + self.engine = UnknownModule(self.path + "/engine") + +class InstrumentOutput(EffectSlot): + class Status(EffectSlot.Status): + gain_linear = float + gain = float + output = SettableProperty(int) + def init_object(self): + EffectSlot.init_object(self) + self.rec_dry = RecSource(self.make_path('/rec_dry')) + self.rec_wet = RecSource(self.make_path('/rec_wet')) + +class DocInstrument(DocObj): + class Status: + name = str + outputs = int + aux_offset = int + engine = str + def init_object(self): + s = self.status() + engine = s.engine + if engine in engine_classes: + self.engine = engine_classes[engine]("/doc/uuid/" + self.uuid + "/engine") + else: + raise ValueError("Unknown engine %s" % engine) + self.output_slots = [] + for i in range(s.outputs): + io = InstrumentOutput(self.make_path('/output/%d' % (i + 1))) + io.init_object() + self.output_slots.append(io) + def move_to(self, target_scene, pos = 0): + return self.cmd_makeobj("/move_to", target_scene.uuid, pos + 1) + def get_output_slot(self, slot): + return self.output_slots[slot] +Document.classmap['cbox_instrument'] = DocInstrument + +class DocLayer(DocObj): + class Status: + name = str + instrument_name = str + instrument = AltPropName('/instrument_uuid', DocInstrument) + enable = SettableProperty(bool) + low_note = SettableProperty(int) + high_note = SettableProperty(int) + fixed_note = SettableProperty(int) + in_channel = SettableProperty(int) + out_channel = SettableProperty(int) + disable_aftertouch = SettableProperty(bool) + invert_sustain = SettableProperty(bool) + consume = SettableProperty(bool) + ignore_scene_transpose = SettableProperty(bool) + ignore_program_changes = SettableProperty(bool) + transpose = SettableProperty(int) + external_output = SettableProperty(str) + def get_instrument(self): + return self.status().instrument +Document.classmap['cbox_layer'] = DocLayer + +class SamplerEngine(NonDocObj): + class Status(object): + """Maximum number of voices playing at the same time.""" + polyphony = int + """Current number of voices playing.""" + active_voices = int + """Current number of delayed-startup voices waiting to be played.""" + active_prevoices = int + """Current number of disk streams.""" + active_pipes = int + """GM volume (14-bit) per MIDI channel.""" + volume = {int:int} + """GM pan (14-bit) per MIDI channel.""" + pan = {int:int} + """Output offset per MIDI channel.""" + output = {int:int} + """Current number of voices playing per MIDI channel.""" + channel_voices = AltPropName('/channel_voices', {int:int}) + """Current number of voices waiting to be played per MIDI channel.""" + channel_prevoices = AltPropName('/channel_prevoices', {int:int}) + """MIDI channel -> (program number, program name)""" + patches = {int:(int, str)} + + def load_patch_from_cfg(self, patch_no, cfg_section, display_name): + """Load a sampler program from an 'spgm:' config section.""" + return self.cmd_makeobj("/load_patch", int(patch_no), cfg_section, display_name) + + def load_patch_from_string(self, patch_no, sample_dir, sfz_data, display_name): + """Load a sampler program from a string, using given filesystem path for sample directory.""" + return self.cmd_makeobj("/load_patch_from_string", int(patch_no), sample_dir, sfz_data, display_name) + + def load_patch_from_file(self, patch_no, sfz_name, display_name): + """Load a sampler program from a filesystem file.""" + return self.cmd_makeobj("/load_patch_from_file", int(patch_no), sfz_name, display_name) + + def load_patch_from_tar(self, patch_no, tar_name, sfz_name, display_name): + """Load a sampler program from a tar file.""" + return self.cmd_makeobj("/load_patch_from_file", int(patch_no), "sbtar:%s;%s" % (tar_name, sfz_name), display_name) + + def set_patch(self, channel, patch_no): + """Select patch identified by patch_no in a specified MIDI channel.""" + self.cmd("/set_patch", None, int(channel), int(patch_no)) + def set_output(self, channel, output): + """Set output offset value in a specified MIDI channel.""" + self.cmd("/set_output", None, int(channel), int(output)) + def get_unused_program(self): + """Returns first program number that has no program associated with it.""" + return self.get_thing("/get_unused_program", '/program_no', int) + def set_polyphony(self, polyphony): + """Set a maximum number of voices that can be played at a given time.""" + self.cmd("/polyphony", None, int(polyphony)) + def get_patches(self): + """Return a map of program identifiers to program objects.""" + return self.get_thing("/patches", '/patch', {int : (str, SamplerProgram, int)}) + +class FluidsynthEngine(NonDocObj): + class Status: + polyphony = int + soundfont = str + patch = {int: (int, str)} + def load_soundfont(self, filename): + return self.cmd_makeobj("/load_soundfont", filename) + def set_patch(self, channel, patch_no): + self.cmd("/set_patch", None, int(channel), int(patch_no)) + def set_polyphony(self, polyphony): + self.cmd("/polyphony", None, int(polyphony)) + def get_patches(self): + return self.get_thing("/patches", '/patch', {int: str}) + + +class StreamPlayerEngine(NonDocObj): + class Status: + filename = str + pos = int + length = int + playing = int + def play(self): + self.cmd('/play') + def stop(self): + self.cmd('/stop') + def seek(self, place): + self.cmd('/seek', None, int(place)) + def load(self, filename, loop_start = -1): + self.cmd('/load', None, filename, int(loop_start)) + def unload(self): + self.cmd('/unload') + +class TonewheelOrganEngine(NonDocObj): + class Status: + upper_drawbar = SettableProperty({int: int}) + lower_drawbar = SettableProperty({int: int}) + pedal_drawbar = SettableProperty({int: int}) + upper_vibrato = SettableProperty(bool) + lower_vibrato = SettableProperty(bool) + vibrato_mode = SettableProperty(int) + vibrato_chorus = SettableProperty(int) + percussion_enable = SettableProperty(bool) + percussion_3rd = SettableProperty(bool) + +class JackInputEngine(NonDocObj): + class Status: + inputs = (int, int) + +engine_classes = { + 'sampler' : SamplerEngine, + 'fluidsynth' : FluidsynthEngine, + 'stream_player' : StreamPlayerEngine, + 'tonewheel_organ' : TonewheelOrganEngine, + 'jack_input' : JackInputEngine, +} + +class DocAuxBus(DocObj): + class Status: + name = str + def init_object(self): + self.slot = EffectSlot("/doc/uuid/" + self.uuid + "/slot") + self.slot.init_object() + +Document.classmap['cbox_aux_bus'] = DocAuxBus + +class DocScene(DocObj): + class Status: + name = str + title = str + transpose = int + layers = [DocLayer] + instruments = {str: (str, DocInstrument)} + auxes = {str: DocAuxBus} + enable_default_song_input = SettableProperty(bool) + enable_default_external_input = SettableProperty(bool) + def clear(self): + self.cmd("/clear", None) + def load(self, name): + self.cmd("/load", None, name) + def load_aux(self, aux): + return self.cmd_makeobj("/load_aux", aux) + def delete_aux(self, aux): + return self.cmd("/delete_aux", None, aux) + def delete_layer(self, pos): + self.cmd("/delete_layer", None, int(1 + pos)) + def move_layer(self, old_pos, new_pos): + self.cmd("/move_layer", None, int(old_pos + 1), int(new_pos + 1)) + + def add_layer(self, aux, pos = None): + if pos is None: + return self.cmd_makeobj("/add_layer", 0, aux) + else: + # Note: The positions in high-level API are zero-based. + return self.cmd_makeobj("/add_layer", int(1 + pos), aux) + def add_instrument_layer(self, name, pos = None): + if pos is None: + return self.cmd_makeobj("/add_instrument_layer", 0, name) + else: + return self.cmd_makeobj("/add_instrument_layer", int(1 + pos), name) + def add_new_instrument_layer(self, name, engine, pos = None): + if pos is None: + return self.cmd_makeobj("/add_new_instrument_layer", 0, name, engine) + else: + return self.cmd_makeobj("/add_new_instrument_layer", int(1 + pos), name, engine) + def add_new_midi_layer(self, ext_output_uuid, pos = None): + if pos is None: + return self.cmd_makeobj("/add_midi_layer", 0, ext_output_uuid) + else: + return self.cmd_makeobj("/add_midi_layer", int(1 + pos), ext_output_uuid) + def send_midi_event(self, *data): + self.cmd('/send_event', None, *data) + def play_pattern(self, pattern, tempo, id = 0): + self.cmd('/play_pattern', None, pattern.uuid, float(tempo), int(id)) +Document.classmap['cbox_scene'] = DocScene + +class DocRt(DocObj): + class Status: + audio_channels = (int, int) + state = (int, str) +Document.classmap['cbox_rt'] = DocRt + +class DocModule(DocObj): + class Status: + pass +Document.classmap['cbox_module'] = DocModule + +class DocEngine(DocObj): + class Status: + scenes = AltPropName('/scene', [DocScene]) + def init_object(self): + self.master_effect = EffectSlot(self.path + "/master_effect") + self.master_effect.init_object() + def new_scene(self): + return self.cmd_makeobj('/new_scene') + def new_recorder(self, filename): + return self.cmd_makeobj("/new_recorder", filename) + def render_stereo(self, samples): + return self.get_thing("/render_stereo", '/data', bytes, samples) +Document.classmap['cbox_engine'] = DocEngine + +class SamplerProgram(DocObj): + class Status: + name = str + sample_dir = str + source_file = str + program_no = int + in_use = int + def get_regions(self): + return self.get_thing("/regions", '/region', [SamplerLayer]) + def get_global(self): + return self.cmd_makeobj("/global") + def get_control_inits(self): + return self.get_thing("/control_inits", '/control_init', [(int, int)]) + def new_group(self): + # Obsolete + return self.cmd_makeobj("/new_group") + def add_control_init(self, controller, value): + return self.cmd("/add_control_init", None, controller, value) + def add_control_label(self, controller, label): + return self.cmd("/add_control_label", None, controller, label) + # which = -1 -> remove all controllers with that number from the list + def delete_control_init(self, controller, which = 0): + return self.cmd("/delete_control_init", None, controller, which) + def load_file(self, filename, max_size = -1): + """Return an in-memory file corresponding to a given file inside sfbank. + This can be used for things like scripts, images, descriptions etc.""" + data = self.get_thing("/load_file", '/data', bytes, filename, max_size) + if data is None: + return data + return BytesIO(data) + def clone_to(self, dest_module, prog_index): + return self.cmd_makeobj('/clone_to', dest_module.uuid, int(prog_index)) +Document.classmap['sampler_program'] = SamplerProgram + +class SamplerLayer(DocObj): + class Status: + parent_program = SamplerProgram + parent_group = DocObj + def get_children(self): + return self.get_thing("/get_children", '/child', [SamplerLayer]) + def as_string(self): + return self.get_thing("/as_string", '/value', str) + def as_string_full(self): + return self.get_thing("/as_string_full", '/value', str) + def set_param(self, key, value): + self.cmd("/set_param", None, key, str(value)) + def unset_param(self, key): + self.cmd("/unset_param", None, key) + def new_child(self): + return self.cmd_makeobj("/new_child") +Document.classmap['sampler_layer'] = SamplerLayer + diff --git a/template/calfbox/py/drum_pattern_editor.py b/template/calfbox/py/drum_pattern_editor.py new file mode 100644 index 0000000..fc32e77 --- /dev/null +++ b/template/calfbox/py/drum_pattern_editor.py @@ -0,0 +1,603 @@ +#from gui_tools import * +from gi.repository import GObject, Gdk, Gtk, GooCanvas, GLib +import gui_tools + +def guint(x): + value = GObject.Value() + value.init(GObject.TYPE_UINT) + value.set_uint(x) + return value + +PPQN = 48 + +def standard_filter(patterns, name): + f = Gtk.FileFilter() + for p in patterns: + f.add_pattern(p) + f.set_name(name) + return f + +def hide_item(citem): + citem.visibility = GooCanvas.CanvasItemVisibility.HIDDEN + +def show_item(citem): + citem.visibility = GooCanvas.CanvasItemVisibility.VISIBLE + +def polygon_to_path(points): + assert len(points) % 2 == 0, "Invalid number of points (%d) in %s" % (len(points), repr(points)) + path = "" + if len(points) > 0: + path += "M %s %s" % (points[0], points[1]) + if len(points) > 1: + for i in range(2, len(points), 2): + path += " L %s %s" % (points[i], points[i + 1]) + return path + +class NoteModel(object): + def __init__(self, pos, channel, row, vel, len = 1): + self.pos = int(pos) + self.channel = int(channel) + self.row = int(row) + self.vel = int(vel) + self.len = int(len) + self.item = None + self.selected = False + def __str__(self): + return "pos=%s row=%s vel=%s len=%s" % (self.pos, self.row, self.vel, self.len) + +# This is stupid and needs rewriting using a faster data structure +class DrumPatternModel(GObject.GObject): + def __init__(self, beats, bars): + GObject.GObject.__init__(self) + self.ppqn = PPQN + self.beats = beats + self.bars = bars + self.notes = [] + + def import_data(self, data): + self.clear() + active_notes = {} + for t in data: + cmd = t[1] & 0xF0 + if len(t) == 4 and (cmd == 0x90) and (t[3] > 0): + note = NoteModel(t[0], (t[1] & 15) + 1, t[2], t[3]) + active_notes[t[2]] = note + self.add_note(note) + if len(t) == 4 and ((cmd == 0x90 and t[3] == 0) or cmd == 0x80): + if t[2] in active_notes: + active_notes[t[2]].len = t[0] - active_notes[t[2]].pos + del active_notes[t[2]] + end = self.get_length() + for n in active_notes.values(): + n.len = end - n.pos + + def clear(self): + self.notes = [] + self.changed() + + def add_note(self, note, send_changed = True): + self.notes.append(note) + if send_changed: + self.changed() + + def remove_note(self, pos, row, channel): + self.notes = [note for note in self.notes if note.pos != pos or note.row != row or (channel is not None and note.channel != channel)] + self.changed() + + def set_note_vel(self, note, vel): + note.vel = int(vel) + self.changed() + + def set_note_len(self, note, len): + note.len = int(len) + self.changed() + + def has_note(self, pos, row, channel): + for n in self.notes: + if n.pos == pos and n.row == row and (channel is None or n.channel == channel): + return True + return False + + def get_note(self, pos, row, channel): + for n in self.notes: + if n.pos == pos and n.row == row and (channel is None or n.channel == channel): + return n + return None + + def items(self): + return self.notes + + def get_length(self): + return int(self.beats * self.bars * self.ppqn) + + def changed(self): + self.emit('changed') + + def delete_selected(self): + self.notes = [note for note in self.notes if not note.selected] + self.changed() + + def group_set(self, **vals): + for n in self.notes: + if not n.selected: + continue + for k, v in vals.items(): + setattr(n, k, v) + self.changed() + + def transpose_selected(self, amount): + for n in self.notes: + if not n.selected: + continue + if n.row + amount < 0 or n.row + amount > 127: + continue + n.row += amount + self.changed() + +GObject.type_register(DrumPatternModel) +GObject.signal_new("changed", DrumPatternModel, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) + +channel_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT) +for ch in range(1, 17): + channel_ls.append((str(ch), ch)) +snap_settings_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT) +for row in [("1/4", PPQN), ("1/8", PPQN // 2), ("1/8T", PPQN//3), ("1/16", PPQN//4), ("1/16T", PPQN//6), ("1/32", PPQN//8), ("1/32T", PPQN//12), ("1/64", PPQN//4)]: + snap_settings_ls.append(row) +edit_mode_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING) +for row in [("Drum", "D"), ("Melodic", "M")]: + edit_mode_ls.append(row) + +class DrumEditorToolbox(Gtk.HBox): + def __init__(self, canvas): + Gtk.HBox.__init__(self, spacing = 5) + self.canvas = canvas + self.vel_adj = Gtk.Adjustment(100, 1, 127, 1, 10, 0) + + self.pack_start(Gtk.Label("Channel:"), False, False, 0) + self.channel_setting = gui_tools.standard_combo(channel_ls, active_item_lookup = self.canvas.channel, lookup_column = 1) + self.channel_setting.connect('changed', lambda w: self.canvas.set_channel(w.get_model()[w.get_active()][1])) + self.pack_start(self.channel_setting, False, True, 0) + + self.pack_start(Gtk.Label("Mode:"), False, False, 0) + self.mode_setting = gui_tools.standard_combo(edit_mode_ls, active_item_lookup = self.canvas.edit_mode, lookup_column = 1) + self.mode_setting.connect('changed', lambda w: self.canvas.set_edit_mode(w.get_model()[w.get_active()][1])) + self.pack_start(self.mode_setting, False, True, 0) + + self.pack_start(Gtk.Label("Snap:"), False, False, 0) + self.snap_setting = gui_tools.standard_combo(snap_settings_ls, active_item_lookup = self.canvas.grid_unit, lookup_column = 1) + self.snap_setting.connect('changed', lambda w: self.canvas.set_grid_unit(w.get_model()[w.get_active()][1])) + self.pack_start(self.snap_setting, False, True, 0) + + self.pack_start(Gtk.Label("Velocity:"), False, False, 0) + self.pack_start(Gtk.SpinButton(adjustment = self.vel_adj, climb_rate = 0, digits = 0), False, False, 0) + button = Gtk.Button("Load") + button.connect('clicked', self.load_pattern) + self.pack_start(button, True, True, 0) + button = Gtk.Button("Save") + button.connect('clicked', self.save_pattern) + self.pack_start(button, True, True, 0) + button = Gtk.Button("Double") + button.connect('clicked', self.double_pattern) + self.pack_start(button, True, True, 0) + self.pack_start(Gtk.Label("--"), False, False, 0) + + def update_edit_mode(self): + self.mode_setting.set_active(gui_tools.ls_index(edit_mode_ls, self.canvas.edit_mode, 1)) + + def load_pattern(self, w): + dlg = Gtk.FileChooserDialog('Open a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.OPEN, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY)) + dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns")) + dlg.add_filter(standard_filter(["*"], "All files")) + try: + if dlg.run() == Gtk.ResponseType.APPLY: + pattern = self.canvas.pattern + f = file(dlg.get_filename(), "r") + pattern.clear() + pattern.beats, pattern.bars = [int(v) for v in f.readline().strip().split(";")] + for line in f.readlines(): + line = line.strip() + if not line.startswith("n:"): + pos, row, vel = line.split(";") + row = int(row) + 36 + channel = 10 + len = 1 + else: + pos, channel, row, vel, len = line[2:].split(";") + self.canvas.pattern.add_note(NoteModel(pos, channel, row, vel, len), send_changed = False) + f.close() + self.canvas.pattern.changed() + self.canvas.update_grid() + self.canvas.update_notes() + finally: + dlg.destroy() + def save_pattern(self, w): + dlg = Gtk.FileChooserDialog('Save a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY)) + dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns")) + dlg.add_filter(standard_filter(["*"], "All files")) + dlg.set_current_name("pattern.cbdp") + try: + if dlg.run() == Gtk.ResponseType.APPLY: + pattern = self.canvas.pattern + f = file(dlg.get_filename(), "w") + f.write("%s;%s\n" % (pattern.beats, pattern.bars)) + for i in self.canvas.pattern.items(): + f.write("n:%s;%s;%s;%s;%s\n" % (i.pos, i.channel, i.row, i.vel, i.len)) + f.close() + finally: + dlg.destroy() + def double_pattern(self, w): + len = self.canvas.pattern.get_length() + self.canvas.pattern.bars *= 2 + new_notes = [] + for note in self.canvas.pattern.items(): + new_notes.append(NoteModel(note.pos + len, note.channel, note.row, note.vel, note.len)) + for note in new_notes: + self.canvas.pattern.add_note(note, send_changed = False) + self.canvas.pattern.changed() + self.canvas.update_size() + self.canvas.update_grid() + self.canvas.update_notes() + +class DrumCanvasCursor(object): + def __init__(self, canvas): + self.canvas = canvas + self.canvas_root = canvas.get_root_item() + self.frame = GooCanvas.CanvasRect(parent = self.canvas_root, x = -6, y = -6, width = 12, height = 12 , stroke_color = "gray", line_width = 1) + hide_item(self.frame) + self.vel = GooCanvas.CanvasText(parent = self.canvas_root, x = 0, y = 0, fill_color = "blue", stroke_color = "blue", anchor = GooCanvas.CanvasAnchorType.S) + hide_item(self.vel) + self.rubberband = False + self.rubberband_origin = None + self.rubberband_current = None + + def hide(self): + hide_item(self.frame) + hide_item(self.vel) + + def show(self): + show_item(self.frame) + show_item(self.vel) + + def move_to_note(self, note): + self.move(note.pos, note.row, note) + + def move(self, pulse, row, note): + x = self.canvas.pulse_to_screen_x(pulse) + y = self.canvas.row_to_screen_y(row) + self.canvas.row_height / 2 + dx = 0 + if note is not None: + dx = self.canvas.pulse_to_screen_x(pulse + note.len) - x + self.frame.set_properties(x = x - 6, width = 12 + dx, y = y - 6, height = 12) + cy = y - self.canvas.row_height * 1.5 if y >= self.canvas.rows * self.canvas.row_height / 2 else y + self.canvas.row_height * 1.5 + if note is None: + text = "" + else: + text = "[%s] %s" % (note.channel, note.vel) + self.vel.set_properties(x = x, y = cy, text = text) + + def start_rubberband(self, x, y): + self.rubberband = True + self.rubberband_origin = (x, y) + self.rubberband_current = (x, y) + self.update_rubberband_frame() + show_item(self.frame) + + def update_rubberband(self, x, y): + self.rubberband_current = (x, y) + self.update_rubberband_frame() + + def end_rubberband(self, x, y): + self.rubberband_current = (x, y) + self.update_rubberband_frame() + hide_item(self.frame) + self.rubberband = False + + def cancel_rubberband(self): + hide_item(self.frame) + self.rubberband = False + + def update_rubberband_frame(self): + self.frame.set_properties(x = self.rubberband_origin[0], + y = self.rubberband_origin[1], + width = self.rubberband_current[0] - self.rubberband_origin[0], + height = self.rubberband_current[1] - self.rubberband_origin[1]) + + def get_rubberband_box(self): + x1, y1 = self.rubberband_origin + x2, y2 = self.rubberband_current + return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)) + +class DrumCanvas(GooCanvas.Canvas): + def __init__(self, rows, pattern): + GooCanvas.Canvas.__init__(self) + self.rows = rows + self.pattern = pattern + self.row_height = 24 + self.grid_unit = PPQN / 4 # unit in pulses + self.zoom_in = 2 + self.instr_width = 120 + self.edited_note = None + self.orig_velocity = None + self.orig_length = None + self.orig_y = None + self.channel_modes = ['D' if ch == 10 else 'M' for ch in range(1, 17)] + + self.update_size() + + self.grid = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width) + self.update_grid() + + self.notes = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width) + + self.names = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = 0) + self.update_names() + + self.cursor = DrumCanvasCursor(self) + hide_item(self.cursor) + + self.connect('event', self.on_grid_event) + + self.channel = 10 + self.edit_mode = self.channel_modes[self.channel - 1] + self.toolbox = DrumEditorToolbox(self) + + self.add_events(Gdk.EventMask.POINTER_MOTION_HINT_MASK) + + self.grab_focus(self.grid) + self.update_notes() + + def set_edit_mode(self, mode): + self.edit_mode = mode + self.channel_modes[self.channel - 1] = mode + + def calc_size(self): + return (self.instr_width + self.pattern.get_length() * self.zoom_in + 1, self.rows * self.row_height + 1) + + def set_grid_unit(self, grid_unit): + self.grid_unit = grid_unit + self.update_grid() + + def set_channel(self, channel): + self.channel = channel + self.set_edit_mode(self.channel_modes[self.channel - 1]) + self.update_notes() + self.toolbox.update_edit_mode() + + def update_size(self): + sx, sy = self.calc_size() + self.set_bounds(0, 0, sx, self.rows * self.row_height) + self.set_size_request(sx, sy) + + def update_names(self): + for i in self.names.items: + i.destroy() + for i in range(0, self.rows): + #GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = Gtk.AnchorType.E, size_points = 10, font = "Sans", size_set = True) + GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = GooCanvas.CanvasAnchorType.E, font = "Sans") + + def update_grid(self): + for i in self.grid.items: + i.destroy() + bg = GooCanvas.CanvasRect(parent = self.grid, x = 0, y = 0, width = self.pattern.get_length() * self.zoom_in, height = self.rows * self.row_height, fill_color = "white") + bar_fg = "blue" + beat_fg = "darkgray" + grid_fg = "lightgray" + row_grid_fg = "lightgray" + row_ext_fg = "black" + for i in range(0, self.rows + 1): + color = row_ext_fg if (i == 0 or i == self.rows) else row_grid_fg + GooCanvas.CanvasPath(parent = self.grid, data = "M %s %s L %s %s" % (0, i * self.row_height, self.pattern.get_length() * self.zoom_in, i * self.row_height), stroke_color = color, line_width = 1) + for i in range(0, self.pattern.get_length() + 1, int(self.grid_unit)): + color = grid_fg + if i % self.pattern.ppqn == 0: + color = beat_fg + if (i % (self.pattern.ppqn * self.pattern.beats)) == 0: + color = bar_fg + GooCanvas.CanvasPath(parent = self.grid, data = "M %s %s L %s %s" % (i * self.zoom_in, 1, i * self.zoom_in, self.rows * self.row_height - 1), stroke_color = color, line_width = 1) + + def update_notes(self): + while self.notes.get_n_children() > 0: + self.notes.remove_child(0) + for item in self.pattern.items(): + x = self.pulse_to_screen_x(item.pos) - self.instr_width + y = self.row_to_screen_y(item.row + 0.5) + if item.channel == self.channel: + fill_color = 0xC0C0C0 - int(item.vel * 1.5) * 0x000101 + stroke_color = 0x808080 + if item.selected: + stroke_color = 0xFF8080 + else: + fill_color = 0xE0E0E0 + stroke_color = 0xE0E0E0 + if item.len > 1: + x2 = self.pulse_to_screen_x(item.pos + item.len) - self.pulse_to_screen_x(item.pos) + polygon = [-2, 0, 0, -5, x2 - 5, -5, x2, 0, x2 - 5, 5, 0, 5] + else: + polygon = [-5, 0, 0, -5, 5, 0, 0, 5, -5, 0] + item.item = GooCanvas.CanvasPath(parent = self.notes, data = polygon_to_path(polygon), line_width = 1, fill_color = ("#%06x" % fill_color), stroke_color = ("#%06x" % stroke_color)) + #item.item.set_property('stroke_color_rgba', guint(stroke_color)) + item.item.translate(x, y) + + def set_selection_from_rubberband(self): + sx, sy, ex, ey = self.cursor.get_rubberband_box() + for item in self.pattern.items(): + x = self.pulse_to_screen_x(item.pos) + y = self.row_to_screen_y(item.row + 0.5) + item.selected = (x >= sx and x <= ex and y >= sy and y <= ey) + self.update_notes() + + def on_grid_event(self, item, event): + if event.type == Gdk.EventType.KEY_PRESS: + return self.on_key_press(item, event) + if event.type in [Gdk.EventType.BUTTON_PRESS, Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.LEAVE_NOTIFY, Gdk.EventType.MOTION_NOTIFY, Gdk.EventType.BUTTON_RELEASE]: + return self.on_mouse_event(item, event) + + def on_key_press(self, item, event): + keyval, state = event.keyval, event.state + kvname = Gdk.keyval_name(keyval) + if kvname == 'Delete': + self.pattern.delete_selected() + self.update_notes() + elif kvname == 'c': + self.pattern.group_set(channel = self.channel) + self.update_notes() + elif kvname == 'v': + self.pattern.group_set(vel = int(self.toolbox.vel_adj.get_value())) + self.update_notes() + elif kvname == 'plus': + self.pattern.transpose_selected(1) + self.update_notes() + elif kvname == 'minus': + self.pattern.transpose_selected(-1) + self.update_notes() + #else: + # print kvname + + def on_mouse_event(self, item, event): + ex, ey = self.convert_to_item_space(self.get_root_item(), event.x, event.y) + column = self.screen_x_to_column(ex) + row = self.screen_y_to_row(ey) + pulse = column * self.grid_unit + epulse = (ex - self.instr_width) / self.zoom_in + unit = self.grid_unit * self.zoom_in + if self.cursor.rubberband: + if event.type == Gdk.EventType.MOTION_NOTIFY: + self.cursor.update_rubberband(ex, ey) + return + button = event.get_button()[1] + if event.type == Gdk.EventType.BUTTON_RELEASE and button == 1: + self.cursor.end_rubberband(ex, ey) + self.set_selection_from_rubberband() + self.request_update() + return + return + if event.type == Gdk.EventType.BUTTON_PRESS: + button = event.get_button()[1] + self.grab_focus(self.grid) + if ((event.state & Gdk.ModifierType.SHIFT_MASK) == Gdk.ModifierType.SHIFT_MASK) and button == 1: + self.cursor.start_rubberband(ex, ey) + return + if pulse < 0 or pulse >= self.pattern.get_length(): + return + note = self.pattern.get_note(pulse, row, self.channel) + if note is not None: + if button == 3: + vel = int(self.toolbox.vel_adj.get_value()) + self.pattern.set_note_vel(note, vel) + self.cursor.move(pulse, row, note) + self.update_notes() + return + self.toolbox.vel_adj.set_value(note.vel) + else: + note = NoteModel(pulse, self.channel, row, int(self.toolbox.vel_adj.get_value()), self.grid_unit if self.edit_mode == 'M' else 1) + self.pattern.add_note(note) + self.edited_note = note + self.orig_length = note.len + self.orig_velocity = note.vel + self.orig_y = ey + self.grab_add() + self.cursor.move(self.edited_note.pos, self.edited_note.row, note) + self.update_notes() + return + if event.type == Gdk.EventType._2BUTTON_PRESS: + if pulse < 0 or pulse >= self.pattern.get_length(): + return + if self.pattern.has_note(pulse, row, self.channel): + self.pattern.remove_note(pulse, row, self.channel) + self.cursor.move(pulse, row, None) + self.update_notes() + if self.edited_note is not None: + self.grab_remove() + self.edited_note = None + return + if event.type == Gdk.EventType.LEAVE_NOTIFY and self.edited_note is None: + hide_item(self.cursor) + return + if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is None: + if pulse < 0 or pulse >= self.pattern.get_length(): + hide_item(self.cursor) + return + + if abs(pulse - epulse) > 5: + hide_item(self.cursor) + return + note = self.pattern.get_note(column * self.grid_unit, row, self.channel) + self.cursor.move(pulse, row, note) + show_item(self.cursor) + return + if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is not None: + vel = int(self.orig_velocity - self.snap(ey - self.orig_y) / 2) + if vel < 1: vel = 1 + if vel > 127: vel = 127 + self.pattern.set_note_vel(self.edited_note, vel) + len = pulse - self.edited_note.pos + if self.edit_mode == 'D': + len = 1 + elif len <= -self.grid_unit: + len = self.orig_length + elif len <= self.grid_unit: + len = self.grid_unit + else: + len = int((len + 1) / self.grid_unit) * self.grid_unit - 1 + self.pattern.set_note_len(self.edited_note, len) + self.toolbox.vel_adj.set_value(vel) + self.cursor.move_to_note(self.edited_note) + self.update_notes() + self.request_update() + return + if event.type == Gdk.EventType.BUTTON_RELEASE and self.edited_note is not None: + self.edited_note = None + self.grab_remove() + return + + def screen_x_to_column(self, x): + unit = self.grid_unit * self.zoom_in + return int((x - self.instr_width + unit / 2) / unit) + + def screen_y_to_row(self, y): + return int((y - 1) / self.row_height) + + def pulse_to_screen_x(self, pulse): + return pulse * self.zoom_in + self.instr_width + + def column_to_screen_x(self, column): + unit = self.grid_unit * self.zoom_in + return column * unit + self.instr_width + + def row_to_screen_y(self, row): + return row * self.row_height + 1 + + def snap(self, val): + if val > -10 and val < 10: + return 0 + if val >= 10: + return val - 10 + if val <= -10: + return val + 10 + assert False + +class DrumSeqWindow(Gtk.Window): + def __init__(self, length, pat_data): + Gtk.Window.__init__(self, Gtk.WindowType.TOPLEVEL) + self.vbox = Gtk.VBox(spacing = 5) + self.pattern = DrumPatternModel(4, length / (4 * PPQN)) + if pat_data is not None: + self.pattern.import_data(pat_data) + + self.canvas = DrumCanvas(128, self.pattern) + sw = Gtk.ScrolledWindow() + sw.set_size_request(640, 400) + sw.add_with_viewport(self.canvas) + self.vbox.pack_start(sw, True, True, 0) + self.vbox.pack_start(self.canvas.toolbox, False, False, 0) + self.add(self.vbox) + +if __name__ == "__main__": + w = DrumSeqWindow() + w.set_title("Drum pattern editor") + w.show_all() + w.connect('destroy', lambda w: Gtk.main_quit()) + + Gtk.main() diff --git a/template/calfbox/py/drumkit_editor.py b/template/calfbox/py/drumkit_editor.py new file mode 100644 index 0000000..7d42352 --- /dev/null +++ b/template/calfbox/py/drumkit_editor.py @@ -0,0 +1,540 @@ +import cbox +import glob +import os +from gui_tools import * +import sfzparser + +#sample_dir = "/media/resources/samples/dooleydrums/" +sample_dir = cbox.Config.get("init", "sample_dir") + +#################################################################################################################################################### + +class SampleDirsModel(Gtk.ListStore): + def __init__(self): + Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING) + found = False + for entry in cbox.Config.keys("sample_dirs"): + path = cbox.Config.get("sample_dirs", entry) + self.append((entry, path)) + found = True + if not found: + print ("Warning: no sample directories defined. Please add one or more entries of a form: 'name=/path/to/my/samples' to [sample_dirs] section of .cboxrc") + self.append(("home", os.getenv("HOME"))) + self.append(("/", "/")) + def has_dir(self, dir): + return dir in [path for entry, path in self] + +#################################################################################################################################################### + +class SampleFilesModel(Gtk.ListStore): + def __init__(self, dirs_model): + self.dirs_model = dirs_model + self.is_refreshing = False + Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING) + + def refresh(self, sample_dir): + try: + self.is_refreshing = True + self.clear() + if sample_dir is not None: + if not self.dirs_model.has_dir(sample_dir): + self.append((os.path.dirname(sample_dir.rstrip("/")) + "/", "(up)")) + filelist = sorted(glob.glob("%s/*" % sample_dir)) + for f in sorted(filelist): + if os.path.isdir(f) and not self.dirs_model.has_dir(f + "/"): + self.append((f + "/", os.path.basename(f)+"/")) + for f in sorted(filelist): + if f.lower().endswith(".wav") and not os.path.isdir(f): + self.append((f,os.path.basename(f))) + finally: + self.is_refreshing = False + +#################################################################################################################################################### + +class KeyModelPath(object): + def __init__(self, controller, var = None): + self.controller = controller + self.var = var + self.args = [] + def plus(self, var): + if self.var is not None: + print ("Warning: key model plus used twice with %s and %s" % (self.var, var)) + return KeyModelPath(self.controller, var) + def set(self, value): + model = self.controller.get_current_layer_model() + oldval = model.attribs[self.var] + model.attribs[self.var] = value + if value != oldval and not self.controller.no_sfz_update: + print ("%s: set %s to %s" % (self.controller, self.var, value)) + self.controller.update_kit_later() + +#################################################################################################################################################### + +layer_attribs = { + 'volume' : 0.0, + 'pan' : 0.0, + 'ampeg_attack' : 0.001, + 'ampeg_hold' : 0.001, + 'ampeg_decay' : 0.001, + 'ampeg_sustain' : 100.0, + 'ampeg_release' : 0.1, + 'tune' : 0.0, + 'transpose' : 0, + 'cutoff' : 22000.0, + 'resonance' : 0.7, + 'fileg_depth' : 0.0, + 'fileg_attack' : 0.001, + 'fileg_hold' : 0.001, + 'fileg_decay' : 0.001, + 'fileg_sustain' : 100.0, + 'fileg_release' : 0.1, + 'lovel' : 1, + 'hivel' : 127, + 'group' : 0, + 'off_by' : 0, + 'effect1' : 0, + 'effect2' : 0, + 'output' : 0, +} + +#################################################################################################################################################### + +class KeySampleModel(object): + def __init__(self, key, sample, filename): + self.key = key + self.sample = sample + self.filename = filename + self.mode = "one_shot" + self.attribs = layer_attribs.copy() + def set_sample(self, sample, filename): + self.sample = sample + self.filename = filename + def to_sfz(self): + if self.filename == '': + return "" + s = " key=%d sample=%s loop_mode=%s" % (self.key, self.filename, self.mode) + s += "".join([" %s=%s" % item for item in self.attribs.items()]) + return s + "\n" + def to_markup(self): + return "%s" % self.sample + +#################################################################################################################################################### + +class KeyModel(Gtk.ListStore): + def __init__(self, key): + self.key = key + Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_PYOBJECT) + def to_sfz(self): + return "".join([ksm.to_sfz() for name, ksm in self]) + def to_markup(self): + return "\n".join([ksm.to_markup() for name, ksm in self]) + +#################################################################################################################################################### + +class BankModel(dict): + def __init__(self): + dict.__init__(self) + self.clear() + def to_sfz(self): + s = "" + for key in self: + s += self[key].to_sfz() + return s + def clear(self): + dict.clear(self) + for b in range(36, 36 + 16): + self[b] = KeyModel(b) + def from_sfz(self, data, path): + self.clear() + sfz = sfzparser.SFZ() + sfz.parse(data) + for r in sfz.regions: + rdata = r.merged() + if ('key' in rdata) and ('sample' in rdata) and (rdata['sample'] != ''): + key = sfznote2value(rdata['key']) + sample = rdata['sample'] + sample_short = os.path.basename(sample) + if key in self: + ksm = KeySampleModel(key, sample_short, sfzparser.find_sample_in_path(path, sample)) + for k, v in rdata.items(): + if k in ksm.attribs: + if type(layer_attribs[k]) is float: + ksm.attribs[k] = float(v) + elif type(layer_attribs[k]) is int: + ksm.attribs[k] = int(float(v)) + else: + ksm.attribs[k] = v + self[key].append((sample_short, ksm)) + +#################################################################################################################################################### + +class LayerListView(Gtk.TreeView): + def __init__(self, controller): + Gtk.TreeView.__init__(self, None) + self.controller = controller + self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=0) + self.set_cursor((0,)) + #self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [("text/plain", 0, 1)], Gdk.DragAction.COPY) + self.connect('cursor-changed', self.cursor_changed) + #self.connect('drag-data-get', self.drag_data_get) + self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) + self.drag_dest_set_target_list([]) + self.drag_dest_add_text_targets() + self.connect('drag_data_received', self.drag_data_received) + def cursor_changed(self, w): + self.controller.on_layer_changed() + def drag_data_received(self, widget, context, x, y, selection, info, etime): + sample, filename = selection.get_text().split("|") + pad_model = self.controller.get_current_pad_model() + pad_model.append((sample, KeySampleModel(pad_model.key, sample, filename))) + self.controller.current_pad.update_label() + self.controller.on_sample_dragged(self) + +#################################################################################################################################################### + +class LayerEditor(Gtk.VBox): + def __init__(self, controller, bank_model): + Gtk.VBox.__init__(self) + self.table = Gtk.Table(len(self.fields) + 1, 2) + self.table.set_size_request(240, -1) + self.controller = controller + self.bank_model = bank_model + self.name_widget = Gtk.Label() + self.table.attach(self.name_widget, 0, 2, 0, 1) + self.refreshers = [] + for i in range(len(self.fields)): + self.refreshers.append(self.fields[i].add_row(self.table, i + 1, KeyModelPath(controller), None)) + #self.table.attach(left_label(self.fields[i].label), 0, 1, i + 1, i + 2) + self.pack_start(self.table, False, False, 0) + + def refresh(self): + data = self.controller.get_current_layer_model() + if data is None: + self.name_widget.set_text("") + else: + self.name_widget.set_text(data.sample) + data = data.attribs + for r in self.refreshers: + r(data) + + fields = [ + SliderRow("Volume", "volume", -100, 0), + SliderRow("Pan", "pan", -100, 100), + SliderRow("Effect 1", "effect1", 0, 100), + SliderRow("Effect 2", "effect2", 0, 100), + IntSliderRow("Output", "output", 0, 7), + SliderRow("Tune", "tune", -100, 100), + IntSliderRow("Transpose", "transpose", -48, 48), + IntSliderRow("Low velocity", "lovel", 1, 127), + IntSliderRow("High velocity", "hivel", 1, 127), + MappedSliderRow("Amp Attack", "ampeg_attack", env_mapper), + MappedSliderRow("Amp Hold", "ampeg_hold", env_mapper), + MappedSliderRow("Amp Decay", "ampeg_decay", env_mapper), + SliderRow("Amp Sustain", "ampeg_sustain", 0, 100), + MappedSliderRow("Amp Release", "ampeg_release", env_mapper), + MappedSliderRow("Flt Cutoff", "cutoff", filter_freq_mapper), + MappedSliderRow("Flt Resonance", "resonance", LogMapper(0.707, 16, "%0.1f x")), + SliderRow("Flt Depth", "fileg_depth", -4800, 4800), + MappedSliderRow("Flt Attack", "fileg_attack", env_mapper), + MappedSliderRow("Flt Hold", "fileg_hold", env_mapper), + MappedSliderRow("Flt Decay", "fileg_decay", env_mapper), + SliderRow("Flt Sustain", "fileg_sustain", 0, 100), + MappedSliderRow("Flt Release", "fileg_release", env_mapper), + IntSliderRow("Group", "group", 0, 15), + IntSliderRow("Off by group", "off_by", 0, 15), + ] + +#################################################################################################################################################### + +class PadButton(Gtk.RadioButton): + def __init__(self, controller, bank_model, key): + Gtk.RadioButton.__init__(self, use_underline = False) + self.set_mode(False) + self.controller = controller + self.bank_model = bank_model + self.key = key + self.set_size_request(100, 100) + self.update_label() + self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) + self.drag_dest_set_target_list([]) + self.drag_dest_add_text_targets() + self.connect('drag_data_received', self.drag_data_received) + #self.connect('toggled', lambda widget: widget.controller.on_pad_selected(widget) if widget.get_active() else None) + self.connect('pressed', self.on_clicked) + def get_key_model(self): + return self.bank_model[self.key] + def drag_data_received(self, widget, context, x, y, selection, info, etime): + sample, filename = selection.get_text().split("|") + self.get_key_model().clear() + self.get_key_model().append((sample, KeySampleModel(self.key, sample, filename))) + self.update_label() + self.controller.on_sample_dragged(self) + def update_label(self): + data = self.get_key_model() + if data == None: + self.set_label("-") + else: + self.set_label("-") + self.get_child().set_markup(data.to_markup()) + self.get_child().set_line_wrap(True) + def on_clicked(self, w): + self.controller.play_note(self.key) + w.controller.on_pad_selected(w) + +#################################################################################################################################################### + +class PadTable(Gtk.Table): + def __init__(self, controller, bank_model, rows, columns): + Gtk.Table.__init__(self, rows, columns, True) + + self.keys = {} + group = None + for r in range(0, rows): + for c in range(0, columns): + key = 36 + (rows - r - 1) * columns + c + b = PadButton(controller, bank_model, key) + if group is not None: + b.set_group(group) + self.attach(standard_align(b, 0.5, 0.5, 0, 0), c, c + 1, r, r + 1) + self.keys[key] = b + def refresh(self): + for pad in self.keys.values(): + pad.update_label() + +#################################################################################################################################################### + +class FileView(Gtk.TreeView): + def __init__(self, dirs_model, controller): + self.controller = controller + self.is_playing = True + self.dirs_model = dirs_model + self.files_model = SampleFilesModel(dirs_model) + Gtk.TreeView.__init__(self, self.files_model) + self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=1) + self.set_cursor((0,)) + self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY) + self.drag_source_add_text_targets() + self.cursor_changed_handler = self.connect('cursor-changed', self.cursor_changed) + self.connect('drag-data-get', self.drag_data_get) + self.connect('row-activated', self.on_row_activated) + + def stop_playing(self): + if self.is_playing: + self.controller.stop_preview() + self.is_playing = False + + def start_playing(self, fn): + self.is_playing = True + self.controller.start_preview(fn) + + def cursor_changed(self, w): + if self.files_model.is_refreshing: + self.stop_playing() + return + + c = self.get_cursor() + if c[0] is not None: + fn = self.files_model[c[0].get_indices()[0]][0] + if fn.endswith("/"): + return + if fn != "": + self.start_playing(fn) + else: + self.stop_playing(fn) + + def drag_data_get(self, treeview, context, selection, target_id, etime): + cursor = treeview.get_cursor() + if cursor is not None: + c = cursor[0].get_indices()[0] + fr = self.files_model[c] + selection.set_text(str(fr[1]+"|"+fr[0]), -1) + + def on_row_activated(self, treeview, path, column): + c = self.get_cursor() + fn, label = self.files_model[c[0].get_indices()[0]] + if fn.endswith("/"): + self.files_model.refresh(fn) + +#################################################################################################################################################### + +class EditorDialog(Gtk.Dialog): + def __init__(self, parent): + self.prepare_scene() + Gtk.Dialog.__init__(self, "Drum kit editor", parent, Gtk.DialogFlags.DESTROY_WITH_PARENT, + ()) + + self.menu_bar = Gtk.MenuBar() + self.menu_bar.append(create_menu("_Kit", [ + ("_New", self.on_kit_new), + ("_Open...", self.on_kit_open), + ("_Save as...", self.on_kit_save_as), + ("_Close", lambda w: self.response(Gtk.ResponseType.OK)), + ])) + self.menu_bar.append(create_menu("_Layer", [ + ("_Delete", self.on_layer_delete), + ])) + self.vbox.pack_start(self.menu_bar, False, False, 0) + + self.hbox = Gtk.HBox(spacing = 5) + + self.update_source = None + self.current_pad = None + self.dirs_model = SampleDirsModel() + self.bank_model = BankModel() + self.tree = FileView(self.dirs_model, self) + self.layer_list = LayerListView(self) + self.layer_editor = LayerEditor(self, self.bank_model) + self.no_sfz_update = False + + combo = Gtk.ComboBox(model = self.dirs_model) + cell = Gtk.CellRendererText() + combo.pack_start(cell, True) + combo.add_attribute(cell, 'text', 0) + combo.connect('changed', lambda combo, tree_model, combo_model: tree_model.refresh(combo_model[combo.get_active()][1] if combo.get_active() >= 0 else None), self.tree.get_model(), combo.get_model()) + combo.set_active(0) + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) + sw.add(self.tree) + + left_box = Gtk.VBox(spacing = 5) + left_box.pack_start(combo, False, False, 0) + left_box.pack_start(sw, True, True, 5) + self.hbox.pack_start(left_box, True, True, 0) + sw.set_size_request(200, -1) + + self.pads = PadTable(self, self.bank_model, 4, 4) + self.hbox.pack_start(self.pads, True, True, 5) + + right_box = Gtk.VBox(spacing = 5) + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) + sw.set_size_request(320, 100) + sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + sw.add(self.layer_list) + right_box.pack_start(sw, True, True, 0) + sw = Gtk.ScrolledWindow() + sw.set_size_request(320, 200) + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS) + sw.add_with_viewport(self.layer_editor) + right_box.pack_start(sw, True, True, 0) + self.hbox.pack_start(right_box, True, True, 0) + + self.vbox.pack_start(self.hbox, False, False, 0) + self.vbox.show_all() + widget = self.pads.keys[36] + widget.set_active(True) + + self.update_kit() + + def prepare_scene(self): + found_scene = None + for scene in cbox.Document.get_engine().status().scenes: + scene_status = scene.status() + layers = [layer.status().instrument_name for layer in scene_status.layers] + if '_preview_sample' in layers and '_preview_kit' in layers: + found_scene = scene + break + if found_scene is None: + self.scene = cbox.Document.get_engine().new_scene() + self.scene.add_new_instrument_layer("_preview_sample", "stream_player", pos = 0) + ps = self.scene.status().instruments['_preview_sample'][1] + ps.cmd('/output/1/gain', None, -12.0) + self.scene.add_new_instrument_layer("_preview_kit", "sampler", pos = 1) + else: + self.scene = found_scene + _, self._preview_kit = self.scene.status().instruments['_preview_kit'] + _, self._preview_sample = self.scene.status().instruments['_preview_sample'] + + def update_kit(self): + self._preview_kit.engine.load_patch_from_string(0, "", self.bank_model.to_sfz(), "Preview") + self.update_source = None + return False + + def update_kit_later(self): + if self.update_source is not None: + glib.source_remove(self.update_source) + self.update_source = glib.idle_add(self.update_kit) + + def on_sample_dragged(self, widget): + self.update_kit() + if widget == self.current_pad: + self.layer_list.set_cursor(len(self.layer_list.get_model()) - 1) + # self.pad_editor.refresh() + + def refresh_layers(self): + try: + self.no_sfz_update = True + if self.current_pad is not None: + self.layer_list.set_model(self.bank_model[self.current_pad.key]) + self.layer_list.set_cursor(0) + else: + self.layer_list.set_model(None) + self.layer_editor.refresh() + finally: + self.no_sfz_update = False + + def on_pad_selected(self, widget): + self.current_pad = widget + self.refresh_layers() + + def on_layer_changed(self): + self.layer_editor.refresh() + + def on_layer_delete(self, w): + if self.layer_list.get_cursor()[0] is None: + return None + model = self.layer_list.get_model() + model.remove(model.get_iter(self.layer_list.get_cursor()[0])) + self.current_pad.update_label() + self.layer_editor.refresh() + self.update_kit() + self.layer_list.set_cursor(0) + + def get_current_pad_model(self): + return self.current_pad.get_key_model() + + def get_current_layer_model(self): + if self.layer_list.get_cursor()[0] is None: + return None + return self.layer_list.get_model()[self.layer_list.get_cursor()[0]][1] + + def on_kit_new(self, widget): + self.bank_model.from_sfz('', '') + self.pads.refresh() + self.refresh_layers() + self.update_kit() + + def on_kit_open(self, widget): + dlg = Gtk.FileChooserDialog('Open a pad bank', self, Gtk.FileChooserAction.OPEN, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY)) + dlg.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ files")) + dlg.add_filter(standard_filter(["*"], "All files")) + try: + if dlg.run() == Gtk.ResponseType.APPLY: + sfz_data = open(dlg.get_filename(), "r").read() + self.bank_model.from_sfz(sfz_data, dlg.get_current_folder()) + self.pads.refresh() + self.refresh_layers() + self.update_kit() + finally: + dlg.destroy() + + def on_kit_save_as(self, widget): + dlg = Gtk.FileChooserDialog('Save a pad bank', self, Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY)) + dlg.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ files")) + dlg.add_filter(standard_filter(["*"], "All files")) + try: + if dlg.run() == Gtk.ResponseType.APPLY: + open(dlg.get_filename(), "w").write(self.bank_model.to_sfz()) + finally: + dlg.destroy() + + def start_preview(self, filename): + self._preview_sample.engine.load(filename) + self._preview_sample.engine.play() + def stop_preview(self): + self._preview_sample.engine.unload() + def play_note(self, note, vel = 127): + self.scene.send_midi_event(0x90, note, vel) + self.scene.send_midi_event(0x80, note, vel) diff --git a/template/calfbox/py/fx_gui.py b/template/calfbox/py/fx_gui.py new file mode 100644 index 0000000..093f450 --- /dev/null +++ b/template/calfbox/py/fx_gui.py @@ -0,0 +1,432 @@ +import cbox +from gui_tools import * + +################################################################################################################################# + +class EffectWindow(Gtk.Window): + engine_name = None + + def __init__(self, location, main_window, path): + Gtk.Window.__init__(self, Gtk.WindowType.TOPLEVEL) + self.set_type_hint(Gdk.WindowTypeHint.UTILITY) + self.set_transient_for(main_window) + self.main_window = main_window + self.path = path + self.vpath = cbox.VarPath(path) + self.set_title("%s - %s" % (self.effect_name, location)) + self.vbox = Gtk.VBox() + menu_bar = Gtk.MenuBar() + menu_bar.append(create_menu("_Effect", [ + ("_Save as...", self.on_effect_save_as if self.engine_name is not None else None), + ("_Close", lambda w: self.destroy()), + ])) + self.vbox.pack_start(menu_bar, False, False, 0) + if hasattr(self, 'params'): + values = cbox.GetThings(self.path + "/status", [p.name for p in self.params], []) + self.refreshers = [] + t = Gtk.Table(2, len(self.params)) + for i in range(len(self.params)): + p = self.params[i] + t.attach(p.create_label(), 0, 1, i, i+1, Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + widget, refresher = p.create_widget(self.vpath) + refresher(values) + t.attach(widget, 1, 2, i, i+1, Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + self.refreshers.append(refresher) + self.vbox.pack_start(t, True, True, 5) + self.add(self.vbox) + + def create_param_table(self, cols, rows, values, extra_rows = 0): + t = Gtk.Table(4, rows + 1 + extra_rows) + self.cols = cols + self.table_refreshers = [] + for i in range(len(self.cols)): + par = self.cols[i] + t.attach(par.create_label(), i, i + 1, 0, 1, Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL) + for j in range(rows): + widget, refresher = par.create_widget(self.vpath.plus(None, j)) + t.attach(widget, i, i + 1, j + 1, j + 2, Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL) + refresher(values) + self.table_refreshers.append(refresher) + return t + + def get_save_params(self): + if hasattr(self, 'params'): + values = cbox.GetThings(self.path + "/status", [p.name for p in self.params], []) + result = {'engine' : self.engine_name} + for p in self.params: + result[p.name] = str(getattr(values, p.name)) + return result + return None + + def refresh(self): + if hasattr(self, 'params'): + values = cbox.GetThings(self.path + "/status", [p.name for p in self.params], []) + for refresher in self.refreshers: + refresher(values) + + def on_effect_save_as(self, w): + data = self.get_save_params() + if data is None: + print ("Save not implemented for this effect") + return + + dlg = SaveConfigObjectDialog(self, "Select name for effect preset") + try: + if dlg.run() == Gtk.ResponseType.OK and dlg.get_name() != "": + cs = cbox.CfgSection("fxpreset:" + dlg.get_name()) + for name in sorted(data.keys()): + cs[name] = data[name] + cbox.Config.save() + finally: + dlg.destroy() + +################################################################################################################################# + +class PhaserWindow(EffectWindow): + params = [ + MappedSliderRow("Center", "center_freq", LogMapper(100, 2000, freq_format)), + SliderRow("Mod depth", "mod_depth", 0, 7200), + SliderRow("Feedback", "fb_amt", -1, 1), + MappedSliderRow("LFO frequency", "lfo_freq", lfo_freq_mapper), + SliderRow("Stereo", "stereo_phase", 0, 360), + SliderRow("Wet/dry", "wet_dry", 0, 1), + IntSliderRow("Stages", "stages", 1, 12) + ] + engine_name = "phaser" + effect_name = "Phaser" + +class ChorusWindow(EffectWindow): + params = [ + SliderRow("Min. delay", "min_delay", 1, 20), + SliderRow("Mod depth", "mod_depth", 1, 20), + MappedSliderRow("LFO frequency", "lfo_freq", lfo_freq_mapper), + SliderRow("Stereo", "stereo_phase", 0, 360), + SliderRow("Wet/dry", "wet_dry", 0, 1) + ] + engine_name = "chorus" + effect_name = "Chorus" + +class DelayWindow(EffectWindow): + params = [ + SliderRow("Delay time (ms)", "time", 1, 1000), + SliderRow("Feedback", "fb_amt", 0, 1), + SliderRow("Wet/dry", "wet_dry", 0, 1) + ] + engine_name = "delay" + effect_name = "Delay" + +class ReverbWindow(EffectWindow): + params = [ + SliderRow("Decay time", "decay_time", 500, 5000), + SliderRow("Dry amount", "dry_amt", -100, 12), + SliderRow("Wet amount", "wet_amt", -100, 12), + MappedSliderRow("Lowpass", "lowpass", LogMapper(300, 20000, freq_format)), + MappedSliderRow("Highpass", "highpass", LogMapper(30, 2000, freq_format)) + ] + engine_name = "reverb" + effect_name = "Reverb" + +class ToneControlWindow(EffectWindow): + params = [ + MappedSliderRow("Lowpass", "lowpass", LogMapper(300, 20000, freq_format)), + MappedSliderRow("Highpass", "highpass", LogMapper(30, 2000, freq_format)) + ] + engine_name = "tone_control" + effect_name = "Tone Control" + +class CompressorWindow(EffectWindow): + params = [ + SliderRow("Threshold", "threshold", -100, 12), + MappedSliderRow("Ratio", "ratio", LogMapper(1, 100, "%0.2f")), + MappedSliderRow("Attack", "attack", LogMapper(1, 1000, ms_format)), + MappedSliderRow("Release", "release", LogMapper(1, 1000, ms_format)), + SliderRow("Make-up gain", "makeup", -48, 48), + ] + engine_name = "compressor" + effect_name = "Compressor" + +class GateWindow(EffectWindow): + params = [ + SliderRow("Threshold", "threshold", -100, 12), + MappedSliderRow("Ratio", "ratio", LogMapper(1, 100, "%0.2f")), + MappedSliderRow("Attack", "attack", LogMapper(1, 1000, ms_format)), + MappedSliderRow("Hold", "hold", LogMapper(1, 1000, ms_format)), + MappedSliderRow("Release", "release", LogMapper(1, 1000, ms_format)), + ] + engine_name = "gate" + effect_name = "Gate" + +class DistortionWindow(EffectWindow): + params = [ + SliderRow("Drive", "drive", -36, 36), + SliderRow("Shape", "shape", -1, 2), + ] + engine_name = "distortion" + effect_name = "Distortion" + +class FuzzWindow(EffectWindow): + params = [ + SliderRow("Drive", "drive", -36, 36), + SliderRow("Wet/dry", "wet_dry", 0, 1), + SliderRow("Rectify", "rectify", 0, 1), + MappedSliderRow("Pre freq", "band", LogMapper(100, 5000, freq_format)), + SliderRow("Pre width", "bandwidth", 0.25, 4), + MappedSliderRow("Post freq", "band2", LogMapper(100, 5000, freq_format)), + SliderRow("Post width", "bandwidth2", 0.25, 4), + ] + engine_name = "fuzz" + effect_name = "Fuzz" + +class LimiterWindow(EffectWindow): + params = [ + SliderRow("Threshold", "threshold", -100, 12), + MappedSliderRow("Attack", "attack", LogMapper(1, 1000, ms_format)), + MappedSliderRow("Release", "release", LogMapper(1, 5000, ms_format)), + ] + engine_name = "limiter" + effect_name = "Limiter" + +class EQCommon(object): + columns = [ + CheckBoxRow("Active", "active"), + MappedSliderRow("Center Freq", "center", filter_freq_mapper), + MappedSliderRow("Filter Q", "q", LogMapper(0.01, 100, "%f")), + SliderRow("Gain", "gain", -36, 36), + ] + def get_save_params(self): + values = cbox.GetThings(self.path + "/status", ["%active", "%center", "%q", "%gain"], []) + result = {'engine':self.engine_name} + for row in range(self.bands): + row2 = 1 + row + result['band%s_active' % row2] = values.active[row] + result['band%s_center' % row2] = values.center[row] + result['band%s_q' % row2] = values.q[row] + result['band%s_gain' % row2] = values.gain[row] + return result + +class EQWindow(EffectWindow, EQCommon): + def __init__(self, instrument, main_window, path): + EffectWindow.__init__(self, instrument, main_window, path) + values = cbox.GetThings(self.path + "/status", ["%active", "%center", "%q", "%gain"], []) + self.vbox.add(self.create_param_table(self.columns, 4, values)) + def get_save_params(self): + return EQCommon.get_save_params(self) + effect_name = "Equalizer" + engine_name = "parametric_eq" + bands = 4 + +class FBRWindow(EffectWindow, EQCommon): + effect_name = "Feedback Reduction" + engine_name = "feedback_reducer" + bands = 16 + + def __init__(self, instrument, main_window, path): + EffectWindow.__init__(self, instrument, main_window, path) + values = cbox.GetThings(self.path + "/status", ["%active", "%center", "%q", "%gain"], []) + t = self.create_param_table(self.columns, 16, values, 1) + self.vbox.add(t) + self.ready_label = Gtk.Label("-") + t.attach(self.ready_label, 0, 2, 17, 18) + set_timer(self, 100, self.update) + sbutton = Gtk.Button.new_with_mnemonic("_Start") + sbutton.connect("clicked", lambda button, path: cbox.do_cmd(path + "/start", None, []), self.path) + t.attach(sbutton, 2, 4, 17, 18) + + def refresh_table(self): + values = cbox.GetThings(self.path + "/status", ["%active", "%center", "%q", "%gain"], []) + for refresher in self.table_refreshers: + refresher(values) + + def update(self): + values = cbox.GetThings(self.path + "/status", ["finished", "refresh"], []) + if values.refresh: + self.refresh_table() + + if values.finished > 0: + self.ready_label.set_text("Ready") + else: + self.ready_label.set_text("Not Ready") + return True + + def get_save_params(self): + return EQCommon.get_save_params(self) + +class FXChainWindow(EffectWindow): + effect_name = "Effect chain" + + def __init__(self, instrument, main_window, path): + EffectWindow.__init__(self, instrument, main_window, path) + self.fx_table = None + self.choosers = [] + self.refresh_table() + + def refresh_table(self): + res = cbox.GetThings(self.path + "/status", ["*module", "%bypass"], []) + values = res.module + bypass = res.bypass + fx_count = len(values) + t = Gtk.Table(fx_count + 2, 9) + for c in self.choosers: + c.close_popup() + self.choosers = [] + for i in range(1, fx_count + 1): + engine, preset = values[i - 1] + chooser = InsertEffectChooser("%s/module/%s" % (self.path, i), "%s: slot %s" % (self.get_title(), i), engine, preset, bypass[i], self.main_window) + t.attach(chooser.fx_engine, 0, 1, i, i + 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(chooser.fx_preset, 1, 2, i, i + 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(chooser.fx_edit, 2, 3, i, i + 1, 0, Gtk.AttachOptions.SHRINK) + t.attach(chooser.fx_bypass, 3, 4, i, i + 1, 0, Gtk.AttachOptions.SHRINK) + buttons = [ + ("+", self.on_add_clicked, lambda pos: True), + ("-", self.on_delete_clicked, lambda pos: True), + ("Up", self.on_up_clicked, lambda pos: pos > 1), + ("Down", self.on_down_clicked, lambda pos: pos < fx_count) + ] + for j in range(len(buttons)): + label, method, cond = buttons[j] + if not cond(i): + continue + button = Gtk.Button(label) + button.connect('clicked', lambda button, method, pos: method(pos), method, i) + t.attach(button, 4 + j, 5 + j, i, i + 1, 0, Gtk.AttachOptions.SHRINK) + self.choosers.append(chooser) + button = Gtk.Button("+") + button.connect('clicked', lambda button, pos: self.on_add_clicked(pos), fx_count + 1) + t.attach(button, 3, 4, fx_count + 1, fx_count + 2, 0, Gtk.AttachOptions.SHRINK) + if self.fx_table is not None: + self.vbox.remove(self.fx_table) + self.vbox.pack_start(t, True, True, 5) + t.show_all() + self.fx_table = t + def on_add_clicked(self, pos): + cbox.do_cmd(self.path + "/insert", None, [pos]) + self.refresh_table() + def on_delete_clicked(self, pos): + cbox.do_cmd(self.path + "/delete", None, [pos]) + self.refresh_table() + def on_up_clicked(self, pos): + cbox.do_cmd(self.path + "/move", None, [pos, pos - 1]) + self.refresh_table() + def on_down_clicked(self, pos): + cbox.do_cmd(self.path + "/move", None, [pos, pos + 1]) + self.refresh_table() + + +################################################################################################################################# + +effect_engines = ['', 'phaser', 'reverb', 'chorus', 'feedback_reducer', 'tone_control', 'delay', 'parametric_eq', 'compressor', 'gate', 'distortion', 'fuzz', 'fxchain', 'limiter'] + +effect_window_map = { + 'phaser': PhaserWindow, + 'chorus': ChorusWindow, + 'delay': DelayWindow, + 'reverb' : ReverbWindow, + 'feedback_reducer': FBRWindow, + 'parametric_eq': EQWindow, + 'tone_control': ToneControlWindow, + 'compressor': CompressorWindow, + 'gate': GateWindow, + 'distortion': DistortionWindow, + 'fuzz': FuzzWindow, + 'limiter': LimiterWindow, + 'fxchain': FXChainWindow, +} + +################################################################################################################################# + +class EffectListModel(Gtk.ListStore): + def __init__(self): + self.presets = {} + Gtk.ListStore.__init__(self, GObject.TYPE_STRING) + for engine in effect_engines: + self.presets[engine] = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING) + self.append((engine,)) + + for preset in cbox.Config.sections("fxpreset:"): + engine = preset["engine"] + if engine in self.presets: + title = preset.title if hasattr(preset, 'title') else preset.name[9:] + self.presets[engine].append((preset.name[9:], title)) + + def get_model_for_engine(self, engine): + return self.presets[engine] + +effect_list_model = EffectListModel() + +################################################################################################################################# + +class InsertEffectChooser(object): + def __init__(self, opath, location, engine, preset, bypass, main_window): + self.opath = opath + self.location = location + self.main_window = main_window + self.popup = None + + self.fx_engine = standard_combo(effect_list_model, ls_index(effect_list_model, engine, 0), width = 120) + self.fx_engine.connect('changed', self.fx_engine_changed) + + if engine in effect_engines: + model = effect_list_model.get_model_for_engine(engine) + self.fx_preset = standard_combo(model, active_item_lookup = preset, column = 1, lookup_column = 0, width = 120) + else: + self.fx_preset = standard_combo(None, active_item = 0, column = 1, width = 120) + self.fx_preset.connect('changed', self.fx_preset_changed) + + self.fx_edit = Gtk.Button.new_with_mnemonic("_Edit") + self.fx_edit.connect("clicked", self.edit_effect_clicked) + self.fx_edit.set_sensitive(engine in effect_window_map) + + self.fx_bypass = Gtk.ToggleButton.new_with_mnemonic("_Bypass") + self.fx_bypass.set_active(bypass > 0) + self.fx_bypass.connect("clicked", self.bypass_effect_clicked) + + def edit_effect_clicked(self, button): + if self.popup is not None: + self.popup.present() + return + engine = cbox.GetThings(self.opath + "/status", ['insert_engine'], []).insert_engine + wclass = effect_window_map[engine] + popup = wclass(self.location, self.main_window, "%s/engine" % self.opath) + popup.show_all() + popup.present() + popup.connect('delete_event', self.on_popup_closed) + self.popup = popup + + def fx_engine_changed(self, combo): + if self.popup is not None: + self.popup.destroy() + self.popup = None + + engine = combo.get_model()[combo.get_active()][0] + cbox.do_cmd(self.opath + '/insert_engine', None, [engine]) + self.fx_preset.set_model(effect_list_model.get_model_for_engine(engine)) + self.fx_preset.set_active(0) + self.fx_edit.set_sensitive(engine in effect_window_map) + + def fx_preset_changed(self, combo): + if combo.get_active() >= 0: + cbox.do_cmd(self.opath + '/insert_preset', None, [combo.get_model()[combo.get_active()][0]]) + if self.popup is not None: + self.popup.refresh() + + def on_popup_closed(self, popup, event): + self.popup = None + + def close_popup(self): + if self.popup is not None: + self.popup.destroy(); + + def bypass_effect_clicked(self, button): + cbox.do_cmd(self.opath + "/set_bypass", None, [1 if button.get_active() else 0]) + +################################################################################################################################# + +class LoadEffectDialog(SelectObjectDialog): + title = "Load an aux effect" + def __init__(self, parent): + SelectObjectDialog.__init__(self, parent) + def update_model(self, model): + for s in cbox.Config.sections("fxpreset:"): + title = s["title"] + model.append((s.name[9:], s['engine'], s.name, title)) + diff --git a/template/calfbox/py/gui_tools.py b/template/calfbox/py/gui_tools.py new file mode 100644 index 0000000..aa47781 --- /dev/null +++ b/template/calfbox/py/gui_tools.py @@ -0,0 +1,345 @@ +import cbox +import math +import re +from gi.repository import GObject, Gdk, Gtk, GLib as glib + +# Compatibility stuff +Gtk.TreeView.insert_column_with_attributes = lambda self, col, title, renderer, xarg = None, **kwargs: self.insert_column(Gtk.TreeViewColumn(title = title, cell_renderer = renderer, **kwargs), col) + +def callback(cmd, cb, args): + print ("%s %s %s" % (cmd, cb, args)) + +def bold_label(text, halign = 1): + l = Gtk.Label() + l.set_markup("%s" % text) + l.set_alignment(halign, 0.5) + return l + +def left_label(text): + l = Gtk.Label(text) + l.set_alignment(0, 0.5) + return l + +def standard_hslider(adj): + sc = Gtk.HScale(adjustment = adj) + sc.set_size_request(160, -1) + sc.set_value_pos(Gtk.PositionType.RIGHT) + return sc + +notenames = { + 'c' : 0, 'c#' : 1, 'db' : 1, + 'd' : 2, 'd#' : 3, 'eb' : 3, + 'e' : 4, 'e#' : 5, 'fb' : 5, + 'f' : 5, 'f#' : 6, 'gb' : 6, + 'g' : 7, 'g#' : 8, 'ab' : 8, + 'a' : 9, 'a#' : 10, 'bb' : 10, + 'b' : 11, 'b#' : 0, 'cb' : 11, +} + +def sfznote2value(note): + if re.match("[0-9]+$", note): + return int(rdata['key']) + else: + g = re.match("([cdefgab](#|b)?)(-?[0-9])", note) + if g is None: + raise ValueError(note) + return 12 + int(g.group(3)) * 12 + notenames[g.group(1).lower()] + +class LogMapper: + def __init__(self, min, max, format = "%f"): + self.min = min + self.max = max + self.format_string = format + def map(self, value): + return float(self.min) * ((float(self.max) / self.min) ** (value / 100.0)) + def unmap(self, value): + return math.log(value / float(self.min)) * 100 / math.log(float(self.max) / self.min) + def format_value(self, value): + return self.format_string % self.map(value) + + +freq_format = "%0.1f" +ms_format = "%0.1f ms" +lfo_freq_mapper = LogMapper(0.01, 20, "%0.2f") +env_mapper = LogMapper(0.002, 20, "%f") +filter_freq_mapper = LogMapper(20, 20000, "%0.1f Hz") + +def standard_mapped_hslider(adj, mapper): + sc = Gtk.HScale(adjustment = adj) + sc.set_size_request(160, -1) + sc.set_value_pos(Gtk.PositionType.RIGHT) + sc.connect('format-value', lambda scale, value, mapper: mapper.format_value(value), mapper) + return sc + +def standard_align(w, xo, yo, xs, ys): + a = Gtk.Alignment(xalign = xo, yalign = yo, xscale = xs, yscale = ys) + a.add(w) + return a + +def standard_vscroll_window(width, height, content = None): + scroller = Gtk.ScrolledWindow() + scroller.set_size_request(width, height) + scroller.set_shadow_type(Gtk.ShadowType.NONE); + scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + if content is not None: + scroller.add_with_viewport(content) + return scroller + +def standard_combo(model, active_item = None, column = 0, active_item_lookup = None, lookup_column = None, width = None): + cb = Gtk.ComboBox(model = model) + if active_item_lookup is not None: + if lookup_column is None: + lookup_column = column + active_item = ls_index(model, active_item_lookup, lookup_column) + if active_item is not None: + cb.set_active(active_item) + cell = Gtk.CellRendererText() + if width is not None: + cb.set_size_request(width, -1) + cb.pack_start(cell, True) + cb.add_attribute(cell, 'text', column) + return cb + +def ls_index(list_store, value, column): + for i in range(len(list_store)): + if list_store[i][column] == value: + return i + return None + +def standard_filter(patterns, name): + f = Gtk.FileFilter() + for p in patterns: + f.add_pattern(p) + f.set_name(name) + return f + +def checkbox_changed_bool(checkbox, vpath): + vpath.set(1 if checkbox.get_active() else 0) + +def adjustment_changed_int(adjustment, vpath): + vpath.set(int(adjustment.get_value())) + +def adjustment_changed_float(adjustment, vpath): + vpath.set(float(adjustment.get_value())) + +def adjustment_changed_float_mapped(adjustment, vpath, mapper): + vpath.set(mapper.map(adjustment.get_value())) + +def combo_value_changed(combo, vpath, value_offset = 0): + if combo.get_active() != -1: + vpath.set(value_offset + combo.get_active()) + +def combo_value_changed_use_column(combo, vpath, column): + if combo.get_active() != -1: + vpath.set(combo.get_model()[combo.get_active()][column]) + +def tree_toggle_changed_bool(renderer, tree_path, model, opath, column): + model[int(tree_path)][column] = not model[int(tree_path)][column] + cbox.do_cmd(model.make_row_item(opath, tree_path), None, [1 if model[int(tree_path)][column] else 0]) + +def tree_combo_changed(renderer, tree_path, new_value, model, opath, column): + new_value = renderer.get_property('model')[new_value][0] + model[int(tree_path)][column] = new_value + cbox.do_cmd(model.make_row_item(opath, tree_path), None, [new_value]) + +def standard_toggle_renderer(list_model, path, column): + toggle = Gtk.CellRendererToggle() + toggle.connect('toggled', tree_toggle_changed_bool, list_model, path, column) + return toggle + +def standard_combo_renderer(list_model, model, path, column): + combo = Gtk.CellRendererCombo() + combo.set_property('model', model) + combo.set_property('editable', True) + combo.set_property('has-entry', False) + combo.set_property('text-column', 1) + combo.set_property('mode', Gtk.CellRendererMode.EDITABLE) + combo.connect('changed', tree_combo_changed, list_model, path, column) + return combo + +def add_display_row(t, row, label, path, values, item): + t.attach(bold_label(label), 0, 1, row, row+1, Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + w = left_label(getattr(values, item)) + t.attach(w, 1, 2, row, row+1, Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + return w + +def set_timer(widget, time, func, *args): + refresh_id = glib.timeout_add(time, func, *args) + widget.connect('destroy', lambda obj, id: glib.source_remove(id), refresh_id) + +def create_menu(title, items): + menuitem = Gtk.MenuItem.new_with_mnemonic(title) + if items is not None: + menu = Gtk.Menu() + menuitem.set_submenu(menu) + for label, meth in items: + mit = Gtk.MenuItem.new_with_mnemonic(label) + if meth is None: + mit.set_sensitive(False) + else: + mit.connect('activate', meth) + menu.append(mit) + return menuitem + +def note_to_name(note): + if note < 0: + return "N/A" + n = note % 12 + return ("C C#D D#E F F#G G#A A#B "[n * 2 : n * 2 + 2]) + str((note // 12) - 2) + +################################################################################################################################# + +class TableRowWidget: + def __init__(self, label, name, **kwargs): + self.label = label + self.name = name + self.kwargs = kwargs + def get_with_default(self, name, def_value): + return self.kwargs[name] if name in self.kwargs else def_value + def create_label(self): + return bold_label(self.label) + def has_attr(self, values): + if type(values) is dict: + return self.name in values + return hasattr(values, self.name) + def get_attr(self, values): + if type(values) is dict: + return values[self.name] + return getattr(values, self.name) + def get_value(self, values, vpath): + if len(vpath.args) == 1: + return self.get_attr(values)[vpath.args[0]] + return self.get_attr(values) + def add_row(self, table, row, vpath, values): + table.attach(self.create_label(), 0, 1, row, row + 1, Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + widget, refresher = self.create_widget(vpath) + table.attach(widget, 1, 2, row, row + 1, Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, Gtk.AttachOptions.SHRINK) + if values is not None: + refresher(values) + return refresher + def update_sensitive(self, widget, values): + sensitive = (values is not None) and (self.get_with_default('setter', 0) is not None) and self.has_attr(values) + widget.set_sensitive(sensitive) + return sensitive + +class SliderRow(TableRowWidget): + def __init__(self, label, name, minv, maxv, **kwargs): + TableRowWidget.__init__(self, label, name, **kwargs) + self.minv = minv + self.maxv = maxv + def create_widget(self, vpath): + setter = self.get_with_default('setter', adjustment_changed_float) + step = self.kwargs['step'] if 'step' in self.kwargs else 1 + adj = Gtk.Adjustment(self.minv, self.minv, self.maxv, step, 6, 0) + slider = standard_hslider(adj) + if 'digits' in self.kwargs: + slider.set_digits(self.kwargs['digits']) + if setter is not None: + adj.connect("value_changed", setter, vpath.plus(self.name)) + else: + slider.set_sensitive(False) + def refresher(values): + if self.update_sensitive(slider, values): + adj.set_value(self.get_value(values, vpath)) + return (slider, refresher) + +class IntSliderRow(SliderRow): + def __init__(self, label, name, minv, maxv, **kwargs): + SliderRow.__init__(self, label, name, minv, maxv, setter = adjustment_changed_int, **kwargs) + def create_widget(self, vpath): + (slider, refresher) = SliderRow.create_widget(self, vpath) + slider.connect('change-value', self.on_change_value) + slider.set_digits(0) + return slider, refresher + def on_change_value(self, range, scroll, value): + range.set_value(int(value)) + return True + + +class MappedSliderRow(TableRowWidget): + def __init__(self, label, name, mapper, **kwargs): + TableRowWidget.__init__(self, label, name, **kwargs) + self.mapper = mapper + def create_widget(self, vpath): + setter = self.get_with_default('setter', adjustment_changed_float_mapped) + adj = Gtk.Adjustment(0, 0, 100, 1, 6, 0) + slider = standard_mapped_hslider(adj, self.mapper) + if setter is not None: + adj.connect("value_changed", setter, vpath.plus(self.name), self.mapper) + else: + slider.set_sensitive(False) + def refresher(values): + if self.update_sensitive(slider, values): + adj.set_value(self.mapper.unmap(self.get_value(values, vpath))) + return (slider, refresher) + +class CheckBoxRow(TableRowWidget): + def create_widget(self, vpath): + widget = Gtk.CheckButton(self.label) + widget.connect("clicked", checkbox_changed_bool, vpath.plus(self.name)) + def refresher(values): + if self.update_sensitive(widget, values): + widget.set_active(self.get_value(values, vpath) > 0) + return (widget, refresher) + +################################################################################################################################# + +class SelectObjectDialog(Gtk.Dialog): + def __init__(self, parent): + Gtk.Dialog.__init__(self, self.title, parent=parent, modal=True) + self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK) + self.set_default_response(Gtk.ResponseType.OK) + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) + + self.update_model(model) + + scroll = Gtk.ScrolledWindow() + scenes = Gtk.TreeView(model) + scenes.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=0) + scenes.insert_column_with_attributes(1, "Title", Gtk.CellRendererText(), text=3) + scenes.insert_column_with_attributes(2, "Type", Gtk.CellRendererText(), text=1) + scenes.get_column(0).set_property('min_width', 150) + scenes.get_column(1).set_property('min_width', 300) + scenes.get_column(2).set_property('min_width', 150) + scroll.add(scenes) + self.vbox.pack_start(scroll, False, False, 0) + scenes.show() + scroll.set_size_request(640, 500) + scroll.show() + self.scenes = scenes + self.scenes.connect('cursor-changed', lambda w: self.update_default_button()) + self.scenes.connect('row-activated', lambda w, path, column: self.response(Gtk.ResponseType.OK)) + self.scenes.set_cursor((0,), scenes.get_column(0)) + self.scenes.grab_focus() + self.update_default_button() + + def get_selected_object(self): + return self.scenes.get_model()[self.scenes.get_cursor()[0].get_indices()[0]] + def update_default_button(self): + self.set_response_sensitive(Gtk.ResponseType.OK, self.scenes.get_cursor()[1] is not None) + +################################################################################################################################# + +class SaveConfigObjectDialog(Gtk.Dialog): + def __init__(self, parent, title): + Gtk.Dialog.__init__(self, title, parent, Gtk.DialogFlags.MODAL, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) + self.set_default_response(Gtk.ResponseType.OK) + + l = Gtk.Label() + l.set_text_with_mnemonic("_Name") + e = Gtk.Entry() + self.entry = e + e.set_activates_default(True) + e.connect('changed', self.on_entry_changed) + row = Gtk.HBox() + row.pack_start(l, False, False, 5) + row.pack_start(e, True, True, 5) + row.show_all() + self.vbox.pack_start(row, True, True, 0) + e.grab_focus() + self.on_entry_changed(e) + def get_name(self): + return self.entry.get_text() + def on_entry_changed(self, w): + self.set_response_sensitive(Gtk.ResponseType.OK, w.get_text() != '') diff --git a/template/calfbox/py/instr_gui.py b/template/calfbox/py/instr_gui.py new file mode 100644 index 0000000..1ae4d51 --- /dev/null +++ b/template/calfbox/py/instr_gui.py @@ -0,0 +1,385 @@ +import cbox +from gui_tools import * + +class StreamWindow(Gtk.VBox): + def __init__(self, instrument, iobj): + Gtk.Widget.__init__(self) + self.engine = iobj.engine + self.path = self.engine.path + + panel = Gtk.VBox(spacing=5) + + self.filebutton = Gtk.FileChooserButton("Streamed file") + self.filebutton.set_action(Gtk.FileChooserAction.OPEN) + self.filebutton.set_local_only(True) + self.filebutton.set_filename(self.engine.status().filename) + self.filebutton.add_filter(standard_filter(["*.wav", "*.WAV", "*.ogg", "*.OGG", "*.flac", "*.FLAC"], "All loadable audio files")) + self.filebutton.add_filter(standard_filter(["*.wav", "*.WAV"], "RIFF WAVE files")) + self.filebutton.add_filter(standard_filter(["*.ogg", "*.OGG"], "OGG container files")) + self.filebutton.add_filter(standard_filter(["*.flac", "*.FLAC"], "FLAC files")) + self.filebutton.add_filter(standard_filter(["*"], "All files")) + self.filebutton.connect('file-set', self.file_set) + hpanel = Gtk.HBox(spacing = 5) + hpanel.pack_start(Gtk.Label.new_with_mnemonic("_Play file:"), False, False, 5) + hpanel.pack_start(self.filebutton, True, True, 5) + panel.pack_start(hpanel, False, False, 5) + + self.adjustment = Gtk.Adjustment() + self.adjustment_handler = self.adjustment.connect('value-changed', self.pos_slider_moved) + self.progress = standard_hslider(self.adjustment) + panel.pack_start(self.progress, False, False, 5) + + self.play_button = Gtk.Button.new_with_mnemonic("_Play") + self.rewind_button = Gtk.Button.new_with_mnemonic("_Rewind") + self.stop_button = Gtk.Button.new_with_mnemonic("_Stop") + buttons = Gtk.HBox(spacing = 5) + buttons.add(self.play_button) + buttons.add(self.rewind_button) + buttons.add(self.stop_button) + panel.pack_start(buttons, False, False, 5) + + self.add(panel) + self.play_button.connect('clicked', lambda x: self.engine.play()) + self.rewind_button.connect('clicked', lambda x: self.engine.seek(0)) + self.stop_button.connect('clicked', lambda x: self.engine.stop()) + set_timer(self, 30, self.update) + + def update(self): + attribs = cbox.GetThings("%s/status" % self.path, ['filename', 'pos', 'length', 'playing'], []) + self.progress.set_sensitive(attribs.length is not None) + if attribs.length is not None: + try: + self.adjustment.handler_block(self.adjustment_handler) + self.adjustment.set_properties(value = attribs.pos, lower = 0, upper = attribs.length) + #self.adjustment.set_all(attribs.pos, 0, attribs.length, 44100, 44100 * 10, 0) + finally: + self.adjustment.handler_unblock(self.adjustment_handler) + return True + + def pos_slider_moved(self, adjustment): + self.engine.seek(adjustment.get_value()) + + def file_set(self, button): + self.engine.load(button.get_filename()) + + +class WithPatchTable: + def __init__(self, attribs): + self.patches = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT) + self.patch_combos = [] + + self.table = Gtk.Table(2, 16) + self.table.set_col_spacings(5) + + for i in range(16): + self.table.attach(bold_label("Channel %s" % (1 + i)), 0, 1, i, i + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + cb = standard_combo(self.patches, None) + cb.connect('changed', self.patch_combo_changed, i + 1) + self.table.attach(cb, 1, 2, i, i + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + self.patch_combos.append(cb) + + self.update_model() + set_timer(self, 500, self.patch_combo_update) + + def update_model(self): + self.patches.clear() + patches = self.engine.get_patches() + ch_patches = self.engine.status().patches + self.mapping = {} + for id in patches: + self.mapping[id] = len(self.mapping) + self.patches.append((self.fmt_patch_name(patches[id], id), id)) + self.patch_combo_update() + + def patch_combo_changed(self, combo, channel): + if combo.get_active() == -1: + return + self.engine.set_patch(channel, self.patches[combo.get_active()][1]) + + def patch_combo_update(self): + patch = self.engine.status().patches + for i in range(16): + cb = self.patch_combos[i] + old_patch_index = cb.get_active() if cb.get_active() >= 0 else -1 + patch_id = patch[i + 1][0] + current_patch_index = self.mapping[patch_id] if (patch_id >= 0 and patch_id in self.mapping) else -1 + if old_patch_index != current_patch_index: + cb.set_active(current_patch_index) + #self.status_label.set_markup(s) + return True + + def fmt_patch_name(self, patch, id): + return "%s (%s)" % (patch, id) + +class FluidsynthWindow(Gtk.VBox, WithPatchTable): + def __init__(self, instrument, iobj): + Gtk.VBox.__init__(self) + self.engine = iobj.engine + self.path = self.engine.path + print (iobj.path) + + attribs = iobj.status() + + panel = Gtk.VBox(spacing=5) + table = Gtk.Table(2, 1) + IntSliderRow("Polyphony", "polyphony", 2, 256).add_row(table, 0, cbox.VarPath(self.path), attribs) + + WithPatchTable.__init__(self, attribs) + panel.pack_start(standard_vscroll_window(-1, 160, self.table), True, True, 5) + + hpanel = Gtk.HBox(spacing = 5) + self.filebutton = Gtk.FileChooserButton("Soundfont") + self.filebutton.set_action(Gtk.FileChooserAction.OPEN) + self.filebutton.set_local_only(True) + self.filebutton.set_filename(cbox.GetThings("%s/status" % self.path, ['soundfont'], []).soundfont) + self.filebutton.add_filter(standard_filter(["*.sf2", "*.SF2"], "SF2 Soundfonts")) + self.filebutton.add_filter(standard_filter(["*"], "All files")) + hpanel.pack_start(Gtk.Label.new_with_mnemonic("_Load SF2:"), False, False, 5) + hpanel.pack_start(self.filebutton, True, True, 5) + unload = Gtk.Button.new_with_mnemonic("_Unload") + hpanel.pack_start(unload, False, False, 5) + unload.connect('clicked', self.unload) + panel.pack_start(hpanel, False, False, 5) + + self.filebutton.connect('file-set', self.file_set) + + self.add(panel) + def file_set(self, button): + self.engine.load_soundfont(button.get_filename()) + self.update_model() + def unload(self, button): + self.filebutton.set_filename('') + +class LoadProgramDialog(SelectObjectDialog): + title = "Load a sampler program" + def __init__(self, parent): + SelectObjectDialog.__init__(self, parent) + def update_model(self, model): + for s in cbox.Config.sections("spgm:"): + title = s["title"] + if s["sfz"] == None: + model.append((s.name[5:], "Program", s.name, title)) + else: + model.append((s.name[5:], "SFZ", s.name, title)) + +class SamplerWindow(Gtk.VBox, WithPatchTable): + def __init__(self, instrument, iobj): + Gtk.VBox.__init__(self) + self.engine = iobj.engine + self.path = self.engine.path + + iattribs = iobj.status() + attribs = self.engine.status() + + panel = Gtk.VBox(spacing=5) + table = Gtk.Table(2, 2) + table.set_col_spacings(5) + IntSliderRow("Polyphony", "polyphony", 1, 128).add_row(table, 0, cbox.VarPath(self.path), attribs) + self.voices_widget = add_display_row(table, 1, "Voices in use", cbox.VarPath(self.path), attribs, "active_voices") + panel.pack_start(table, False, False, 5) + + WithPatchTable.__init__(self, attribs) + panel.pack_start(standard_vscroll_window(-1, 160, self.table), True, True, 5) + self.add(panel) + hpanel = Gtk.HBox(spacing = 5) + + hpanel.pack_start(Gtk.Label.new_with_mnemonic("Add from _SFZ:"), False, False, 5) + self.filebutton = Gtk.FileChooserButton("Soundfont") + self.filebutton.set_action(Gtk.FileChooserAction.OPEN) + self.filebutton.set_local_only(True) + #self.filebutton.set_filename(cbox.GetThings("%s/status" % self.path, ['soundfont'], []).soundfont) + self.filebutton.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ Programs")) + self.filebutton.add_filter(standard_filter(["*"], "All files")) + self.filebutton.connect('file-set', self.load_sfz) + hpanel.pack_start(self.filebutton, False, True, 5) + + load_button = Gtk.Button.new_with_mnemonic("Add from _config") + load_button.connect('clicked', self.load_config) + hpanel.pack_start(load_button, False, True, 5) + panel.pack_start(hpanel, False, False, 5) + + set_timer(self, 200, self.voices_update) + self.output_model = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) + for i in range(iattribs.outputs): + self.output_model.append((i + 1, "Out %d" % (i + 1))) + + self.polyphony_labels = {} + self.output_combos = {} + for i in range(16): + button = Gtk.Button("Dump SFZ") + button.connect("clicked", self.dump_sfz, i + 1) + self.table.attach(button, 2, 3, i, i + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + label = Gtk.Label("") + self.table.attach(label, 3, 4, i, i + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + self.polyphony_labels[i + 1] = label + combo = standard_combo(self.output_model, column = 1) + combo.connect('changed', self.output_combo_changed, i + 1) + self.table.attach(combo, 4, 5, i, i + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + self.output_combos[i + 1] = combo + self.output_combo_update() + + def output_combo_update(self): + output = self.engine.status().output + for i in range(16): + cb = self.output_combos[i + 1] + old_channel_index = cb.get_active() if cb.get_active() >= 0 else -1 + if old_channel_index != output[1 + i]: + cb.set_active(output[1 + i]) + #self.status_label.set_markup(s) + return True + + def output_combo_changed(self, combo, channel): + if combo.get_active() == -1: + return + self.engine.set_output(channel, combo.get_active()) + + def dump_sfz(self, w, channel): + attribs = cbox.GetThings("%s/status" % self.path, ['%patch', 'polyphony', 'active_voices'], []) + prog_no, patch_name = attribs.patch[channel] + pname, uuid, in_use_cnt = cbox.GetThings("%s/patches" % self.path, ['%patch'], []).patch[prog_no] + print ("UUID=%s" % uuid) + patch = cbox.Document.map_uuid(uuid) + groups = patch.get_groups() + for r in groups[0].get_children(): + print (" %s" % (r.as_string())) + for grp in patch.get_groups()[1:]: + print (" %s" % (grp.as_string())) + for r in grp.get_children(): + print (" %s" % (r.as_string())) + + def load_config(self, event): + d = LoadProgramDialog(self.get_toplevel()) + response = d.run() + try: + if response == Gtk.ResponseType.OK: + scene = d.get_selected_object() + pgm_id = self.engine.get_unused_program() + self.engine.load_patch_from_cfg(pgm_id, scene[2], scene[2][5:]) + self.update_model() + finally: + d.destroy() + + def load_sfz(self, button): + pgm_id = self.engine.get_unused_program() + self.engine.load_patch_from_file(pgm_id, self.filebutton.get_filename(), self.filebutton.get_filename()) + self.update_model() + + def voices_update(self): + status = self.engine.status() + self.voices_widget.set_text("%s voices, %s waiting, %s pipes" % (status.active_voices, status.active_prevoices, status.active_pipes)) + for i in range(16): + self.polyphony_labels[i + 1].set_text("%d voices, %d waiting" % (status.channel_voices[i + 1], status.channel_prevoices[i + 1])) + + return True + + def fmt_patch_name(self, patch, id): + return "%s (%s)" % (patch[0], id) + +class TonewheelOrganWindow(Gtk.VBox): + combos = [ + (1, 'Upper', 'upper_vibrato', [(0, 'Off'), (1, 'On')]), + (1, 'Lower', 'lower_vibrato', [(0, 'Off'), (1, 'On')]), + (1, 'Mode', 'vibrato_mode', [(0, '1'), (1, '2'), (2, '3')]), + (1, 'Chorus', 'vibrato_chorus', [(0, 'Off'), (1, 'On')]), + (2, 'Enable', 'percussion_enable', [(0, 'Off'), (1, 'On')]), + (2, 'Harmonic', 'percussion_3rd', [(0, '2nd'), (1, '3rd')]), + ] + def __init__(self, instrument, iobj): + Gtk.VBox.__init__(self) + self.engine = iobj.engine + self.path = self.engine.path + panel = Gtk.VBox(spacing=10) + table = Gtk.Table(4, 10) + table.props.row_spacing = 10 + table.set_col_spacings(5) + self.drawbars = {} + self.hboxes = {} + self.hboxes[1] = Gtk.HBox(spacing = 10) + self.hboxes[1].pack_start(Gtk.Label('Vibrato: '), False, False, 5) + self.hboxes[2] = Gtk.HBox(spacing = 10) + self.hboxes[2].pack_start(Gtk.Label('Percussion: '), False, False, 5) + self.combos = {} + for row, name, flag, options in TonewheelOrganWindow.combos: + label = Gtk.Label(name) + self.hboxes[row].pack_start(label, False, False, 5) + model = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) + for oval, oname in options: + model.append((oval, oname)) + combo = standard_combo(model, column = 1) + self.hboxes[row].pack_start(combo, False, False, 5) + combo.update_handler = combo.connect('changed', lambda w, setter: setter(w.get_model()[w.get_active()][0]), getattr(self.engine, 'set_' + flag)) + self.combos[flag] = combo + panel.pack_start(self.hboxes[1], False, False, 5) + panel.pack_start(self.hboxes[2], False, False, 5) + table.attach(Gtk.Label("Upper"), 0, 1, 0, 1) + table.attach(Gtk.Label("Lower"), 0, 1, 1, 2) + for i in range(9): + slider = Gtk.VScale(adjustment = Gtk.Adjustment(0, 0, 8, 1, 1)) + slider.props.digits = 0 + table.attach(slider, i + 1, i + 2, 0, 1) + self.drawbars['u%d' % i] = slider.get_adjustment() + slider.get_adjustment().connect('value-changed', lambda adj, drawbar: self.engine.set_upper_drawbar(drawbar, int(adj.get_value())), i) + slider = Gtk.VScale(adjustment = Gtk.Adjustment(0, 0, 8, 1, 1)) + slider.props.digits = 0 + table.attach(slider, i + 1, i + 2, 1, 2) + self.drawbars['l%d' % i] = slider.get_adjustment() + slider.get_adjustment().connect('value-changed', lambda adj, drawbar: self.engine.set_lower_drawbar(drawbar, int(adj.get_value())), i) + panel.add(table) + self.add(panel) + self.refresh() + + def refresh(self): + attribs = self.engine.status() + for i in range(9): + self.drawbars['u%d' % i].set_value(attribs.upper_drawbar[i]) + self.drawbars['l%d' % i].set_value(attribs.lower_drawbar[i]) + for row, name, flag, options in TonewheelOrganWindow.combos: + combo = self.combos[flag] + combo.handler_block(combo.update_handler) + combo.set_active(ls_index(combo.get_model(), getattr(attribs, flag), 0)) + combo.handler_unblock(combo.update_handler) + +class JackInputWindow(Gtk.VBox): + def __init__(self, instrument, iobj): + Gtk.VBox.__init__(self) + print (iobj.status()) + self.engine = iobj.engine + self.path = self.engine.path + table = Gtk.Table(2, 2) + table.props.row_spacing = 10 + no_inputs = cbox.JackIO.status().audio_inputs + model = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) + model.append((0, "Unconnected")) + for i in range(no_inputs): + model.append((1 + i, "Input#%s" % (1 + i))) + self.combos = [] + for i, name in ((0, "Left"), (1, "Right")): + table.attach(Gtk.Label(name), 0, 1, i, i + 1) + combo = standard_combo(model, column = 1) + table.attach(combo, 1, 2, i, i + 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK) + combo.update_handler = combo.connect('changed', self.update_inputs) + self.combos.append(combo) + self.pack_start(table, False, False, 5) + self.refresh() + + def update_inputs(self, w): + def to_base1(value): + return -1 if value <= 0 else value + left = to_base1(self.combos[0].get_active()) + right = to_base1(self.combos[1].get_active()) + self.engine.cmd("/inputs", None, left, right) + + def refresh(self): + inputs = self.engine.status().inputs + self.combos[0].set_active(max(0, inputs[0])) + self.combos[1].set_active(max(0, inputs[1])) + +instrument_window_map = { + 'stream_player' : StreamWindow, + 'fluidsynth' : FluidsynthWindow, + 'sampler' : SamplerWindow, + 'tonewheel_organ' : TonewheelOrganWindow +} + +if int(cbox.Config.get("io", "use_usb") or "0") == 0 and cbox.JackIO.status().audio_inputs > 0: + instrument_window_map['jack_input'] = JackInputWindow diff --git a/template/calfbox/py/metadata.py b/template/calfbox/py/metadata.py new file mode 100644 index 0000000..cf6a4d4 --- /dev/null +++ b/template/calfbox/py/metadata.py @@ -0,0 +1,99 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +This file implements the JackIO Python side of Jack Medata as described here: + http://www.jackaudio.org/files/docs/html/group__Metadata.html + +""" + +#get_thing +from calfbox._cbox2 import do_cmd + +def get_thing(): pass #overwritten by cbox.py after import + +class Metadata: + @staticmethod + def get_property(port, key): + """port is the portname as string System:out_1 + returns tuple (value, mime/type)""" + result = get_thing("/io/get_property", "/value", [(str, str)], port, key) + return result + + @staticmethod + def get_properties(port): + """returns a list of tuples (key, value, mime/type)""" + result = get_thing("/io/get_properties", "/properties", [(str, str, str)], port) + return result + + @staticmethod + def get_all_properties(): + """returns a list of tuples (portname, key, value, mime/type)""" + result = get_thing("/io/get_all_properties", "/all_properties", [(str, str, str, str)]) + return result + + @staticmethod + def set_property(port, key, value, jackPropertyType=""): + """empty jackPropertyType leads to UTF-8 string + for convenience we see if value is a python int and send the right jack_property_t::type + jackio.c checks if the port exists, even though metadata allows keys for non-existent uuids. + """ + if type(value) is int: + jackPropertyType = "http://www.w3.org/2001/XMLSchema#int" + value = str(value) + elif not type(value) is str: + return TypeError("value {} must be int or str but was {}".format(value, type(value))) + do_cmd("/io/set_property", None, [port, key, value, jackPropertyType]) + + @staticmethod + def remove_property(port, key): + """port is the portname as string System:out_1""" + do_cmd("/io/remove_property", None, [port, key]) + + @staticmethod + def remove_properties(port): + """port is the portname as string System:out_1""" + do_cmd("/io/remove_properties", None, [port]) + + @staticmethod + def remove_all_properties(): + """Remove all metadata from jack server""" + do_cmd("/io/remove_all_properties", None, []) + + + #Higher Level Functions + + @staticmethod + def set_port_order(port, index): + """ + port is the portname as string including a client name System:out_1 + + https://github.com/drobilla/jackey + + Order for a port. + This is used to specify the best order to show ports in user interfaces. + The value MUST be an integer. There are no other requirements, so there may + be gaps in the orders for several ports. Applications should compare the + orders of ports to determine their relative order, but must not assign any + other relevance to order values. + + #define JACKEY_ORDER "http://jackaudio.org/metadata/order" + """ + Metadata.set_property(port, "http://jackaudio.org/metadata/order", index) #automatically converted to int-mime + + @staticmethod + def set_all_port_order(pDict): + """ + pDict portname as string : index as integer + """ + if not (len(pDict.values()) == len(set(pDict.values()))): + raise ValueError("All indices for ordering must be unique") + + for port, index in pDict.items(): + Metadata.set_port_order(port, index) + + @staticmethod + def set_pretty_name(port, name): + """port is the portname as string including a client name System:out_1 + Name however is just the port name, without a client.""" + Metadata.set_property(port, "http://jackaudio.org/metadata/pretty-name", name) diff --git a/template/calfbox/py/nocturn.py b/template/calfbox/py/nocturn.py new file mode 100644 index 0000000..8ac1915 --- /dev/null +++ b/template/calfbox/py/nocturn.py @@ -0,0 +1,122 @@ +# Novation Nocturn driver +# Based on DWTFYW code by De Wet van Niekert (dewert). However, I had to +# put reading of the input endpoint in a separate thread because the only +# reliable way to read it is by using large timeouts (1s or so). With shorter +# timeouts, some events are lost/replaced by crossfader value. + +import array +import binascii +import fcntl +import os +import usb.core +import usb.util +import sys +import time +import threading + +class NocturnCommands: + def __init__(self): + self.pkt = "" + def setModeButtonLight(self, button, state): + self.pkt += chr(0x70 + button) + ('\x01' if state else '\x00') + def setUserButtonLight(self, button, state): + self.pkt += chr(0x78 + button) + ('\x01' if state else '\x00') + def setEncoderMode(self, encoder, mode): + self.pkt += chr(0x48 + encoder) + chr(mode << 4) + def setEncoderValue(self, encoder, value): + self.pkt += chr(0x40 + encoder) + chr(value) + def setSpeedDialMode(self, mode): + self.pkt += chr(0x51) + chr(mode << 4) + def setSpeedDialValue(self, value): + self.pkt += chr(0x50) + chr(value) + def clear(self): + for i in range(8): + self.setModeButtonLight(i, False) + self.setUserButtonLight(i, False) + if i & 1: + self.setEncoderMode(i, 3) + else: + self.setEncoderMode(i, 4) + self.setEncoderValue(i, 64) + self.setSpeedDialMode(5) + self.setSpeedDialValue(64) + +class NocturnHandler(threading.Thread): + def __init__(self, n): + threading.Thread.__init__(self) + self.nocturn = n + self.rpipefd, self.wpipefd = os.pipe() + self.rpipe = os.fdopen(self.rpipefd, "rb") + self.wpipe = os.fdopen(self.wpipefd, "wb") + flags = fcntl.fcntl(self.rpipe, fcntl.F_GETFL) + fcntl.fcntl(self.rpipe, fcntl.F_SETFL, flags | os.O_NONBLOCK) + self.setDaemon(True) + def run(self): + while True: + pkt = self.nocturn.read() + if pkt is not None: + self.wpipe.write(pkt) + self.wpipe.flush() + def poll(self, handler): + try: + data = array.array('B', self.rpipe.read()) + i = 0 + # For longer sequences, Nocturn skips the control change message + while i < len(data): + if data[i] == 176: + i += 1 + continue + handler(data[i], data[i + 1]) + i += 2 + except IOError as e: + pass + def get_poll_fd(self): + return self.rpipefd + +class Nocturn: + vendorID = 0x1235 + productID = 0x000a + def __init__(self): + dev = usb.core.find(idVendor=self.vendorID, idProduct=self.productID) + # The values in here don't seem to matter THAT much + initPackets=["b00000","28002b4a2c002e35","2a022c722e30"] + #This is a minimum set that enables the device, but then it doesn't + #really work reliably, at least the touch sensing + #initPackets=["b00000", "2800"] + + if dev is None: + raise ValueError('Device not found') + sys.exit() + + self.dev = dev + cfg = dev[1] + intf = cfg[(0,0)] + + self.ep = intf[1] + self.ep2 = intf[0] + dev.set_configuration(2) + for packet in initPackets: + self.ep.write(binascii.unhexlify(packet)) + + self.reset() + self.reader = NocturnHandler(self) + self.reader.start() + + def reset(self): + cmd = NocturnCommands() + cmd.clear() + self.execute(cmd) + + def execute(self, cmd): + self.ep.write(cmd.pkt) + def read(self): + try: + data = self.ep2.read(8, None) + return buffer(data) + except usb.core.USBError as e: + return None + def poll(self, handler): + return self.reader.poll(handler) + def get_poll_fd(self): + return self.reader.get_poll_fd() + diff --git a/template/calfbox/py/nullbox.py b/template/calfbox/py/nullbox.py new file mode 100644 index 0000000..01cfdbc --- /dev/null +++ b/template/calfbox/py/nullbox.py @@ -0,0 +1,53 @@ +class NullCalfbox(str): #iterable + """A drop-in replacement for calfboxs python module. + Use this for testing and development. + + At the start of your program, first file, insert: + import calfbox.nullbox + + All further + from calfbox import cbox + will use the null module. + + Even additional + import calfbox + will use the nullbox module. + """ + + def __init__(self, *args, **kwargs): + self.client_name = "" + self.pos_ppqn = 0 + self.ignore_program_changes = False + self.patch = {i:(0, "nullbox") for i in range(1,17)} #catches status().patch + self.frame_rate = 48000 + self.frame = 0 + + def __getattr__(self, *args, **kwargs): + return __class__() + + def __call__(self, *args, **kwargs): + return __class__() + + def __getitem__(self, key): + return __class__() + + def serialize_event(self, *args, **kwargs): + return b'' + + def get_patches(self, *args): + """sf2 compatibility""" + return { + 0 : "nullbox", + } + + def set_ignore_program_changes(self, state): + self.ignore_program_changes = state + + + +import sys +import calfbox.nullbox +sys.modules["calfbox"] = sys.modules["calfbox.nullbox"] +import calfbox + +cbox = NullCalfbox("fake cbox null client") diff --git a/template/calfbox/py/sfzparser.py b/template/calfbox/py/sfzparser.py new file mode 100644 index 0000000..9cadc3c --- /dev/null +++ b/template/calfbox/py/sfzparser.py @@ -0,0 +1,63 @@ +import os +import re + +class SFZRegion(dict): + def __init__(self, group): + dict.__init__(self) + self.group = group + + def merged(self): + if self.group is None: + return dict(self) + v = dict(self) + v.update(self.group) + return v + + def __str__(self): + return "(" + str(self.group) + ") : " + dict.__str__(self.merged()) + +class SFZContext: + def __init__(self): + self.group = None + self.region = None + +class SFZ: + def __init__(self): + self.regions = [] + + def load(self, fname): + self.parse(open(fname, "r").read()) + + def parse(self, data): + context = SFZContext() + for ptype, pdata in re.findall("<(region|group)>\s*([^<]*)", data, re.S): + self.parse_part(ptype, pdata.strip(), context) + + def parse_part(self, ptype, pdata, context): + if ptype == 'group': + context.group = {} + context.region = None + target = context.group + else: + context.region = SFZRegion(context.group) + target = context.region + + pairs = re.split("\s+([a-zA-Z_0-9]+)=", " "+pdata)[1:] + for i in range(0, len(pairs), 2): + target[pairs[i]] = pairs[i + 1] + if ptype == 'region': + self.regions.append(target) + +def find_sample_in_path(path, sample): + jpath = os.path.join(path, sample) + if os.path.exists(jpath): + return jpath + path, sample = os.path.split(jpath) + sample = sample.lower() + files = os.path.listdir(path) + for f in files: + if f.lower() == sample: + return os.path.join(path, f) + return None + + \ No newline at end of file diff --git a/template/calfbox/recsrc.c b/template/calfbox/recsrc.c new file mode 100644 index 0000000..55a3317 --- /dev/null +++ b/template/calfbox/recsrc.c @@ -0,0 +1,130 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "errors.h" +#include "recsrc.h" +#include "rt.h" +#include "scene.h" +#include "stm.h" + +static void cbox_recorder_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_recorder *rec = CBOX_H2O(objhdr); + if (rec->destroy) + rec->destroy(rec); + free(rec); +} + +CBOX_CLASS_DEFINITION_ROOT(cbox_recorder) + +static gboolean cbox_recording_source_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +void cbox_recording_source_init(struct cbox_recording_source *src, struct cbox_scene *scene, uint32_t max_numsamples, int channels) +{ + src->scene = scene; + src->handlers = NULL; + src->handler_count = 0; + src->max_numsamples = max_numsamples; + src->channels = channels; + cbox_command_target_init(&src->cmd_target, cbox_recording_source_process_cmd, src); +} + +gboolean cbox_recording_source_attach(struct cbox_recording_source *src, struct cbox_recorder *rec, GError **error) +{ + if (rec->attach && !rec->attach(rec, src, error)) + return FALSE; + cbox_rt_array_insert(app.rt, (void ***)&src->handlers, &src->handler_count, 0, rec); + return TRUE; +} + +int cbox_recording_source_detach(struct cbox_recording_source *src, struct cbox_recorder *rec, GError **error) +{ + int index = -1; + for (uint32_t i = 0; i < src->handler_count; i++) + { + if (src->handlers[i] == rec) + { + index = i; + break; + } + } + if (index == -1) + { + if (error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Recorder is not attached to this source"); + return 0; + } + + cbox_rt_array_remove(app.rt, (void ***)&src->handlers, &src->handler_count, index); + // XXXKF: when converting to async API, the array_remove must be done synchronously or + // detach needs to be called in the cleanup part of the remove command, otherwise detach + // may be called on 'live' recorder, which may cause unpredictable results. + return rec->detach ? rec->detach(rec, error) : 1; +} + +void cbox_recording_source_push(struct cbox_recording_source *src, const float **buffers, uint32_t offset, uint32_t numsamples) +{ + for (uint32_t i = 0; i < src->handler_count; i++) + src->handlers[i]->record_block(src->handlers[i], buffers, offset, numsamples); +} + +void cbox_recording_source_uninit(struct cbox_recording_source *src) +{ + STM_ARRAY_FREE_OBJS(src->handlers, src->handler_count); + src->handlers = NULL; + src->handler_count = 0; +} + +gboolean cbox_recording_source_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_recording_source *src = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + for (uint32_t i = 0; i < src->handler_count; i++) + { + if (!cbox_execute_on(fb, NULL, "/handler", "o", error, src->handlers[i])) + return FALSE; + } + return TRUE; + } + else + if (!strcmp(cmd->command, "/attach") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_objhdr *objhdr = CBOX_ARG_O(cmd, 0, src->scene, cbox_recorder, error); + if (!objhdr) + return FALSE; + struct cbox_recorder *rec = CBOX_H2O(objhdr); + return cbox_recording_source_attach(src, rec, error); + } + else + if (!strcmp(cmd->command, "/detach") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_objhdr *objhdr = CBOX_ARG_O(cmd, 0, src->scene, cbox_recorder, error); + if (!objhdr) + return FALSE; + struct cbox_recorder *rec = CBOX_H2O(objhdr); + return cbox_recording_source_detach(src, rec, error); + } + else + return cbox_set_command_error(error, cmd); +} + diff --git a/template/calfbox/recsrc.h b/template/calfbox/recsrc.h new file mode 100644 index 0000000..23cf498 --- /dev/null +++ b/template/calfbox/recsrc.h @@ -0,0 +1,64 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_RECSRC_H +#define CBOX_RECSRC_H + +#include "cmd.h" +#include "dom.h" + +struct cbox_recording_source; +struct cbox_rt; +struct cbox_engine; + +CBOX_EXTERN_CLASS(cbox_recorder) + +struct cbox_recorder +{ + CBOX_OBJECT_HEADER() + void *user_data; + struct cbox_command_target cmd_target; + + gboolean (*attach)(struct cbox_recorder *handler, struct cbox_recording_source *src, GError **error); + void (*record_block)(struct cbox_recorder *handler, const float **buffers, uint32_t offset, uint32_t numsamples); + gboolean (*detach)(struct cbox_recorder *handler, GError **error); + void (*destroy)(struct cbox_recorder *handler); +}; + +struct cbox_recording_source +{ + struct cbox_command_target cmd_target; + struct cbox_scene *scene; + + struct cbox_recorder **handlers; + uint32_t handler_count; + uint32_t max_numsamples; + int channels; +}; + +#define IS_RECORDING_SOURCE_CONNECTED(src) ((src).handler_count != 0) + +extern void cbox_recording_source_init(struct cbox_recording_source *src, struct cbox_scene *scene, uint32_t max_numsamples, int channels); +extern gboolean cbox_recording_source_attach(struct cbox_recording_source *src, struct cbox_recorder *rec, GError **error); +extern int cbox_recording_source_detach(struct cbox_recording_source *src, struct cbox_recorder *rec, GError **error); +extern void cbox_recording_source_push(struct cbox_recording_source *src, const float **buffers, uint32_t offset, uint32_t numsamples); +extern void cbox_recording_source_uninit(struct cbox_recording_source *src); + +extern struct cbox_recorder *cbox_recorder_new_stream(struct cbox_engine *engine, struct cbox_rt *rt, const char *filename); + +#endif diff --git a/template/calfbox/reverb.c b/template/calfbox/reverb.c new file mode 100644 index 0000000..d490d3c --- /dev/null +++ b/template/calfbox/reverb.c @@ -0,0 +1,355 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-float.h" +#include +#include +#include +#include +#include +#include +#include + +// The reverb structure is based on this article: +// http://www.spinsemi.com/knowledge_base/effects.html#Reverberation + +#define DELAY_BUFFER 1024 +#define ALLPASS_BUFFER 2048 + +struct allpass_param +{ + int delay; + float diffusion; +}; + +struct cbox_reverb_leg_params +{ + int delay_length; + int allpass_units; + struct allpass_param allpasses[0]; +}; + +struct cbox_reverb_leg +{ + struct cbox_reverb_leg_params *params; + float (*allpass_storage)[ALLPASS_BUFFER]; + float delay_storage[DELAY_BUFFER]; + struct cbox_onepolef_state filter_state; + float buffer[CBOX_BLOCK_SIZE]; +}; + +static struct cbox_reverb_leg_params *leg_params_new(int delay_length, int allpasses, const struct allpass_param *allpass_params) +{ + struct cbox_reverb_leg_params *p = malloc(sizeof(struct cbox_reverb_leg_params) + sizeof(struct allpass_param) * allpasses); + p->delay_length = delay_length; + p->allpass_units = allpasses; + + if (allpass_params) + { + for (int i = 0; i < allpasses; i++) + memcpy(&p->allpasses[i], &allpass_params[i], sizeof(struct allpass_param)); + } + + return p; +} + +static void cbox_reverb_leg_reset(struct cbox_reverb_leg *leg) +{ + cbox_onepolef_reset(&leg->filter_state); + int i; + for (int a = 0; a < leg->params->allpass_units; a++) + for (i = 0; i < ALLPASS_BUFFER; i++) + leg->allpass_storage[a][i] = 0.f; + for (i = 0; i < DELAY_BUFFER; i++) + leg->delay_storage[i] = 0.f; +} + +static void cbox_reverb_leg_init(struct cbox_reverb_leg *leg) +{ + leg->allpass_storage = malloc(leg->params->allpass_units * ALLPASS_BUFFER * sizeof(float)); + cbox_reverb_leg_reset(leg); +} + +static void cbox_reverb_leg_cleanup(struct cbox_reverb_leg *leg) +{ + free(leg->params); + free(leg->allpass_storage); +} + +#define MODULE_PARAMS reverb_params + +struct reverb_params +{ + float decay_time; + float wetamt; + float dryamt; + float lowpass, highpass; +}; + +struct reverb_state +{ + struct cbox_reverb_leg *legs; + int leg_count; + int total_time; +}; + +struct reverb_module +{ + struct cbox_module module; + + struct cbox_onepolef_coeffs filter_coeffs[2]; + struct reverb_params *params, *old_params; + struct reverb_state *state; + float gain; + int pos; +}; + +gboolean reverb_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct reverb_module *m = (struct reverb_module *)ct->user_data; + + EFFECT_PARAM("/wet_amt", "f", wetamt, double, dB2gain_simple, -100, 100) else + EFFECT_PARAM("/dry_amt", "f", dryamt, double, dB2gain_simple, -100, 100) else + EFFECT_PARAM("/decay_time", "f", decay_time, double, , 500, 5000) else + EFFECT_PARAM("/lowpass", "f", lowpass, double, , 30, 20000) else + EFFECT_PARAM("/highpass", "f", highpass, double, , 30, 20000) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/wet_amt", "f", error, gain2dB_simple(m->params->wetamt)) && + cbox_execute_on(fb, NULL, "/dry_amt", "f", error, gain2dB_simple(m->params->dryamt)) && + cbox_execute_on(fb, NULL, "/decay_time", "f", error, m->params->decay_time) && + cbox_execute_on(fb, NULL, "/lowpass", "f", error, m->params->lowpass) && + cbox_execute_on(fb, NULL, "/highpass", "f", error, m->params->highpass) && + CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void reverb_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct reverb_module *m = (struct reverb_module *)module; +} + +static void cbox_reverb_process_leg(struct reverb_module *m, int u) +{ + int pos; + int dv; + float *storage; + + struct reverb_state *state = m->state; + struct cbox_reverb_leg *b = &state->legs[u]; + int uprev = u ? (u - 1) : (state->leg_count - 1); + struct cbox_reverb_leg *bprev = &state->legs[uprev]; + + float gain = m->gain; + pos = m->pos; + storage = bprev->delay_storage; + float *buf = b->buffer; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + buf[i] += cbox_onepolef_process_sample(&b->filter_state, &m->filter_coeffs[u&1], storage[pos & (DELAY_BUFFER - 1)] * gain); + pos++; + } + + int units = b->params->allpass_units; + for (int a = 0; a < units; a++) + { + pos = m->pos; + storage = b->allpass_storage[a]; + dv = b->params->allpasses[a].delay; + float w = b->params->allpasses[a].diffusion; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float dry = buf[i]; + float out = dry; + + float delayed = storage[pos & (ALLPASS_BUFFER - 1)]; + + float feedback = sanef(out - w * delayed); + + buf[i] = sanef(feedback * w + delayed); + + storage[(pos + dv) & (ALLPASS_BUFFER - 1)] = feedback; + pos++; + } + } + pos = m->pos; + storage = b->delay_storage; + dv = b->params->delay_length - (u == 0 ? CBOX_BLOCK_SIZE : 0); + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + storage[(pos + dv) & (DELAY_BUFFER - 1)] = buf[i]; + pos++; + } +} + +void reverb_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct reverb_module *m = (struct reverb_module *)module; + struct reverb_params *p = m->params; + + float dryamt = p->dryamt; + float wetamt = p->wetamt; + struct reverb_state *s = m->state; + + if (p != m->old_params) + { + float tpdsr = 2.f * M_PI * m->module.srate_inv; + cbox_onepolef_set_lowpass(&m->filter_coeffs[0], p->lowpass * tpdsr); + cbox_onepolef_set_highpass(&m->filter_coeffs[1], p->highpass * tpdsr); + float rv = p->decay_time * m->module.srate / 1000; + m->gain = pow(0.001, s->total_time / (rv * s->leg_count / 2)); + m->old_params = p; + } + + int mid = s->leg_count >> 1; + memcpy(s->legs[0].buffer, inputs[0], CBOX_BLOCK_SIZE * sizeof(float)); + memcpy(s->legs[mid].buffer, inputs[1], CBOX_BLOCK_SIZE * sizeof(float)); + for (int u = 1; u < mid; u++) + { + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + s->legs[u].buffer[i] = 0.f; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + s->legs[u + mid].buffer[i] = 0.f; + } + + for (int u = 0; u < s->leg_count; u++) + cbox_reverb_process_leg(m, u); + + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + outputs[0][i] = inputs[0][i] * dryamt + s->legs[mid - 1].buffer[i] * wetamt; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + outputs[1][i] = inputs[1][i] * dryamt + s->legs[s->leg_count - 1].buffer[i] * wetamt; + m->pos += CBOX_BLOCK_SIZE; +} + +static void reverb_destroyfunc(struct cbox_module *module_) +{ + struct reverb_module *m = (struct reverb_module *)module_; + free(m->params); + for (int i = 0; i < m->state->leg_count; i++) + cbox_reverb_leg_cleanup(&m->state->legs[i]); + free(m->state->legs); + free(m->state); +} + +static struct reverb_state *create_reverb_state(int leg_count, ...) +{ + struct reverb_state *state = malloc(sizeof(struct reverb_state)); + state->leg_count = leg_count; + state->legs = malloc(state->leg_count * sizeof(struct cbox_reverb_leg)); + va_list va; + va_start(va, leg_count); + state->total_time = 0; + for (int u = 0; u < state->leg_count; u++) + { + int delay_length = va_arg(va, int); + int allpasses = va_arg(va, int); + state->total_time += delay_length; + state->legs[u].params = leg_params_new(delay_length, allpasses, NULL); + for (int i = 0; i < allpasses; i++) + { + struct allpass_param *ap = &state->legs[u].params->allpasses[i]; + ap->delay = va_arg(va, int); + ap->diffusion = va_arg(va, double); + state->total_time += ap->delay * ap->diffusion; // very rough approximation + } + } + va_end(va); + for (int u = 0; u < state->leg_count; u++) + cbox_reverb_leg_init(&state->legs[u]); + return state; +} + +MODULE_CREATE_FUNCTION(reverb) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct reverb_module *m = malloc(sizeof(struct reverb_module)); + CALL_MODULE_INIT(m, 2, 2, reverb); + m->module.process_event = reverb_process_event; + m->module.process_block = reverb_process_block; + m->pos = 0; + m->old_params = NULL; + m->params = malloc(sizeof(struct reverb_params)); + m->params->decay_time = cbox_config_get_float(cfg_section, "decay_time", 1000); + m->params->dryamt = cbox_config_get_gain_db(cfg_section, "dry_gain", 0.f); + m->params->wetamt = cbox_config_get_gain_db(cfg_section, "wet_gain", -6.f); + + m->state = create_reverb_state(4, + 133, 3, + 731, 0.45, + 873, 0.5, + 1215, 0.55, + 461, 3, + 1054, 0.5, + 1519, 0.5, + 973, 0.5, + 251, 3, + 617, 0.45, + 941, 0.5, + 1277, 0.55, + 379, 3, + 1119, 0.5, + 1477, 0.5, + 933, 0.5); + +#if 0 + m->state = create_reverb_state(2, + 133, 6, + 1573, 0.35, + 587, 0.35, + 921, 0.45, + 605, 0.5, + 1051, 0.45, + 397, 0.5, + 251, 6, + 1561, 0.35, + 594, 0.35, + 927, 0.55, + 611, 0.5, + 1147, 0.55, + 393, 0.5); +#endif + + m->params->lowpass = cbox_config_get_float(cfg_section, "lowpass", 8000.f); + m->params->highpass = cbox_config_get_float(cfg_section, "highpass", 35.f); + + return &m->module; +} + +struct cbox_module_keyrange_metadata reverb_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata reverb_controllers[] = { +}; + +DEFINE_MODULE(reverb, 2, 2) + diff --git a/template/calfbox/rt.c b/template/calfbox/rt.c new file mode 100644 index 0000000..f8d658e --- /dev/null +++ b/template/calfbox/rt.c @@ -0,0 +1,506 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "dom.h" +#include "engine.h" +#include "io.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "stm.h" +#include "seq.h" +#include "song.h" +#include +#include +#include + +CBOX_CLASS_DEFINITION_ROOT(cbox_rt) + +static void cbox_rt_process(void *user_data, struct cbox_io *io, uint32_t nframes); + +struct cbox_rt_cmd_instance +{ + struct cbox_rt_cmd_definition *definition; + void *user_data; + int *completed_ptr; // for synchronous commands only +}; + +static gboolean cbox_rt_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_rt *rt = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (rt->io) + { + GError *cerror = NULL; + if (cbox_io_get_disconnect_status(rt->io, &cerror)) + { + return cbox_execute_on(fb, NULL, "/audio_channels", "ii", error, rt->io->io_env.input_count, rt->io->io_env.output_count) && + cbox_execute_on(fb, NULL, "/state", "is", error, 1, "OK") && + CBOX_OBJECT_DEFAULT_STATUS(rt, fb, error); + } + else + { + return cbox_execute_on(fb, NULL, "/audio_channels", "ii", error, rt->io->io_env.input_count, rt->io->io_env.output_count) && + cbox_execute_on(fb, NULL, "/state", "is", error, -1, cerror ? cerror->message : "Unknown error") && + CBOX_OBJECT_DEFAULT_STATUS(rt, fb, error); + } + } + else + return cbox_execute_on(fb, NULL, "/audio_channels", "ii", error, 0, 2) && + cbox_execute_on(fb, NULL, "/state", "is", error, 0, "Offline") && + CBOX_OBJECT_DEFAULT_STATUS(rt, fb, error); + } + else if (!strcmp(cmd->command, "/cycle") && !strcmp(cmd->arg_types, "")) + { + if (rt->io && !cbox_io_get_disconnect_status(rt->io, NULL)) + { + return cbox_io_cycle(rt->io, fb, error); + } + else + { + if (rt->io) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Already connected"); + else + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot cycle connection in off-line mode"); + return FALSE; + } + } + else if (!strcmp(cmd->command, "/flush") && !cmd->arg_types[0]) { + cbox_rt_handle_cmd_queue(rt); + return TRUE; + } + else if (!strncmp(cmd->command, "/engine/", 8)) + return cbox_execute_sub(&rt->engine->cmd_target, fb, cmd, cmd->command + 7, error); + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +struct cbox_rt *cbox_rt_new(struct cbox_document *doc) +{ + struct cbox_rt *rt = malloc(sizeof(struct cbox_rt)); + CBOX_OBJECT_HEADER_INIT(rt, cbox_rt, doc); + rt->rb_execute = cbox_fifo_new(sizeof(struct cbox_rt_cmd_instance) * RT_CMD_QUEUE_ITEMS); + rt->rb_cleanup = cbox_fifo_new(sizeof(struct cbox_rt_cmd_instance) * RT_CMD_QUEUE_ITEMS * 2); + rt->io = NULL; + rt->engine = NULL; + rt->started = FALSE; + rt->disconnected = FALSE; + rt->io_env.srate = 0; + rt->io_env.buffer_size = 0; + rt->io_env.input_count = 0; + rt->io_env.output_count = 0; + + cbox_command_target_init(&rt->cmd_target, cbox_rt_process_cmd, rt); + CBOX_OBJECT_REGISTER(rt); + cbox_document_set_service(doc, "rt", &rt->_obj_hdr); + + return rt; +} + +struct cbox_objhdr *cbox_rt_newfunc(struct cbox_class *class_ptr, struct cbox_document *doc) +{ + return NULL; +} + +void cbox_rt_destroyfunc(struct cbox_objhdr *obj_ptr) +{ + struct cbox_rt *rt = (void *)obj_ptr; + cbox_fifo_destroy(rt->rb_execute); + cbox_fifo_destroy(rt->rb_cleanup); + + free(rt); +} + +static void cbox_rt_on_disconnected(void *user_data) +{ + struct cbox_rt *rt = user_data; + rt->disconnected = TRUE; +} + +static void cbox_rt_on_reconnected(void *user_data) +{ + struct cbox_rt *rt = user_data; + rt->disconnected = FALSE; +} + +static void cbox_rt_on_midi_outputs_changed(void *user_data) +{ + struct cbox_rt *rt = user_data; + if (rt->engine) + { + cbox_engine_update_song_playback(rt->engine); + cbox_engine_update_output_connections(rt->engine); + } +} + +static void cbox_rt_on_midi_inputs_changed(void *user_data) +{ + struct cbox_rt *rt = user_data; + if (rt->engine) + cbox_engine_update_input_connections(rt->engine); +} + +static void cbox_rt_get_transport_data(void *user_data, gboolean explicit_pos, uint32_t time_samples, struct cbox_transport_position *tp) +{ + struct cbox_rt *rt = user_data; + if (rt->engine) + { + struct cbox_master *master = rt->engine->master; + uint32_t time_ppqn; + if (explicit_pos) + time_ppqn = cbox_master_samples_to_ppqn(master, time_samples); + else + time_ppqn = master->spb ? master->spb->song_pos_ppqn : 0; + struct cbox_bbt bbt; + struct cbox_master_track_item mti; + cbox_master_ppqn_to_bbt(master, &bbt, time_ppqn, &mti); + tp->bar = bbt.bar; + tp->beat = bbt.beat; + tp->tick = bbt.tick; + tp->ticks_per_beat = master->ppqn_factor * 4 / mti.timesig_denom; + tp->bar_start_tick = 0; + tp->timesig_num = mti.timesig_num; + tp->timesig_denom = mti.timesig_denom; + tp->tempo = mti.tempo; + } +} + +void cbox_rt_on_update_io_env(struct cbox_rt *rt) +{ + if (rt->engine) + { + cbox_io_env_copy(&rt->engine->io_env, &rt->io_env); + cbox_master_set_sample_rate(rt->engine->master, rt->io_env.srate); + } +} + +void cbox_rt_set_io(struct cbox_rt *rt, struct cbox_io *io) +{ + assert(!rt->started); + rt->io = io; + if (io) + { + cbox_io_env_copy(&rt->io_env, &io->io_env); + cbox_rt_on_update_io_env(rt); + } + else + { + cbox_io_env_clear(&rt->io_env); + } +} + +void cbox_rt_set_offline(struct cbox_rt *rt, int sample_rate, int buffer_size) +{ + assert(!rt->started); + rt->io = NULL; + rt->io_env.srate = sample_rate; + rt->io_env.buffer_size = buffer_size; + rt->io_env.input_count = 0; + rt->io_env.output_count = 2; + cbox_rt_on_update_io_env(rt); +} + +void cbox_rt_on_started(void *user_data) +{ + struct cbox_rt *rt = user_data; + rt->started = 1; +} + +void cbox_rt_on_stopped(void *user_data) +{ + struct cbox_rt *rt = user_data; + rt->started = 0; +} + +gboolean cbox_rt_on_transport_sync(void *user_data, enum cbox_transport_state state, uint32_t frame) +{ + struct cbox_rt *rt = user_data; + if (!rt->engine) + return TRUE; + return cbox_engine_on_transport_sync(rt->engine, state, frame); +} + +gboolean cbox_rt_on_tempo_sync(void *user_data, double tempo) +{ + struct cbox_rt *rt = user_data; + if (rt->engine) + cbox_engine_on_tempo_sync(rt->engine, tempo); + return TRUE; +} + +void cbox_rt_start(struct cbox_rt *rt, struct cbox_command_target *fb) +{ + if (rt->io) + { + rt->cbs = calloc(1, sizeof(struct cbox_io_callbacks)); + rt->cbs->user_data = rt; + rt->cbs->process = cbox_rt_process; + rt->cbs->on_started = cbox_rt_on_started; + rt->cbs->on_stopped = cbox_rt_on_stopped; + rt->cbs->on_disconnected = cbox_rt_on_disconnected; + rt->cbs->on_reconnected = cbox_rt_on_reconnected; + rt->cbs->on_midi_inputs_changed = cbox_rt_on_midi_inputs_changed; + rt->cbs->on_midi_outputs_changed = cbox_rt_on_midi_outputs_changed; + rt->cbs->on_transport_sync = cbox_rt_on_transport_sync; + rt->cbs->on_tempo_sync = cbox_rt_on_tempo_sync; + rt->cbs->get_transport_data = cbox_rt_get_transport_data; + + assert(!rt->started); + cbox_io_start(rt->io, rt->cbs, fb); + assert(rt->started); + } +} + +void cbox_rt_stop(struct cbox_rt *rt) +{ + if (rt->io) + { + assert(rt->started); + cbox_io_stop(rt->io); + free(rt->cbs); + rt->cbs = NULL; + assert(!rt->started); + } +} + +void cbox_rt_handle_cmd_queue(struct cbox_rt *rt) +{ + struct cbox_rt_cmd_instance cmd; + + while(cbox_fifo_read_atomic(rt->rb_cleanup, &cmd, sizeof(cmd))) + { + assert(!cmd.completed_ptr); + cmd.definition->cleanup(cmd.user_data); + } +} + +static void wait_write_space(struct cbox_fifo *rb) +{ + int t = 0; + while (cbox_fifo_writespace(rb) < sizeof(struct cbox_rt_cmd_instance)) + { + // wait until some space frees up in the execute queue + usleep(1000); + t++; + if (t >= 1000) + { + fprintf(stderr, "Execute queue full, waiting...\n"); + t = 0; + } + } +} + +void cbox_rt_execute_cmd_sync(struct cbox_rt *rt, struct cbox_rt_cmd_definition *def, void *user_data) +{ + struct cbox_rt_cmd_instance cmd; + + if (def->prepare) + if (def->prepare(user_data)) + return; + + // No realtime thread - do it all in the main thread + if (!rt || !rt->started || rt->disconnected) + { + while (!def->execute(user_data)) + ; + if (def->cleanup) + def->cleanup(user_data); + return; + } + + int completed = 0; + memset(&cmd, 0, sizeof(cmd)); + cmd.definition = def; + cmd.user_data = user_data; + cmd.completed_ptr = &completed; + + wait_write_space(rt->rb_execute); + cbox_fifo_write_atomic(rt->rb_execute, &cmd, sizeof(cmd)); + do + { + if (completed) + { + if (def->cleanup) + def->cleanup(user_data); + break; + } + struct cbox_rt_cmd_instance cmd2; + + if (!cbox_fifo_read_atomic(rt->rb_cleanup, &cmd2, sizeof(cmd2))) + { + // still no result in cleanup queue - wait + usleep(10000); + continue; + } + // async command or something from outer layer - clean it up + cmd2.definition->cleanup(cmd2.user_data); + } while(1); +} + +int cbox_rt_execute_cmd_async(struct cbox_rt *rt, struct cbox_rt_cmd_definition *def, void *user_data) +{ + struct cbox_rt_cmd_instance cmd = { def, user_data, NULL }; + + if (def->prepare) + { + int error = def->prepare(user_data); + if (error) + return error; + } + // No realtime thread - do it all in the main thread + if (!rt || !rt->started || rt->disconnected) + { + while (!def->execute(user_data)) + ; + if (def->cleanup) + def->cleanup(user_data); + } else { + wait_write_space(rt->rb_execute); + cbox_fifo_write_atomic(rt->rb_execute, &cmd, sizeof(cmd)); + // will be cleaned up by next sync call or by cbox_rt_cmd_handle_queue + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_rt_process(void *user_data, struct cbox_io *io, uint32_t nframes) +{ + struct cbox_rt *rt = user_data; + if (rt->engine) + cbox_engine_process(rt->engine, io, nframes, io->output_buffers, io->io_env.output_count); + else + cbox_rt_handle_rt_commands(rt); +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_rt_handle_rt_commands(struct cbox_rt *rt) +{ + struct cbox_rt_cmd_instance cmd; + + // Process command queue, needs engine's MIDI aux buf to be initialised to work + int cost = 0; + while(cost < RT_MAX_COST_PER_CALL && cbox_fifo_peek(rt->rb_execute, &cmd, sizeof(cmd))) + { + int result = (cmd.definition->execute)(cmd.user_data); + if (!result) + break; + cost += result; + cbox_fifo_consume(rt->rb_execute, sizeof(cmd)); + if (cmd.completed_ptr) + *cmd.completed_ptr = 1; + else if (cmd.definition->cleanup) + { + gboolean success = cbox_fifo_write_atomic(rt->rb_cleanup, (const char *)&cmd, sizeof(cmd)); + if (!success) + g_error("Clean-up FIFO full. Main thread deadlock?"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////// + +#define cbox_rt_swap_pointers_args(ARG) ARG(void **, ptr) ARG(void *, new_value) + +DEFINE_RT_FUNC(void *, cbox_rt, rt, cbox_rt_swap_pointers) +{ + void *old_value = *ptr; + *ptr = new_value; + return old_value; +} + +#define cbox_rt_swap_pointers_into_args(ARG) ARG(void **, ptr) ARG(void *, new_value) ARG(void **, old_value_p) + +DEFINE_RT_VOID_FUNC(cbox_rt, rt, cbox_rt_swap_pointers_into) +{ + *old_value_p = *ptr; + *ptr = new_value; +} + +#define cbox_rt_swap_pointers_and_update_count_args(ARG) ARG(void **, ptr) ARG(void *, new_value) ARG(uint32_t *, pcount) ARG(uint32_t, new_count) + +DEFINE_RT_FUNC(void *, cbox_rt, rt, cbox_rt_swap_pointers_and_update_count) +{ + void *old_value = *ptr; + *ptr = new_value; + if (pcount) + *pcount = new_count; + return old_value; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +void cbox_rt_array_insert(struct cbox_rt *rt, void ***ptr, uint32_t *pcount, int index, void *new_value) +{ + assert(index >= -1); + assert(index == -1 || (uint32_t)index <= *pcount); + assert(*pcount < (1U << 31)); + void **new_array = stm_array_clone_insert(*ptr, *pcount, index, new_value); + free(cbox_rt_swap_pointers_and_update_count(rt, (void **)ptr, new_array, pcount, *pcount + 1)); +} + +void *cbox_rt_array_remove(struct cbox_rt *rt, void ***ptr, uint32_t *pcount, int index) +{ + if (index == -1) + index = *pcount - 1; + assert(index >= 0); + assert((uint32_t)index < *pcount); + assert(*pcount < (1U << 31)); + void *p = (*ptr)[index]; + void **new_array = stm_array_clone_remove(*ptr, *pcount, index); + free(cbox_rt_swap_pointers_and_update_count(rt, (void **)ptr, new_array, pcount, *pcount - 1)); + return p; +} + +gboolean cbox_rt_array_remove_by_value(struct cbox_rt *rt, void ***ptr, uint32_t *pcount, void *value_to_remove) +{ + for (uint32_t i = 0; i < *pcount; i++) + { + if ((*ptr)[i] == value_to_remove) + { + cbox_rt_array_remove(rt, ptr, pcount, i); + return TRUE; + } + } + return FALSE; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_merger *cbox_rt_get_midi_output(struct cbox_rt *rt, struct cbox_uuid *uuid) +{ + if (rt->engine) + { + struct cbox_midi_merger *merger = cbox_engine_get_midi_output(rt->engine, uuid); + if (merger) + return merger; + } + if (!rt->io) + return NULL; + + struct cbox_midi_output *midiout = cbox_io_get_midi_output(rt->io, NULL, uuid); + return midiout ? &midiout->merger : NULL; +} + +//////////////////////////////////////////////////////////////////////////////////////// + diff --git a/template/calfbox/rt.h b/template/calfbox/rt.h new file mode 100644 index 0000000..11a9c7c --- /dev/null +++ b/template/calfbox/rt.h @@ -0,0 +1,208 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_RT_H +#define CBOX_RT_H + +#include + +#include "cmd.h" +#include "dom.h" +#include "fifo.h" +#include "ioenv.h" +#include "midi.h" +#include "mididest.h" + +#define RT_CMD_QUEUE_ITEMS 1024 +#define RT_MAX_COST_PER_CALL 100 + +struct cbox_instruments; +struct cbox_scene; +struct cbox_io; +struct cbox_midi_pattern; +struct cbox_song; +struct cbox_rt_cmd_instance; + +struct cbox_rt_cmd_definition +{ + int (*prepare)(void *user_data); // non-zero to skip the whole thing + int (*execute)(void *user_data); // returns cost + void (*cleanup)(void *user_data); +}; + +CBOX_EXTERN_CLASS(cbox_rt) + +struct cbox_rt +{ + CBOX_OBJECT_HEADER() + + struct cbox_io *io; + struct cbox_io_callbacks *cbs; + + struct cbox_fifo *rb_execute, *rb_cleanup; + + struct cbox_command_target cmd_target; + int started, disconnected; + struct cbox_io_env io_env; + struct cbox_engine *engine; +}; + +extern struct cbox_rt *cbox_rt_new(struct cbox_document *doc); + +extern void cbox_rt_set_io(struct cbox_rt *rt, struct cbox_io *io); +extern void cbox_rt_set_offline(struct cbox_rt *rt, int sample_rate, int buffer_size); +extern void cbox_rt_start(struct cbox_rt *rt, struct cbox_command_target *fb); +extern void cbox_rt_handle_cmd_queue(struct cbox_rt *rt); +// This one should be called from RT thread process function to execute the queued RT commands +extern void cbox_rt_handle_rt_commands(struct cbox_rt *rt); +extern void cbox_rt_stop(struct cbox_rt *rt); + +// Those are for calling from the main thread. I will add a RT-thread version later. +extern void cbox_rt_execute_cmd_sync(struct cbox_rt *rt, struct cbox_rt_cmd_definition *cmd, void *user_data); +extern int cbox_rt_execute_cmd_async(struct cbox_rt *rt, struct cbox_rt_cmd_definition *cmd, void *user_data); +extern void *cbox_rt_swap_pointers(struct cbox_rt *rt, void **ptr, void *new_value); +extern void cbox_rt_swap_pointers_into(struct cbox_rt *rt, void **ptr, void *new_value, void **old_value_ptr); +extern void *cbox_rt_swap_pointers_and_update_count(struct cbox_rt *rt, void **ptr, void *new_value, uint32_t *pcount, uint32_t new_count); + +extern void cbox_rt_array_insert(struct cbox_rt *rt, void ***ptr, uint32_t *pcount, int index, void *new_value); +extern void *cbox_rt_array_remove(struct cbox_rt *rt, void ***ptr, uint32_t *pcount, int index); +extern gboolean cbox_rt_array_remove_by_value(struct cbox_rt *rt, void ***ptr, uint32_t *pcount, void *value_to_remove); +extern struct cbox_midi_merger *cbox_rt_get_midi_output(struct cbox_rt *rt, struct cbox_uuid *uuid); + +/////////////////////////////////////////////////////////////////////////////// + +#define GET_RT_FROM_cbox_rt(ptr) (ptr) +#define RT_FUNC_ARG_MEMBER(type, name) type name; +#define RT_FUNC_ARG_LIST(type, name) , type name +#define RT_FUNC_ARG_PASS(type, name) _args.name = name; +#define RT_FUNC_ARG_PASS2(type, name) , _args.name +#define RT_FUNC_ARG_PASS3(type, name) , _args->name +#define RT_FUNC_ARG_PASS4(type, name) , name +#define RT_FUNC_ARG_PASS5(type, name) _args->name = name; +#define DEFINE_RT_FUNC(restype, objtype, argname, name) \ + struct rt_function_args_##name { \ + struct objtype *_obj; \ + restype _result; \ + name##_args(RT_FUNC_ARG_MEMBER) \ + }; \ + static restype RT_IMPL_##name(struct objtype *_obj, int *_cost name##_args(RT_FUNC_ARG_LIST)); \ + int exec_##name(void *user_data) { \ + int cost = 1; \ + struct rt_function_args_##name *_args = user_data;\ + _args->_result = RT_IMPL_##name(_args->_obj, &cost name##_args(RT_FUNC_ARG_PASS3)); \ + return cost; \ + } \ + restype name(struct objtype *_obj name##_args(RT_FUNC_ARG_LIST)) \ + { \ + struct cbox_rt *rt = GET_RT_FROM_##objtype(_obj); \ + if (rt) { \ + struct rt_function_args_##name _args; \ + static struct cbox_rt_cmd_definition _cmd = { .prepare = NULL, .execute = exec_##name, .cleanup = NULL }; \ + _args._obj = _obj; \ + name##_args(RT_FUNC_ARG_PASS) \ + cbox_rt_execute_cmd_sync(rt, &_cmd, &_args); \ + return _args._result; \ + } else { \ + int cost; \ + restype _result; \ + do { \ + cost = 1; \ + _result = RT_IMPL_##name(_obj, &cost name##_args(RT_FUNC_ARG_PASS4)); \ + } while(!cost); \ + return _result; \ + } \ + } \ + restype RT_IMPL_##name(struct objtype *argname, int *_cost name##_args(RT_FUNC_ARG_LIST)) + +#define DEFINE_RT_VOID_FUNC(objtype, argname, name) \ + struct rt_function_args_##name { \ + struct objtype *_obj; \ + name##_args(RT_FUNC_ARG_MEMBER) \ + }; \ + static void RT_IMPL_##name(struct objtype *_obj, int *_cost name##_args(RT_FUNC_ARG_LIST)); \ + int exec_##name(void *user_data) { \ + int cost = 1; \ + struct rt_function_args_##name *_args = user_data;\ + RT_IMPL_##name(_args->_obj, &cost name##_args(RT_FUNC_ARG_PASS3)); \ + return cost; \ + } \ + void name(struct objtype *_obj name##_args(RT_FUNC_ARG_LIST)) \ + { \ + struct cbox_rt *rt = GET_RT_FROM_##objtype(_obj); \ + if (rt) { \ + struct rt_function_args_##name _args = { ._obj = _obj }; \ + static struct cbox_rt_cmd_definition _cmd = { .prepare = NULL, .execute = exec_##name, .cleanup = NULL }; \ + name##_args(RT_FUNC_ARG_PASS) \ + cbox_rt_execute_cmd_sync(rt, &_cmd, &_args); \ + } else { \ + int cost; \ + do { \ + cost = 1; \ + RT_IMPL_##name(_obj, &cost name##_args(RT_FUNC_ARG_PASS4)); \ + } while(!cost); \ + } \ + } \ + void RT_IMPL_##name(struct objtype *argname, int *_cost name##_args(RT_FUNC_ARG_LIST)) + +#define DEFINE_ASYNC_RT_FUNC(objtype, argname, name) \ + struct rt_function_args_##name { \ + struct objtype *_obj; \ + name##_args(RT_FUNC_ARG_MEMBER) \ + }; \ + static void RT_IMPL_##name(struct objtype *_obj, int *_cost name##_args(RT_FUNC_ARG_LIST)); \ + static int prepare_##name(void *user_data); \ + static void cleanup_##name(void *user_data); \ + static int exec_##name(void *user_data) { \ + int cost = 1; \ + struct rt_function_args_##name *_args = user_data;\ + RT_IMPL_##name(_args->_obj, &cost name##_args(RT_FUNC_ARG_PASS3)); \ + return cost; \ + } \ + void name(struct objtype *_obj name##_args(RT_FUNC_ARG_LIST)) \ + { \ + struct cbox_rt *rt = GET_RT_FROM_##objtype(_obj); \ + struct rt_function_args_##name *_args = malloc(sizeof(struct rt_function_args_##name)); \ + _args->_obj = _obj; \ + name##_args(RT_FUNC_ARG_PASS5) \ + static struct cbox_rt_cmd_definition _cmd = { .prepare = prepare_##name, .execute = exec_##name, .cleanup = cleanup_##name }; \ + if (cbox_rt_execute_cmd_async(rt, &_cmd, _args)) \ + free(_args); \ + } \ + void RT_IMPL_##name(struct objtype *argname, int *_cost name##_args(RT_FUNC_ARG_LIST)) + +#define ASYNC_PREPARE_FUNC(objtype, argname, name) \ + static int PREPARE_IMPL_##name(struct objtype *argname, struct rt_function_args_##name *args); \ + int prepare_##name(void *user_data) {\ + struct rt_function_args_##name *_args = user_data;\ + return PREPARE_IMPL_##name(_args->_obj, _args); \ + } \ + int PREPARE_IMPL_##name(struct objtype *argname, struct rt_function_args_##name *args) + +#define ASYNC_CLEANUP_FUNC(objtype, argname, name) \ + static void CLEANUP_IMPL_##name(struct objtype *argname, struct rt_function_args_##name *args); \ + void cleanup_##name(void *user_data) {\ + struct rt_function_args_##name *_args = user_data;\ + CLEANUP_IMPL_##name(_args->_obj, _args); \ + free(_args); \ + } \ + void CLEANUP_IMPL_##name(struct objtype *argname, struct rt_function_args_##name *args) + +#define RT_CALL_AGAIN_LATER() ((*_cost) = 0) +#define RT_SET_COST(n) ((*_cost) = (n)) + +#endif diff --git a/template/calfbox/sampler.c b/template/calfbox/sampler.c new file mode 100644 index 0000000..b3bdf92 --- /dev/null +++ b/template/calfbox/sampler.c @@ -0,0 +1,764 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "dspmath.h" +#include "engine.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_impl.h" +#include "sfzloader.h" +#include "stm.h" +#include +#include +#include +#include +#include +#include +#include +#include + +float sampler_sine_wave[2049]; + +GQuark cbox_sampler_error_quark() +{ + return g_quark_from_string("cbox-sampler-error-quark"); +} + +static void sampler_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs); +static void sampler_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len); +static void sampler_destroyfunc(struct cbox_module *module); + +void sampler_steal_voice(struct sampler_module *m) +{ + int max_age = 0; + struct sampler_voice *voice_found = NULL; + for (int i = 0; i < 16; i++) + { + FOREACH_VOICE(m->channels[i].voices_running, v) + { + if (v->amp_env.cur_stage == 15) + continue; + int age = m->serial_no - v->serial_no; + if (v->gen.loop_start == (uint32_t)-1) + age += (int)((v->gen.bigpos >> 32) * 100.0 / v->gen.cur_sample_end); + else + if (v->released) + age += 10; + if (age > max_age) + { + max_age = age; + voice_found = v; + } + } + } + if (voice_found) + { + voice_found->released = 1; + cbox_envelope_go_to(&voice_found->amp_env, 15); + } +} + +static inline float clip01(float v) +{ + if (v < 0.f) + return 0; + if (v > 1.f) + return 1; + return v; +} + +void sampler_create_voice_from_prevoice(struct sampler_module *m, struct sampler_prevoice *pv) +{ + if (!m->voices_free) + return; + struct sampler_released_groups exgroups; + sampler_released_groups_init(&exgroups); + sampler_voice_start(m->voices_free, pv->channel, pv->layer_data, pv->note, pv->vel, &exgroups); + if (exgroups.low_groups || exgroups.group_count) + sampler_channel_release_groups(pv->channel, pv->note, &exgroups); +} + +void sampler_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct sampler_module *m = (struct sampler_module *)module; + + int active_prevoices[16]; + uint32_t active_prevoices_mask = 0; + FOREACH_PREVOICE(m->prevoices_running, pv) + { + uint32_t c = pv->channel - m->channels; + active_prevoices[c] = (active_prevoices_mask >> c) & 1 ? 1 + active_prevoices[c] : 1; + active_prevoices_mask |= 1 << c; + if (sampler_prevoice_process(pv, m)) + { + sampler_prevoice_unlink(&m->prevoices_running, pv); + sampler_create_voice_from_prevoice(m, pv); + sampler_prevoice_link(&m->prevoices_free, pv); + } + } + + for (int c = 0; c < m->output_pairs + m->aux_pairs; c++) + { + int oo = 2 * c; + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + outputs[oo][i] = outputs[oo + 1][i] = 0.f; + } + + int vcount = 0, vrel = 0, pvcount = 0; + for (int i = 0; i < 16; i++) + { + int cvcount = 0; + FOREACH_VOICE(m->channels[i].voices_running, v) + { + sampler_voice_process(v, m, outputs); + + if (v->amp_env.cur_stage == 15) + vrel++; + cvcount++; + } + int cpvcount = (active_prevoices_mask >> i) & 1 ? active_prevoices[i] : 0; + m->channels[i].active_voices = cvcount; + m->channels[i].active_prevoices = cpvcount; + vcount += cvcount; + pvcount += cpvcount; + } + m->active_voices = vcount; + m->active_prevoices = pvcount; + if(vcount - vrel > m->max_voices + 1) + sampler_steal_voice(m); + if(vcount - vrel > m->max_voices) + sampler_steal_voice(m); + m->serial_no++; + m->current_time += CBOX_BLOCK_SIZE; +} + +void sampler_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + struct sampler_module *m = (struct sampler_module *)module; + if (len > 0) + { + int cmd = data[0] >> 4; + int chn = data[0] & 15; + struct sampler_channel *c = &m->channels[chn]; + switch(cmd) + { + case 8: + sampler_channel_stop_note(c, data[1], data[2], FALSE); + break; + + case 9: + if (data[2] > 0) + sampler_channel_start_note(c, data[1], data[2], FALSE); + else + sampler_channel_stop_note(c, data[1], data[2], FALSE); + break; + + case 10: + c->last_polyaft = data[2]; + // Lazy clearing + if (!(c->poly_pressure_mask & (1 << (data[1] >> 2)))) + { + // Clear the group of 4 + memset(c->poly_pressure + (data[1] & ~3), 0, 4); + c->poly_pressure_mask |= 1 << (data[1] >> 2); + } + c->poly_pressure[data[1]] = data[2]; + // XXXKF add a new opcode for cymbal chokes via poly pressure + // if (data[2] == 127) + // sampler_channel_stop_note(c, data[1], data[2], TRUE); + break; + + case 11: + sampler_channel_process_cc(c, data[1], data[2]); + break; + + case 12: + sampler_channel_program_change(c, data[1]); + break; + + case 13: + c->last_chanaft = data[1]; + break; + + case 14: + c->pitchwheel = data[1] + 128 * data[2] - 8192; + break; + + } + } +} + +static int get_first_free_program_no(struct sampler_module *m) +{ + int prog_no = -1; + gboolean found; + + // XXXKF this has a N-squared complexity - but I'm not seeing + // this being used with more than 10 programs at the same time + // in the near future + do { + prog_no++; + found = FALSE; + for (uint32_t i = 0; i < m->program_count; i++) + { + if (m->programs[i]->prog_no == prog_no) + { + found = TRUE; + break; + } + } + } while(found); + + return prog_no; +} + +static int find_program(struct sampler_module *m, int prog_no) +{ + for (uint32_t i = 0; i < m->program_count; i++) + { + if (m->programs[i]->prog_no == prog_no) + return i; + } + return -1; +} + +struct release_program_voices_data +{ + struct sampler_module *module; + + struct sampler_program *old_pgm, *new_pgm; + uint16_t channels_to_wait_for; +}; + +static int release_program_voices_execute(void *data) +{ + struct release_program_voices_data *rpv = data; + struct sampler_module *m = rpv->module; + int finished = 1; + + FOREACH_PREVOICE(m->prevoices_running, pv) + { + if (pv->channel->program == rpv->old_pgm) + { + sampler_prevoice_unlink(&m->prevoices_running, pv); + sampler_prevoice_link(&m->prevoices_free, pv); + } + } + for (int i = 0; i < 16; i++) + { + uint16_t mask = 1 << i; + struct sampler_channel *c = &m->channels[i]; + if (c->program == rpv->old_pgm || c->program == NULL) + { + sampler_channel_set_program_RT(c, rpv->new_pgm); + rpv->channels_to_wait_for |= mask; + } + if (rpv->channels_to_wait_for & mask) + { + FOREACH_VOICE(c->voices_running, v) + { + if (m->deleting || !m->module.rt) + { + sampler_voice_inactivate(v, TRUE); + continue; + } + // This is a new voice, started after program change, so it + // should not be terminated and waited for. + if (v->program == rpv->new_pgm) + continue; + // The voice is still going, so repeat until it fades out + finished = 0; + // If not in final fadeout stage, force final fadeout. + if (v->amp_env.cur_stage != 15) + { + v->released = 1; + cbox_envelope_go_to(&v->amp_env, 15); + } + } + } + } + + return finished; +} + +static void swap_program(struct sampler_module *m, int index, struct sampler_program *pgm, gboolean delete_old) +{ + static struct cbox_rt_cmd_definition release_program_voices = { NULL, release_program_voices_execute, NULL }; + + struct sampler_program *old_program = NULL; + if (pgm) + old_program = cbox_rt_swap_pointers(m->module.rt, (void **)&m->programs[index], pgm); + else + old_program = cbox_rt_array_remove(m->module.rt, (void ***)&m->programs, &m->program_count, index); + + struct release_program_voices_data data = {m, old_program, pgm, 0}; + + cbox_rt_execute_cmd_sync(m->module.rt, &release_program_voices, &data); + + if (delete_old && old_program) + CBOX_DELETE(old_program); +} + +static void select_initial_program(struct sampler_module *m) +{ + static struct cbox_rt_cmd_definition release_program_voices = { NULL, release_program_voices_execute, NULL }; + struct release_program_voices_data data = {m, NULL, m->programs[0], 0}; + cbox_rt_execute_cmd_sync(m->module.rt, &release_program_voices, &data); +} + +void sampler_register_program(struct sampler_module *m, struct sampler_program *pgm) +{ + struct sampler_program **programs = malloc(sizeof(struct sampler_program *) * (m->program_count + 1)); + memcpy(programs, m->programs, sizeof(struct sampler_program *) * m->program_count); + programs[m->program_count] = pgm; + free(cbox_rt_swap_pointers_and_update_count(m->module.rt, (void **)&m->programs, programs, &m->program_count, m->program_count + 1)); + if (m->program_count == 1U) + select_initial_program(m); +} + +static gboolean load_program_at(struct sampler_module *m, const char *cfg_section, const char *name, int prog_no, struct sampler_program **ppgm, GError **error) +{ + struct sampler_program *pgm = NULL; + int index = find_program(m, prog_no); + pgm = sampler_program_new_from_cfg(m, cfg_section, name, prog_no, error); + if (!pgm) + return FALSE; + + if (index != -1) + { + swap_program(m, index, pgm, TRUE); + return TRUE; + } + + sampler_register_program(m, pgm); + if (ppgm) + *ppgm = pgm; + return TRUE; +} + +void sampler_unselect_program(struct sampler_module *m, struct sampler_program *prg) +{ + // Ensure no new notes are played on that program + prg->deleting = TRUE; + // Remove from the list of available programs, so that it cannot be selected again + for (uint32_t i = 0; i < m->program_count; i++) + { + if (m->programs[i] == prg) + swap_program(m, i, NULL, FALSE); + } +} + +static gboolean load_from_string(struct sampler_module *m, const char *sample_dir, const char *sfz_data, const char *name, int prog_no, struct sampler_program **ppgm, GError **error) +{ + int index = find_program(m, prog_no); + struct sampler_program *pgm = sampler_program_new(m, prog_no, name, NULL, sample_dir, error); + if (!pgm) + return FALSE; + pgm->source_file = g_strdup("string"); + if (!sampler_module_load_program_sfz(m, pgm, sfz_data, TRUE, error)) + { + free(pgm); + return FALSE; + } + + if (index != -1) + { + swap_program(m, index, pgm, TRUE); + if (ppgm) + *ppgm = pgm; + return TRUE; + } + + struct sampler_program **programs = calloc((m->program_count + 1), sizeof(struct sampler_program *)); + memcpy(programs, m->programs, sizeof(struct sampler_program *) * m->program_count); + programs[m->program_count] = pgm; + if (ppgm) + *ppgm = pgm; + free(cbox_rt_swap_pointers_and_update_count(m->module.rt, (void **)&m->programs, programs, &m->program_count, m->program_count + 1)); + if (m->program_count == 1) + select_initial_program(m); + return TRUE; +} + +gboolean sampler_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct sampler_module *m = (struct sampler_module *)ct->user_data; + + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (int i = 0; i < 16; i++) + { + struct sampler_channel *channel = &m->channels[i]; + gboolean result; + if (channel->program) + result = cbox_execute_on(fb, NULL, "/patch", "iis", error, i + 1, channel->program->prog_no, channel->program->name); + else + result = cbox_execute_on(fb, NULL, "/patch", "iis", error, i + 1, -1, ""); + if (!result) + return FALSE; + if (!(cbox_execute_on(fb, NULL, "/channel_voices", "ii", error, i + 1, channel->active_voices) && + cbox_execute_on(fb, NULL, "/channel_prevoices", "ii", error, i + 1, channel->active_prevoices) && + cbox_execute_on(fb, NULL, "/output", "ii", error, i + 1, channel->output_shift) && + cbox_execute_on(fb, NULL, "/volume", "ii", error, i + 1, sampler_channel_addcc(channel, 7)) && + cbox_execute_on(fb, NULL, "/pan", "ii", error, i + 1, sampler_channel_addcc(channel, 10)))) + return FALSE; + } + + return cbox_execute_on(fb, NULL, "/active_voices", "i", error, m->active_voices) && + cbox_execute_on(fb, NULL, "/active_prevoices", "i", error, m->active_prevoices) && + cbox_execute_on(fb, NULL, "/active_pipes", "i", error, cbox_prefetch_stack_get_active_pipe_count(m->pipe_stack)) && + cbox_execute_on(fb, NULL, "/polyphony", "i", error, m->max_voices) && + CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + if (!strcmp(cmd->command, "/patches") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (uint32_t i = 0; i < m->program_count; i++) + { + struct sampler_program *prog = m->programs[i]; + if (!cbox_execute_on(fb, NULL, "/patch", "isoi", error, prog->prog_no, prog->name, prog, prog->in_use)) + return FALSE; + } + return TRUE; + } + else if (!strcmp(cmd->command, "/polyphony") && !strcmp(cmd->arg_types, "i")) + { + int polyphony = CBOX_ARG_I(cmd, 0); + if (polyphony < 1 || polyphony > MAX_SAMPLER_VOICES) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid polyphony %d (must be between 1 and %d)", polyphony, (int)MAX_SAMPLER_VOICES); + return FALSE; + } + m->max_voices = polyphony; + return TRUE; + } + else if (!strcmp(cmd->command, "/set_patch") && !strcmp(cmd->arg_types, "ii")) + { + int channel = CBOX_ARG_I(cmd, 0); + if (channel < 1 || channel > 16) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid channel %d", channel); + return FALSE; + } + int value = CBOX_ARG_I(cmd, 1); + struct sampler_program *pgm = NULL; + for (uint32_t i = 0; i < m->program_count; i++) + { + if (m->programs[i]->prog_no == value) + { + pgm = m->programs[i]; + break; + } + } + sampler_channel_set_program(&m->channels[channel - 1], pgm); + return TRUE; + } + else if (!strcmp(cmd->command, "/set_output") && !strcmp(cmd->arg_types, "ii")) + { + int channel = CBOX_ARG_I(cmd, 0); + int output = CBOX_ARG_I(cmd, 1); + if (channel < 1 || channel > 16) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid channel %d", channel); + return FALSE; + } + if (output < 0 || output >= m->output_pairs) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid output %d", output); + return FALSE; + } + m->channels[channel - 1].output_shift = output; + return TRUE; + } + else if (!strcmp(cmd->command, "/load_patch") && !strcmp(cmd->arg_types, "iss")) + { + struct sampler_program *pgm = NULL; + if (!load_program_at(m, CBOX_ARG_S(cmd, 1), CBOX_ARG_S(cmd, 2), CBOX_ARG_I(cmd, 0), &pgm, error)) + return FALSE; + if (fb) + return cbox_execute_on(fb, NULL, "/uuid", "o", error, pgm); + return TRUE; + } + else if (!strcmp(cmd->command, "/load_patch_from_file") && !strcmp(cmd->arg_types, "iss")) + { + struct sampler_program *pgm = NULL; + char *cfg_section = g_strdup_printf("spgm:!%s", CBOX_ARG_S(cmd, 1)); + gboolean res = load_program_at(m, cfg_section, CBOX_ARG_S(cmd, 2), CBOX_ARG_I(cmd, 0), &pgm, error); + g_free(cfg_section); + if (res && pgm && fb) + return cbox_execute_on(fb, NULL, "/uuid", "o", error, pgm); + return res; + } + else if (!strcmp(cmd->command, "/load_patch_from_string") && !strcmp(cmd->arg_types, "isss")) + { + struct sampler_program *pgm = NULL; + if (!load_from_string(m, CBOX_ARG_S(cmd, 1), CBOX_ARG_S(cmd, 2), CBOX_ARG_S(cmd, 3), CBOX_ARG_I(cmd, 0), &pgm, error)) + return FALSE; + if (fb && pgm) + return cbox_execute_on(fb, NULL, "/uuid", "o", error, pgm); + return TRUE; + } + else if (!strcmp(cmd->command, "/get_unused_program") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/program_no", "i", error, get_first_free_program_no(m)); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +gboolean sampler_select_program(struct sampler_module *m, int channel, const gchar *preset, GError **error) +{ + for (uint32_t i = 0; i < m->program_count; i++) + { + if (!strcmp(m->programs[i]->name, preset)) + { + sampler_channel_set_program(&m->channels[channel], m->programs[i]); + return TRUE; + } + } + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Preset not found: %s", preset); + return FALSE; +} + +double sampler_get_current_beat(struct sampler_module *m) +{ + uint32_t song_pos_samples = cbox_engine_current_pos_samples(m->module.engine); + double tempo_bpm = m->module.engine->master->tempo; + double song_pos_sec = song_pos_samples * 1.0 / m->module.srate; + return song_pos_sec * tempo_bpm / 60; +} + +MODULE_CREATE_FUNCTION(sampler) +{ + int i; + static int inited = 0; + if (!inited) + { + for (int i = 0; i < 2049; i++) + sampler_sine_wave[i] = sin(i * M_PI / 1024.0); + inited = 1; + } + + int max_voices = cbox_config_get_int(cfg_section, "polyphony", MAX_SAMPLER_VOICES); + if (max_voices < 1 || max_voices > MAX_SAMPLER_VOICES) + { + g_set_error(error, CBOX_SAMPLER_ERROR, CBOX_SAMPLER_ERROR_INVALID_LAYER, "%s: invalid polyphony value", cfg_section); + return NULL; + } + int output_pairs = cbox_config_get_int(cfg_section, "output_pairs", 1); + if (output_pairs < 1 || output_pairs > 16) + { + g_set_error(error, CBOX_SAMPLER_ERROR, CBOX_SAMPLER_ERROR_INVALID_LAYER, "%s: invalid output pairs value", cfg_section); + return NULL; + } + int aux_pairs = cbox_config_get_int(cfg_section, "aux_pairs", 0); + if (aux_pairs < 0 || aux_pairs > 4) + { + g_set_error(error, CBOX_SAMPLER_ERROR, CBOX_SAMPLER_ERROR_INVALID_LAYER, "%s: invalid aux pairs value", cfg_section); + return NULL; + } + + struct sampler_module *m = calloc(1, sizeof(struct sampler_module)); + CALL_MODULE_INIT(m, 0, (output_pairs + aux_pairs) * 2, sampler); + m->output_pairs = output_pairs; + m->aux_pairs = aux_pairs; + m->module.aux_offset = m->output_pairs * 2; + m->module.process_event = sampler_process_event; + m->module.process_block = sampler_process_block; + m->programs = NULL; + m->max_voices = max_voices; + m->serial_no = 0; + m->deleting = FALSE; + // XXXKF read defaults from some better place, like config + // XXXKF allow dynamic change of the number of the pipes + m->pipe_stack = cbox_prefetch_stack_new(MAX_SAMPLER_VOICES, cbox_config_get_int("streaming", "streambuf_size", 65536), cbox_config_get_int("streaming", "min_buf_frames", PIPE_MIN_PREFETCH_SIZE_FRAMES)); + m->disable_mixer_controls = cbox_config_get_int("sampler", "disable_mixer_controls", 0); + + float srate = m->module.srate; + for (i = 0; i < 12800; i++) + { + float freq = 440 * pow(2, (i - 5700) / 1200.0); + if (freq < 20.0) + freq = 20.0; + if (freq > srate * 0.45) + freq = srate * 0.45; + float omega=(float)(2*M_PI*freq/srate); + m->sincos[i].sine = sinf(omega); + m->sincos[i].cosine = cosf(omega); + m->sincos[i].prewarp = 2.0 * tan(hz2w(freq, srate) * 0.5f); + m->sincos[i].prewarp2 = 1.0 / (1.0 + m->sincos[i].prewarp); + + } + for (i = 0; ; i++) + { + gchar *s = g_strdup_printf("program%d", i); + char *p = cbox_config_get_string(cfg_section, s); + g_free(s); + + if (!p) + { + m->program_count = i; + break; + } + } + m->programs = calloc(m->program_count, sizeof(struct sampler_program *)); + int success = 1; + for (i = 0; i < (int)m->program_count; i++) + { + gchar *s = g_strdup_printf("program%d", i); + char *pgm_section = NULL; + int pgm_id = -1; + const char *pgm_name = cbox_config_get_string(cfg_section, s); + g_free(s); + char *at = strchr(pgm_name, '@'); + if (at) + { + pgm_id = atoi(at + 1); + s = g_strndup(pgm_name, at - pgm_name); + pgm_section = g_strdup_printf("spgm:%s", s); + g_free(s); + } + else + { + pgm_id = i; + pgm_section = g_strdup_printf("spgm:%s", pgm_name); + } + + m->programs[i] = sampler_program_new_from_cfg(m, pgm_section, pgm_section + 5, pgm_id, error); + g_free(pgm_section); + if (!m->programs[i]) + { + success = 0; + break; + } + } + if (!success) + { + // XXXKF free programs/layers, first ensuring that they're fully initialised + free(m); + return NULL; + } + m->voices_free = NULL; + memset(m->voices_all, 0, sizeof(m->voices_all)); + for (i = 0; i < MAX_SAMPLER_VOICES; i++) + { + struct sampler_voice *v = &m->voices_all[i]; + v->gen.mode = spt_inactive; + sampler_voice_link(&m->voices_free, v); + } + m->active_voices = 0; + m->active_prevoices = 0; + + m->prevoices_free = NULL; + memset(m->prevoices_all, 0, sizeof(m->prevoices_all)); + for (i = 0; i < MAX_SAMPLER_PREVOICES; i++) + { + struct sampler_prevoice *v = &m->prevoices_all[i]; + sampler_prevoice_link(&m->prevoices_free, v); + } + + for (i = 0; i < 16; i++) + sampler_channel_init(&m->channels[i], m); + + for (i = 0; i < 16; i++) + { + gchar *key = g_strdup_printf("channel%d", i + 1); + gchar *preset = cbox_config_get_string(cfg_section, key); + if (preset) + { + if (!sampler_select_program(m, i, preset, error)) + { + CBOX_DELETE(&m->module); + return NULL; + } + } + g_free(key); + key = g_strdup_printf("channel%d_output", i + 1); + m->channels[i].output_shift = cbox_config_get_int(cfg_section, key, 1) - 1; + g_free(key); + } + + + return &m->module; +} + +void sampler_destroyfunc(struct cbox_module *module) +{ + struct sampler_module *m = (struct sampler_module *)module; + uint32_t i; + m->deleting = TRUE; + + for (i = 0; i < m->program_count;) + { + if (m->programs[i]) + CBOX_DELETE(m->programs[i]); + else + i++; + } + for (i = 0; i < 16U; i++) + { + assert (m->channels[i].voices_running == NULL); + } + cbox_prefetch_stack_destroy(m->pipe_stack); + free(m->programs); +} + +#define MAKE_TO_STRING_CONTENT(name, v) \ + case v: return name; + +#define MAKE_FROM_STRING_CONTENT(n, v) \ + if (!strcmp(name, n)) { *value = v; return TRUE; } + +#define MAKE_FROM_TO_STRING(enumtype) \ + const char *enumtype##_to_string(enum enumtype value) \ + { \ + switch(value) { \ + ENUM_VALUES_##enumtype(MAKE_TO_STRING_CONTENT) \ + default: return NULL; \ + } \ + } \ + \ + gboolean enumtype##_from_string(const char *name, enum enumtype *value) \ + { \ + ENUM_VALUES_##enumtype(MAKE_FROM_STRING_CONTENT) \ + return FALSE; \ + } + +ENUM_LIST(MAKE_FROM_TO_STRING) + +////////////////////////////////////////////////////////////////////////// + +struct cbox_module_livecontroller_metadata sampler_controllers[] = { +}; + +struct cbox_module_keyrange_metadata sampler_keyranges[] = { +}; + +DEFINE_MODULE(sampler, 0, 2) + diff --git a/template/calfbox/sampler.h b/template/calfbox/sampler.h new file mode 100644 index 0000000..6dba6e9 --- /dev/null +++ b/template/calfbox/sampler.h @@ -0,0 +1,394 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SAMPLER_H +#define CBOX_SAMPLER_H + +#include "biquad-float.h" +#include "envelope.h" +#include "module.h" +#include "onepole-float.h" +#include "prefetch_pipe.h" +#include "sampler_layer.h" +#include "sampler_prg.h" +#include "wavebank.h" +#include + +#define MAX_SAMPLER_VOICES 128 +#define MAX_SAMPLER_PREVOICES 128 +#define SAMPLER_NO_LOOP ((uint32_t)-1) + +#define CBOX_SAMPLER_ERROR cbox_sampler_error_quark() + +enum CboxSamplerError +{ + CBOX_SAMPLER_ERROR_FAILED, + CBOX_SAMPLER_ERROR_INVALID_LAYER, + CBOX_SAMPLER_ERROR_INVALID_WAVEFORM, + CBOX_SAMPLER_ERROR_NO_PROGRAMS +}; + +struct sampler_noteinitfunc; +struct sampler_voice; +struct sampler_prevoice; + +#define GET_RT_FROM_sampler_channel(channel) ((channel)->module->module.rt) + +#define MAX_KEYSWITCH_GROUPS 16 + +struct sampler_channel +{ + struct sampler_module *module; + int pitchwheel; + uint32_t switchmask[4]; + uint32_t sustainmask[4]; + uint32_t sostenutomask[4]; + int previous_note, first_note_vel; + struct sampler_program *program; + struct sampler_voice *voices_running; + int active_voices, active_prevoices; + uint8_t prev_note_velocity[128]; + uint8_t poly_pressure[128]; + uint32_t prev_note_start_time[128]; + int channel_volume_cc, channel_pan_cc; + int output_shift; + uint32_t poly_pressure_mask; + uint8_t intcc[smsrc_perchan_count]; + float floatcc[smsrc_perchan_count]; + uint8_t last_polyaft, last_chanaft; + uint8_t keyswitch_state[MAX_KEYSWITCH_GROUPS]; +}; + +struct sampler_lfo +{ + uint32_t phase, delta, xdelta; + uint32_t age, delay, fade; + int32_t wave; + float random_value; +}; + +struct sampler_gen +{ + enum sampler_player_type mode; + int16_t *sample_data; + int16_t *scratch; + + uint64_t bigpos, bigdelta; + uint64_t virtpos, virtdelta; + uint32_t loop_start, loop_end; + uint32_t cur_sample_end; + float lgain, rgain; + float last_lgain, last_rgain; + float fadein_counter; + uint64_t fadein_pos; + + // In-memory mode only + uint32_t loop_overlap; + float loop_overlap_step; + float stretching_jump; + float stretching_crossfade; + uint32_t play_count, loop_count; + int16_t scratch_bandlimited[2 * MAX_INTERPOLATION_ORDER * 2]; + + // Streaming mode only + int16_t *streaming_buffer; + uint32_t consumed, consumed_credit, streaming_buffer_frames; + gboolean prefetch_only_loop, in_streaming_buffer; +}; + +struct sampler_prevoice +{ + struct sampler_prevoice *prev, *next; + struct sampler_layer_data *layer_data; + struct sampler_channel *channel; + int note, vel; + int age; + double sync_trigger_time, sync_initial_time, sync_beats; + float delay_computed; +}; + +struct sampler_filter +{ + struct cbox_biquadf_coeffs filter_coeffs, filter_coeffs_extra; + struct cbox_biquadf_coeffs *second_filter; + struct cbox_biquadf_state filter_left[3], filter_right[3]; +}; + +struct sampler_voice +{ + struct sampler_voice *prev, *next; + struct sampler_layer_data *layer; + // Note: may be NULL when program is being deleted + struct sampler_program *program; + struct cbox_waveform *last_waveform; + struct sampler_gen gen; + struct cbox_prefetch_pipe *current_pipe; + int note; + int vel; + int released, released_with_sustain, released_with_sostenuto, captured_sostenuto; + int off_by; + int age; + float pitch_shift; + float cutoff_shift, cutoff2_shift; + float gain_shift, gain_fromvel; + struct sampler_filter filter, filter2; + struct cbox_onepolef_state onepole_left, onepole_right; + struct cbox_onepolef_coeffs onepole_coeffs; + struct sampler_channel *channel; + struct cbox_envelope amp_env, filter_env, pitch_env; + struct sampler_lfo amp_lfo, filter_lfo, pitch_lfo; + enum sampler_loop_mode loop_mode; + int output_pair_no; + int send1bus, send2bus; + float send1gain, send2gain; + int serial_no; + struct cbox_envelope_shape vel_envs[3], cc_envs[3]; // amp, filter, pitch + struct cbox_biquadf_state eq_left[3], eq_right[3]; + struct cbox_biquadf_coeffs eq_coeffs[3]; + gboolean layer_changed; + int last_level; + uint64_t last_level_min_rate; + uint32_t last_eq_bitmask; + float reloffset; + uint32_t offset; + int off_vel; +}; + +struct sampler_module +{ + struct cbox_module module; + + struct sampler_voice *voices_free, voices_all[MAX_SAMPLER_VOICES]; + struct sampler_prevoice *prevoices_free, prevoices_all[MAX_SAMPLER_PREVOICES], *prevoices_running; + struct sampler_channel channels[16]; + struct sampler_program **programs; + uint32_t program_count; + int active_voices, max_voices; + int active_prevoices; + int serial_no; + int output_pairs, aux_pairs; + uint32_t current_time; + gboolean deleting; + int disable_mixer_controls; + struct cbox_prefetch_stack *pipe_stack; + struct cbox_sincos sincos[12800]; +}; + +#define MAX_RELEASED_GROUPS 16 + +struct sampler_released_groups +{ + // Groups 1-32 use a bitmask + uint32_t low_groups; + int group_count; + int groups[MAX_RELEASED_GROUPS]; +}; + +static inline void sampler_released_groups_init(struct sampler_released_groups *groups) +{ + groups->low_groups = 0; + groups->group_count = 0; +} + +static inline gboolean sampler_released_groups_check(struct sampler_released_groups *groups, int group) +{ + if (group <= 32) + return (groups->low_groups >> (group - 1)) & 1; + for (int j = 0; j < groups->group_count; j++) + { + if (groups->groups[j] == group) + return TRUE; + } + return FALSE; +} + +static inline void sampler_released_groups_add(struct sampler_released_groups *groups, int group) +{ + if (group <= 32) + { + groups->low_groups |= (1 << (group - 1)); + return; + } + if (groups->group_count >= MAX_RELEASED_GROUPS) + return; + if (!sampler_released_groups_check(groups, group)) + groups->groups[groups->group_count++] = group; +} + +extern GQuark cbox_sampler_error_quark(void); + +extern void sampler_register_program(struct sampler_module *m, struct sampler_program *pgm); +extern gboolean sampler_select_program(struct sampler_module *m, int channel, const gchar *preset, GError **error); +extern void sampler_unselect_program(struct sampler_module *m, struct sampler_program *prg); +extern double sampler_get_current_beat(struct sampler_module *m); + +extern void sampler_channel_init(struct sampler_channel *c, struct sampler_module *m); +// This function may only be called from RT thread! +extern void sampler_channel_set_program_RT(struct sampler_channel *c, struct sampler_program *prg); +// ... and this one is RT-safe +extern void sampler_channel_set_program(struct sampler_channel *c, struct sampler_program *prg); +extern void sampler_channel_start_note(struct sampler_channel *c, int note, int vel, gboolean is_release_trigger); +extern void sampler_channel_stop_note(struct sampler_channel *c, int note, int vel, gboolean is_polyaft); +extern void sampler_channel_program_change(struct sampler_channel *c, int program); +extern void sampler_channel_stop_sustained(struct sampler_channel *c); +extern void sampler_channel_stop_sostenuto(struct sampler_channel *c); +extern void sampler_channel_capture_sostenuto(struct sampler_channel *c); +extern void sampler_channel_release_groups(struct sampler_channel *c, int note, struct sampler_released_groups *exgroups); +extern void sampler_channel_stop_all(struct sampler_channel *c); +extern void sampler_channel_process_cc(struct sampler_channel *c, int cc, int val); +extern void sampler_channel_reset_keyswitches(struct sampler_channel *c); + +extern void sampler_voice_start(struct sampler_voice *v, struct sampler_channel *c, struct sampler_layer_data *l, int note, int vel, struct sampler_released_groups *exgroups); +extern void sampler_voice_start_silent(struct sampler_layer_data *l, struct sampler_released_groups *exgroups); +extern void sampler_voice_release(struct sampler_voice *v, gboolean is_polyaft); +extern void sampler_voice_process(struct sampler_voice *v, struct sampler_module *m, cbox_sample_t **outputs); +extern void sampler_voice_link(struct sampler_voice **pv, struct sampler_voice *v); +extern void sampler_voice_unlink(struct sampler_voice **pv, struct sampler_voice *v); +extern void sampler_voice_inactivate(struct sampler_voice *v, gboolean expect_active); +extern void sampler_voice_update_params_from_layer(struct sampler_voice *v); +extern float sampler_channel_get_expensive_cc(struct sampler_channel *c, struct sampler_voice *v, struct sampler_prevoice *pv, int cc_no); + +extern void sampler_prevoice_start(struct sampler_prevoice *pv, struct sampler_channel *c, struct sampler_layer_data *l, int note, int vel); +extern int sampler_prevoice_process(struct sampler_prevoice *pv, struct sampler_module *m); +extern void sampler_prevoice_link(struct sampler_prevoice **pv, struct sampler_prevoice *v); +extern void sampler_prevoice_unlink(struct sampler_prevoice **pv, struct sampler_prevoice *v); + +extern float sampler_sine_wave[2049]; + +static inline int sampler_channel_addcc(struct sampler_channel *c, int cc_no) +{ + return (((int)c->intcc[cc_no]) << 7) + c->intcc[cc_no + 32]; +} + +static inline float sampler_channel_getcc(struct sampler_channel *c, struct sampler_voice *v, int cc_no) +{ + if (cc_no < 128) + return c->floatcc[cc_no]; + return sampler_channel_get_expensive_cc(c, v, NULL, cc_no); +} + +static inline float sampler_program_get_curve_value(struct sampler_program *program, uint32_t curve_id, float val) +{ + if (val < 0) + val = 0; + if (val > 1) + val = 1; + if (curve_id < MAX_MIDI_CURVES && program->interpolated_curves[curve_id]) + { + float *curve = program->interpolated_curves[curve_id]; + int vint = floorf(val * 127); + // Linear interpolation if within bounds + if (vint < 127) + { + float vfrac = val * 127 - vint; + val = curve[vint] + (curve[vint + 1] - curve[vint]) * vfrac; + } + else + val = curve[vint]; + } + else + { + // possibly wrong implementation of built in curves + switch(curve_id) + { + case 0: + break; + case 1: + case 3: + // slightly fake bipolar, so that both 63 and 64 are 'neutral' (needs to be somewhat symmetric) + if (val < 63.f/127.f) + val = (curve_id == 3 ? -1 : 1) * -(63.f - 127 * val) / 63.f; + else if (val <= 64.f/127.f) + val = 0; + else + val = (curve_id == 3 ? -1 : 1) * (127 * val - 64) / 63.f; + break; + case 2: + val = 1 - val; break; + case 4: + val = val * val; + break; // maybe, or maybe it's inverted? + case 5: val = sqrtf(val); + break; // maybe + case 6: val = sqrtf(1-val); + break; // maybe + } + } + return val; +} + +static inline float sampler_channel_getcc_mod(struct sampler_channel *c, struct sampler_voice *v, int cc_no, int curve_id, float step) +{ + float val = (cc_no < 128) ? c->floatcc[cc_no] : sampler_channel_get_expensive_cc(c, v, NULL, cc_no); + if (step) + val = floorf(0.9999f * val * (step + 1)) / step; + if (curve_id || c->program->interpolated_curves[0]) + val = sampler_program_get_curve_value(c->program, curve_id, val); + return val; +} + +static inline int sampler_channel_getintcc(struct sampler_channel *c, struct sampler_voice *v, int cc_no) +{ + if (cc_no < 128) + return c->intcc[cc_no]; + return (int)127 * (sampler_channel_get_expensive_cc(c, v, NULL, cc_no)); +} + +static inline float sampler_channel_getcc_prevoice(struct sampler_channel *c, struct sampler_prevoice *pv, int cc_no, int curve_id, float step) +{ + float val = (cc_no < 128) ? c->floatcc[cc_no] : sampler_channel_get_expensive_cc(c, NULL, pv, cc_no); + if (step) + val = floorf(0.9999f * val * (step + 1)) / step; + if (curve_id || c->program->interpolated_curves[0]) + val = sampler_program_get_curve_value(c->program, curve_id, val); + return val; +} + +static inline float sampler_channel_get_poly_pressure(struct sampler_channel *c, uint8_t note) +{ + note &= 0x7F; + return (c->poly_pressure_mask & (1 << (note >> 2))) ? c->poly_pressure[note] * (1.f / 127.f) : 0;; +} + +static inline gboolean sampler_cc_range_is_in(const struct sampler_cc_range *range, struct sampler_channel *c) +{ + while(range) + { + int ccval = sampler_channel_getintcc(c, NULL, range->key.cc_number); + if (ccval < range->value.locc || ccval > range->value.hicc) + return FALSE; + range = range->next; + } + return TRUE; +} + +#define FOREACH_VOICE(var, p) \ + for (struct sampler_voice *p = (var), *p##_next = NULL; p && (p##_next = p->next, TRUE); p = p##_next) +#define FOREACH_PREVOICE(var, p) \ + for (struct sampler_prevoice *p = (var), *p##_next = NULL; p && (p##_next = p->next, TRUE); p = p##_next) +#define CANCEL_PREVOICE(var, p) \ + {\ + struct sampler_prevoice *_tmp = (p); \ + if (_tmp->prev) \ + _tmp->prev->next = _tmp->next; \ + else \ + var = _tmp->next; \ + _tmp->prev = _tmp->next = NULL; \ + } + +#endif diff --git a/template/calfbox/sampler_api_example.py b/template/calfbox/sampler_api_example.py new file mode 100644 index 0000000..84bea11 --- /dev/null +++ b/template/calfbox/sampler_api_example.py @@ -0,0 +1,59 @@ +import os +import sys +import struct +import time +import unittest + +sys.path = ["./py"] + sys.path + +import cbox + +global Document +Document = cbox.Document + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() + +npfs = instrument.engine.load_patch_from_string(0, '.', '', 'new_patch') +instrument.engine.set_patch(1, 0) + +mgrp = npfs.get_global().get_children()[0] +g1 = mgrp.new_child() +g1.set_param("cutoff", "100") +g1.set_param("resonance", "6") +g1.set_param("fil_type", "lpf_4p") +g1.set_param("fileg_start", "50") +g1.set_param("fileg_attack", "0.01") +g1.set_param("fileg_decay", "0.2") +g1.set_param("fileg_sustain", "20") +g1.set_param("fileg_depth", "5400") +g1.set_param("fileg_release", "10") +g1.set_param("ampeg_release", "0.1") +g1.set_param("amp_veltrack", "0") +g1.set_param("volume", "-12") +g1.set_param("fileg_depthcc14", "-5400") + +#g1.set_param("cutoff", "1000") +#g1.set_param("fillfo_freq", "4") +#g1.set_param("fillfo_depth", "2400") +#g1.set_param("fillfo_wave", "12") +#g1.set_param("fillfo_freqcc2", "4") + +r1 = g1.new_child() +r1.set_param("sample", "*saw") +r1.set_param("transpose", "0") +r1.set_param("tune", "5") +r1.set_param("gain_cc17", "12") + +r2 = g1.new_child() +r2.set_param("sample", "*sqr") +r2.set_param("transpose", "12") +r2.set_param("gain_cc17", "-12") + +print(instrument.engine.status()) + +print("Ready!") + +while True: + cbox.call_on_idle() diff --git a/template/calfbox/sampler_api_example2.py b/template/calfbox/sampler_api_example2.py new file mode 100644 index 0000000..a7b7ed7 --- /dev/null +++ b/template/calfbox/sampler_api_example2.py @@ -0,0 +1,32 @@ +from calfbox import cbox + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() +pgm_no = instrument.engine.get_unused_program() +pgm = instrument.engine.load_patch_from_file(pgm_no, 'synthbass.sfz', 'SynthBass') +instrument.engine.set_patch(1, pgm_no) +print (instrument.engine.get_patches()) +print (instrument.get_output_slot(0)) +print (instrument.get_output_slot(0).status()) +instrument.get_output_slot(0).set_insert_engine("reverb") +print (instrument.get_output_slot(0).status()) +instrument.get_output_slot(0).engine.cmd("/wet_amt", None, 1.0) +for i in pgm.get_global().get_children()[0].get_children(): + print ("", i.as_string()) + for j in i.get_children(): + print ("", j.as_string()) + +print("Ready!") + +while True: + cbox.call_on_idle(cmd_dumper) diff --git a/template/calfbox/sampler_api_example4.py b/template/calfbox/sampler_api_example4.py new file mode 100644 index 0000000..38cb8f3 --- /dev/null +++ b/template/calfbox/sampler_api_example4.py @@ -0,0 +1,27 @@ +from calfbox import cbox + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() +pgm_no = instrument.engine.get_unused_program() +pgm = instrument.engine.load_patch_from_tar(pgm_no, 'sonatina.sbtar', 'Brass - Horn Solo.sfz', 'HornSolo') +pgm.add_control_init(7, 127) +pgm.add_control_init(10, 0) +print (pgm.load_file('Brass - Horn Solo.sfz').readlines()) +print (pgm.status()) +print (list(pgm.get_control_inits())) +instrument.engine.set_patch(1, pgm_no) + +print("Ready!") + +while True: + cbox.call_on_idle(cmd_dumper) diff --git a/template/calfbox/sampler_api_test.py b/template/calfbox/sampler_api_test.py new file mode 100644 index 0000000..8beca0c --- /dev/null +++ b/template/calfbox/sampler_api_test.py @@ -0,0 +1,267 @@ +import os +import sys +import struct +import time +import unittest + +sys.path = ["./py"] + sys.path + +import cbox + +global Document +Document = cbox.Document + +def verify_region(region, has, has_not, full = False): + values = set() + values_dict = {} + rtext = region.as_string() if not full else region.as_string_full() + for pair in rtext.split(" "): + if "=" in pair: + key, value = pair.split("=") + values.add(pair) + values.add(key) + values_dict[key] = value + if 'genericmod' in rtext: + print (rtext, "Expected:", has) + assert False + for i in has: + if '_oncc' in i: + i = i.replace('_oncc', '_cc') + if i not in values: + print("Not found: %s, has: %s" % (i, rtext)) + assert False + for i in has_not: + if i in values: + print("Found unwanted string: %s=%s" % (i, values_dict[i])) + assert False + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() + +npfs = instrument.engine.load_patch_from_string(0, '.', '', 'new_patch') +instrument.engine.set_patch(1, 0) + +glb = npfs.get_global() +assert glb +assert glb.get_children() +master = glb.get_children()[0] + +g1 = master.new_child() +g1.set_param("cutoff", "100") +g1.set_param("resonance", "6") +g1.set_param("fil_type", "lpf_4p") +g1.set_param("fileg_decay", "0.2") +g1.set_param("fileg_sustain", "10") +g1.set_param("fileg_depth", "5400") +g1.set_param("fileg_release", "10") +g1.set_param("ampeg_release", "0.1") +g1.set_param("amp_veltrack", "0") +g1.set_param("volume", "-12") +g1.set_param("fileg_depthcc14", "-5400") + +def check_exception(param, value, substr): + error = False + try: + g1.set_param(param, value) + except Exception as e: + error = True + assert substr in str(e), "Exception with substring '%s' expected when setting %s to %s, caught another one: %s" % (substr, param, value, str(e)) + assert error, "Exception with substring '%s' expected when setting %s to %s, none caught" % (substr, param, value) + +check_exception("cutoff", "bla", "correct numeric value") +check_exception("key", "bla", "valid note name") +check_exception("lochan", "bla", "correct integer value") +check_exception("lochan", "10.5", "correct integer value") +check_exception("offset", "bla", "correct unsigned integer value") +check_exception("offset", "10.5", "correct unsigned integer value") +check_exception("offset", "-1000", "correct unsigned integer value") + +# Make sure that multiple CC assignments work +g1.set_param("locc5", "100") +g1.set_param("locc8", "100") +g1.set_param("hicc8", "110") + +#g1.set_param("cutoff", "1000") +#g1.set_param("fillfo_freq", "4") +#g1.set_param("fillfo_depth", "2400") +#g1.set_param("fillfo_wave", "12") + +r1 = g1.new_child() +r1.set_param("sample", "*saw") +r1.set_param("transpose", "0") +r1.set_param("tune", "5") +r1.set_param("gain_cc17", "12") + +verify_region(g1, ["locc5=100", "locc8=100", "hicc8=110"], ['hicc5']) +verify_region(r1, ["locc5=100", "locc8=100", "hicc8=110"], [], full=True) + +r1.set_param("hicc5", "120") +r1.set_param("hicc8", "124") + +verify_region(g1, ["locc5=100", "locc8=100", "hicc8=110"], ['hicc5']) +verify_region(g1, ["locc5=100", "hicc5=127", "locc8=100", "hicc8=110"], [], full=True) +verify_region(r1, ["locc5=100", "hicc5=120", "locc8=100", "hicc8=124"], [], full=True) + +r2 = g1.new_child() +r2.set_param("sample", "*sqr") +r2.set_param("transpose", "12") +r2.set_param("gain_cc17", "-12") +verify_region(r2, ["sample=*sqr", "gain_cc17=-12", "transpose=12"], []) +r2.set_param("gain_cc17", "-24") +verify_region(r2, ["gain_cc17=-24"], ["gain_cc17=-12"]) +r2.unset_param("gain_cc17") +verify_region(r2, ["sample=*sqr"], ["gain_cc17"]) +r2.unset_param("transpose") +verify_region(r2, ["sample=*sqr"], ["transpose"]) +r2.unset_param("sample") +verify_region(r2, [], ["transpose", "sample"]) + +g1.unset_param("cutoff") +g1.unset_param("resonance") +g1.unset_param("fil_type") +g1.unset_param("fileg_sustain") +g1.unset_param("fileg_decay") +g1.unset_param("fileg_depth") +g1.unset_param("fileg_release") +g1.unset_param("ampeg_release") +g1.unset_param("amp_veltrack") +g1.unset_param("volume") +g1.unset_param("fileg_depthcc14") +g1.unset_param("locc5") +g1.unset_param("locc8") +g1.unset_param("hicc8") + +params_to_test = [ + 'lokey', 'hikey', 'lovel', 'hivel', 'key', + 'cutoff', 'pan', 'offset', 'tune', 'position', 'width', + 'amp_random', 'fil_random', 'pitch_random', 'delay_random', + 'pitch_veltrack', 'reloffset_veltrack', 'offset_veltrack', + 'delay_cc5', 'delay_cc10', 'reloffset_cc5', 'reloffset_cc10', 'offset_cc5', 'offset_cc10', + 'delay_curvecc8', 'reloffset_curvecc5', 'offset_curvecc10', + 'delay_stepcc8', 'reloffset_stepcc5', 'offset_stepcc10', + 'cutoff_cc1', "resonance_cc1", 'pitch_cc1', 'tonectl_cc1', 'gain_cc1', 'amplitude_cc1', + 'cutoff_oncc2', "resonance_oncc2", 'pitch_oncc2', 'tonectl_oncc2', 'gain_oncc2', 'amplitude_oncc2', + 'cutoff_curvecc1', 'resonance_curvecc5', 'pitch_curvecc10', 'amplitude_curvecc10', + 'cutoff_smoothcc1', 'resonance_smoothcc5', 'pitch_smoothcc10', 'amplitude_smoothcc10', + 'cutoff_stepcc1', 'resonance_stepcc5', 'pitch_stepcc10', 'amplitude_stepcc10', + 'loop_start', 'loop_end', + 'ampeg_attack', + 'amplfo_depth', 'fillfo_depth', + 'fileg_vel2depth', + 'fileg_depthcc5', 'fillfo_depthcc8','amplfo_depthcc5', + 'fileg_depth_curvecc5', 'fillfo_depth_curvecc8','amplfo_depth_curvecc5', + 'fileg_depth_smoothcc5', 'fillfo_depth_smoothcc8','amplfo_depth_smoothcc5', + 'fileg_depth_stepcc5', 'fillfo_depth_stepcc8','amplfo_depth_stepcc5', + 'amplfo_freqcc5', 'fillfo_freqcc10', 'pitchlfo_freqcc5', + 'cutoff_chanaft', 'resonance_chanaft', + 'cutoff_polyaft', 'resonance_polyaft', + 'amplfo_depthpolyaft', 'fillfo_depthpolyaft', 'pitchlfo_depthpolyaft', + 'amplfo_freqpolyaft', 'fillfo_freqpolyaft', 'pitchlfo_freqpolyaft', + 'eq1_freqcc1', 'eq2_gaincc2', 'eq3_bwcc3', + 'eq1_freq_curvecc1', 'eq2_gain_curvecc2', 'eq3_bw_curvecc3', + 'eq1_freq_smoothcc1', 'eq2_gain_smoothcc2', 'eq3_bw_smoothcc3', + 'eq1_freq_stepcc1', 'eq2_gain_stepcc2', 'eq3_bw_stepcc3', + 'fileg_vel2start', 'fileg_vel2delay', 'fileg_vel2attack', 'fileg_vel2hold', 'fileg_vel2decay', 'fileg_vel2sustain', 'fileg_vel2release', + 'fileg_startcc1', 'fileg_delaycc1', 'fileg_attackcc1', 'fileg_holdcc1', 'fileg_decaycc1', 'fileg_sustaincc1', 'fileg_releasecc1', + 'fileg_start_curvecc1', 'fileg_delay_curvecc1', 'fileg_attack_curvecc1', 'fileg_hold_curvecc1', 'fileg_decay_curvecc1', 'fileg_sustain_curvecc1', 'fileg_release_curvecc1', + 'fileg_start_smoothcc1', 'fileg_delay_smoothcc1', 'fileg_attack_smoothcc1', 'fileg_hold_smoothcc1', 'fileg_decay_smoothcc1', 'fileg_sustain_smoothcc1', 'fileg_release_smoothcc1', + 'fileg_start_stepcc1', 'fileg_delay_stepcc1', 'fileg_attack_stepcc1', 'fileg_hold_stepcc1', 'fileg_decay_stepcc1', 'fileg_sustain_stepcc1', 'fileg_release_stepcc1', + 'cutoff2', 'resonance2', 'fileg_depth2', 'fileg_depth2cc1', 'fileg_depth2_stepcc1', + 'fillfo_depth2', 'fillfo_depth2cc1', 'fillfo_depth2_stepcc1', + 'amp_velcurve_5', 'amp_velcurve_127', + 'locc5', 'hicc5', + 'on_locc8', 'on_hicc8', + 'lfo5_freq', 'lfo1_wave', 'lfo3_fade', 'lfo4_delay', + ] +for i in range(len(params_to_test)): + param = params_to_test[0] + print ("Trying %s" % param) + rest = params_to_test[1:] + value1, value2 = "100", "80" + if 'key' in param: + value1, value2 = "e1", "g1" + # Verify that a setting is reported back + r2.set_param(param, value1) + verify_region(r2, ["%s=%s" % (param, value1)], rest) + + # Verify that setting the same value in parent doesn't change the local 'has' flag + g1.set_param(param, value1) + verify_region(r2, ["%s=%s" % (param, value1)], rest) + + # Verify that setting a different local value doesn't get overridden by parent + r2.set_param(param, value2) + verify_region(r2, ["%s=%s" % (param, value2)], rest) + # Write the original value + r2.set_param(param, value1) + verify_region(r2, ["%s=%s" % (param, value1)], rest) + + # Delete the parent value, confirm the deletion doesn't propagate to child + g1.unset_param(param) + verify_region(r2, ["%s=%s" % (param, value1)], rest) + # Set a different value in the parent, confirm it doesn't affect the child + g1.set_param(param, value2) + verify_region(g1, ["%s=%s" % (param, value2)], rest) + verify_region(r2, ["%s=%s" % (param, value1)], rest) + + # Check that the newly created child inherits the setting from the parent + r3 = g1.new_child() + verify_region(r3, [], [param]) + verify_region(r3, ["%s=%s" % (param, value2)], [], full=True) + r3.delete() + + # Verify that the original child still has the original value + verify_region(r2, ["%s=%s" % (param, value1)], [], full=True) + # Delete the child value, make sure it disappears but the inherited + # value is still reported in the full listing + r2.unset_param(param) + verify_region(r2, [], params_to_test) + verify_region(r2, ["%s=%s" % (param, value2)], [], full=True) + # Delete the setting in the parent, make sure the inherited value in the + # child disappears too + g1.unset_param(param) + verify_region(r2, [], ["%s=%s" % (param, value2)], full=True) + + master.set_param(param, value1) + verify_region(master, ["%s=%s" % (param, value1)], []) + verify_region(g1, [], params_to_test) + verify_region(g1, ["%s=%s" % (param, value1)], [], full=True) + verify_region(r2, [], params_to_test) + verify_region(r2, ["%s=%s" % (param, value1)], [], full=True) + master.unset_param(param) + verify_region(master, [], params_to_test) + verify_region(g1, [], params_to_test) + verify_region(r2, [], params_to_test) + + params_to_test = params_to_test[1:] + params_to_test[0:1] + +old_names = [ + ("hilev", "hivel"), + ("lolev", "lovel"), + ("loopstart", "loop_start"), + ("loopend", "loop_end"), + ("loopmode", "loop_mode", "one_shot", "loop_continuous"), + ("bendup", "bend_up"), + ("benddown", "bend_down"), + ("offby", "off_by"), +] + +for t in old_names: + if len(t) == 2: + old, new = t + v1, v2 = "10", "20" + else: + old, new, v1, v2 = t + print ("Trying alias: %s" % old) + r1.set_param(old, v1) + verify_region(r1, ["%s=%s" % (new, v1)], [old]) + r1.set_param(old, v2) + verify_region(r1, ["%s=%s" % (new, v2)], [old]) + r1.unset_param(old) + verify_region(r1, [], [old, new]) + r1.set_param(new, v1) + verify_region(r1, ["%s=%s" % (new, v1)], [old]) + r1.set_param(new, v2) + verify_region(r1, ["%s=%s" % (new, v2)], [old]) + r1.unset_param(new) + verify_region(r1, [], [old, new]) diff --git a/template/calfbox/sampler_channel.c b/template/calfbox/sampler_channel.c new file mode 100644 index 0000000..4af9396 --- /dev/null +++ b/template/calfbox/sampler_channel.c @@ -0,0 +1,535 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "dspmath.h" +#include "engine.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_impl.h" +#include "sfzloader.h" +#include "stm.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static inline void set_cc_int(struct sampler_channel *c, uint32_t cc, uint8_t value) +{ + c->intcc[cc] = value; + c->floatcc[cc] = value * (1.0f / 127.f); +} + +static inline void set_cc_float(struct sampler_channel *c, uint32_t cc, float value) +{ + c->intcc[cc] = (uint8_t)(value * 127) & 127; + c->floatcc[cc] = value; +} + +void sampler_channel_reset_keyswitches(struct sampler_channel *c) +{ + if (c->program && c->program->rll) + { + memset(c->keyswitch_state, 255, sizeof(c->keyswitch_state)); + for (uint32_t i = 0; i < c->program->rll->keyswitch_group_count; ++i) + { + const struct sampler_keyswitch_group *ksg = c->program->rll->keyswitch_groups[i]; + if (ksg->def_value == 255) + c->keyswitch_state[i] = ksg->key_offsets[0]; + else + c->keyswitch_state[i] = ksg->key_offsets[ksg->def_value]; + } + } +} + +void sampler_channel_init(struct sampler_channel *c, struct sampler_module *m) +{ + c->module = m; + c->voices_running = NULL; + c->active_voices = 0; + c->active_prevoices = 0; + c->pitchwheel = 0; + c->output_shift = 0; + for (int i = 0; i < smsrc_perchan_count; ++i) + { + c->intcc[i] = 0; + c->floatcc[i] = 0; + } + c->poly_pressure_mask = 0; + + // default to maximum and pan=centre if MIDI mixing disabled + if (m->disable_mixer_controls) + { + c->channel_volume_cc = 16383; + c->channel_pan_cc = 8192; + } + else + { + sampler_channel_process_cc(c, 7, 100); + sampler_channel_process_cc(c, 7 + 32, 0); + sampler_channel_process_cc(c, 10, 64); + sampler_channel_process_cc(c, 10 + 32, 0); + } + set_cc_int(c, 11, 127); + set_cc_int(c, 71, 64); + set_cc_int(c, 74, 64); + set_cc_float(c, smsrc_alternate, 0); + c->previous_note = -1; + c->last_polyaft = 0; + c->first_note_vel = 100; + c->program = NULL; + sampler_channel_set_program_RT(c, m->program_count ? m->programs[0] : NULL); + memset(c->switchmask, 0, sizeof(c->switchmask)); + memset(c->sustainmask, 0, sizeof(c->sustainmask)); + memset(c->sostenutomask, 0, sizeof(c->sostenutomask)); +} + +void sampler_channel_process_cc(struct sampler_channel *c, int cc, int val) +{ + struct sampler_module *m = c->module; + // Handle CC triggering. + if (c->program && c->program->rll && c->program->rll->layers_oncc) + { + struct sampler_rll *rll = c->program->rll; + if ((rll->cc_trigger_bitmask[cc >> 5] & (1 << (cc & 31)))) + { + int old_value = c->intcc[cc]; + for (GSList *p = rll->layers_oncc; p; p = p->next) + { + struct sampler_layer *layer = p->data; + assert(layer->runtime); + // Default (compatible) behaviour means the region will trigger + // on every CC that has value within the specified range. + // XXXKF add a switch for only triggering the region on + // transition between in-range and out-of-range. + gboolean compatible_oncc_behaviour = TRUE; + + struct sampler_cc_range *on_cc = layer->runtime->on_cc; + gboolean trigger = FALSE; + while(on_cc) + { + if (on_cc->key.cc_number == cc && + (val >= on_cc->value.locc && val <= on_cc->value.hicc) && + (compatible_oncc_behaviour || !(old_value >= on_cc->value.locc && old_value <= on_cc->value.hicc))) + { + trigger = TRUE; + break; + } + on_cc = on_cc->next; + } + if(trigger) + { + struct sampler_voice *v = m->voices_free; + if (!v) + break; + struct sampler_released_groups exgroups; + sampler_released_groups_init(&exgroups); + sampler_voice_start(v, c, layer->runtime, layer->runtime->pitch_keycenter, 127, &exgroups); + sampler_channel_release_groups(c, -1, &exgroups); + } + } + } + } + int was_enabled = c->intcc[cc] >= 64; + int enabled = val >= 64; + switch(cc) + { + case 10: + case 10 + 32: + set_cc_int(c, cc, val); + if (!c->module->disable_mixer_controls) + c->channel_pan_cc = sampler_channel_addcc(c, 10); + break; + case 7: + case 7 + 32: + set_cc_int(c, cc, val); + if (!c->module->disable_mixer_controls) + c->channel_volume_cc = sampler_channel_addcc(c, 7); + break; + case 64: + if (was_enabled && !enabled) + { + sampler_channel_stop_sustained(c); + } + break; + case 66: + if (was_enabled && !enabled) + sampler_channel_stop_sostenuto(c); + else if (!was_enabled && enabled) + sampler_channel_capture_sostenuto(c); + break; + + case 120: + case 123: + sampler_channel_stop_all(c); + break; + case 121: + // Recommended Practice (RP-015) Response to Reset All Controllers + // http://www.midi.org/techspecs/rp15.php + sampler_channel_process_cc(c, 64, 0); + sampler_channel_process_cc(c, 66, 0); + set_cc_int(c, 11, 127); + set_cc_int(c, 1, 0); + set_cc_float(c, smsrc_alternate, 0); + c->pitchwheel = 0; + c->last_chanaft = 0; + c->poly_pressure_mask = 0; + c->last_polyaft = 0; + sampler_channel_reset_keyswitches(c); + return; + } + if (cc < smsrc_perchan_count) + set_cc_int(c, cc, val); +} + +void sampler_channel_release_groups(struct sampler_channel *c, int note, struct sampler_released_groups *exgroups) +{ + if (exgroups->group_count || exgroups->low_groups) + { + FOREACH_VOICE(c->voices_running, v) + { + if (v->off_by && v->note != note) + { + if (sampler_released_groups_check(exgroups, v->off_by)) + { + v->released = 1; + if (v->layer->off_mode == som_fast) + cbox_envelope_go_to(&v->amp_env, 15); + } + } + } + } +} + +void sampler_channel_start_note(struct sampler_channel *c, int note, int vel, gboolean is_release_trigger) +{ + struct sampler_module *m = c->module; + float random = rand() * 1.0 / (RAND_MAX + 1.0); + + set_cc_float(c, smsrc_alternate, c->intcc[smsrc_alternate] ? 0.f : 1.f); + set_cc_float(c, smsrc_random_unipolar, random); // is that a per-voice or per-channel value? is it generated per region or per note? + + gboolean is_first = FALSE; + if (!is_release_trigger) + { + c->switchmask[note >> 5] |= 1 << (note & 31); + c->prev_note_velocity[note] = vel; + c->prev_note_start_time[note] = m->current_time; + is_first = TRUE; + FOREACH_VOICE(c->voices_running, v) + { + if (!v->released && v->layer->trigger != stm_release) + { + is_first = FALSE; + break; + } + } + } + struct sampler_program *prg = c->program; + if (!prg || !prg->rll || prg->deleting) + return; + + if (!is_release_trigger) + { + for (uint32_t i = 0; i < prg->rll->keyswitch_group_count; ++i) + { + const struct sampler_keyswitch_group *ks = prg->rll->keyswitch_groups[i]; + if (note >= ks->lo && note <= ks->hi) + c->keyswitch_state[i] = ks->key_offsets[note - ks->lo]; + } + } + + struct sampler_rll_iterator iter; + sampler_rll_iterator_init(&iter, prg->rll, c, note, vel, random, is_first, is_release_trigger); + + struct sampler_layer *layer = sampler_rll_iterator_next(&iter); + if (!layer) + { + if (!is_release_trigger) + c->previous_note = note; + return; + } + struct sampler_layer_data *layers[MAX_SAMPLER_VOICES]; + struct sampler_layer_data *delayed_layers[MAX_SAMPLER_PREVOICES]; + int lcount = 0, dlcount = 0, slcount = 0; + struct sampler_voice *free_voice = m->voices_free; + struct sampler_prevoice *free_prevoice = m->prevoices_free; + int fvcount = 0, fpcount = 0; + + while(layer && lcount < MAX_SAMPLER_VOICES + MAX_SAMPLER_PREVOICES) + { + if (free_voice) + { + free_voice = free_voice->next; + fvcount++; + } + if (free_prevoice) + { + free_prevoice = free_prevoice->next; + fpcount++; + } + assert(layer->runtime); + if (layer->runtime->computed.eff_use_prevoice) + delayed_layers[dlcount++] = layer->runtime; + else + { + layers[lcount++] = layer->runtime; + if (layer->runtime->computed.eff_is_silent) + slcount++; + } + layer = sampler_rll_iterator_next(&iter); + } + + struct sampler_released_groups exgroups; + sampler_released_groups_init(&exgroups); + // If running out of polyphony, do not start the note if all the regions cannot be played + // (silent notes don't count into polyphony) + if (lcount <= slcount + fvcount && dlcount <= fpcount) + { + // this might perhaps be optimized by mapping the group identifiers to flat-array indexes + // but I'm not going to do that until someone gives me an SFZ worth doing that work ;) + + for (int i = 0; i < lcount; ++i) + { + struct sampler_layer_data *l = layers[i]; + if (l->computed.eff_is_silent) + sampler_voice_start_silent(l, &exgroups); + else + { + int velc = (!is_first && l->vel_mode == svm_previous) ? c->first_note_vel : vel; + sampler_voice_start(m->voices_free, c, l, note, velc, &exgroups); + } + } + for (int i = 0; i < dlcount; ++i) + { + struct sampler_layer_data *l = delayed_layers[i]; + int velc = (!is_first && l->vel_mode == svm_previous) ? c->first_note_vel : vel; + sampler_prevoice_start(m->prevoices_free, c, l, note, velc); + } + } + if (!is_release_trigger) + c->previous_note = note; + if (is_first) + c->first_note_vel = vel; + sampler_channel_release_groups(c, note, &exgroups); +} + +void sampler_channel_start_release_triggered_voices(struct sampler_channel *c, int note) +{ + if (c->program && c->program->rll && c->program->rll->has_release_layers) + { + if (c->prev_note_velocity[note]) + { + sampler_channel_start_note(c, note, c->prev_note_velocity[note], TRUE); + c->prev_note_velocity[note] = 0; + } + } +} + +void sampler_channel_stop_note(struct sampler_channel *c, int note, int vel, gboolean is_polyaft) +{ + c->switchmask[note >> 5] &= ~(1 << (note & 31)); + FOREACH_PREVOICE(c->module->prevoices_running, pv) + { + if (pv->note == note) + sampler_prevoice_unlink(&c->module->prevoices_running, pv); + } + FOREACH_VOICE(c->voices_running, v) + { + if (v->note == note && v->layer->trigger != stm_release) + { + v->off_vel = vel; + if (v->captured_sostenuto) + v->released_with_sostenuto = 1; + else if (c->intcc[64] >= 64) + v->released_with_sustain = 1; + else + sampler_voice_release(v, is_polyaft); + + } + } + if (c->intcc[64] < 64) + sampler_channel_start_release_triggered_voices(c, note); + else + c->sustainmask[note >> 5] |= (1 << (note & 31)); +} + +void sampler_channel_stop_sustained(struct sampler_channel *c) +{ + FOREACH_VOICE(c->voices_running, v) + { + if (v->channel == c && v->released_with_sustain && v->layer->trigger != stm_release) + { + sampler_voice_release(v, FALSE); + v->released_with_sustain = 0; + } + } + // Start release layers for the newly released keys + if (c->program && c->program->rll && c->program->rll->has_release_layers) + { + for (int i = 0; i < 128; i++) + { + if (c->sustainmask[i >> 5] & (1 << (i & 31))) + sampler_channel_start_release_triggered_voices(c, i); + } + } + memset(c->sustainmask, 0, sizeof(c->sustainmask)); +} + +void sampler_channel_stop_sostenuto(struct sampler_channel *c) +{ + FOREACH_VOICE(c->voices_running, v) + { + if (v->released_with_sostenuto && v->layer->trigger != stm_release) + { + sampler_channel_start_release_triggered_voices(c, v->note); + sampler_voice_release(v, FALSE); + v->released_with_sostenuto = 0; + // XXXKF unsure what to do with sustain + } + } + // Start release layers for the newly released keys + if (c->program && c->program->rll && c->program->rll->has_release_layers) + { + for (int i = 0; i < 128; i++) + { + if (c->sostenutomask[i >> 5] & (1 << (i & 31))) + sampler_channel_start_release_triggered_voices(c, i); + } + } + memset(c->sostenutomask, 0, sizeof(c->sostenutomask)); +} + +void sampler_channel_capture_sostenuto(struct sampler_channel *c) +{ + FOREACH_VOICE(c->voices_running, v) + { + if (!v->released && v->loop_mode != slm_one_shot && v->loop_mode != slm_one_shot_chokeable && !v->layer->count) + { + // XXXKF unsure what to do with sustain + v->captured_sostenuto = 1; + c->sostenutomask[v->note >> 5] |= (1 << (v->note & 31)); + } + } +} + +void sampler_channel_stop_all(struct sampler_channel *c) +{ + FOREACH_VOICE(c->voices_running, v) + { + sampler_voice_release(v, v->loop_mode == slm_one_shot_chokeable); + v->released_with_sustain = 0; + v->released_with_sostenuto = 0; + v->captured_sostenuto = 0; + } +} + +void sampler_channel_set_program_RT(struct sampler_channel *c, struct sampler_program *prg) +{ + FOREACH_PREVOICE(c->module->prevoices_running, pv) + { + if (pv->channel == c) + { + sampler_prevoice_unlink(&c->module->prevoices_running, pv); + sampler_prevoice_link(&c->module->prevoices_free, pv); + } + } + if (c->program) + c->program->in_use--; + c->program = prg; + if (prg) + { + assert(prg->rll); + sampler_channel_reset_keyswitches(c); + for(GSList *p = prg->ctrl_init_list; p; p = p->next) + { + union sampler_ctrlinit_union u; + u.ptr = p->data; + // printf("Setting controller %d -> %d\n", u.cinit.controller, u.cinit.value); + set_cc_int(c, u.cinit.controller, u.cinit.value); + } + c->program->in_use++; + } +} + +#define sampler_channel_set_program_args(ARG) ARG(struct sampler_program *, prg) + +DEFINE_RT_VOID_FUNC(sampler_channel, c, sampler_channel_set_program) +{ + sampler_channel_set_program_RT(c, prg); +} + +void sampler_channel_program_change(struct sampler_channel *c, int program) +{ + struct sampler_module *m = c->module; + // XXXKF replace with something more efficient + for (uint32_t i = 0; i < m->program_count; i++) + { + // XXXKF support banks + if (m->programs[i]->prog_no == program) + { + sampler_channel_set_program_RT(c, m->programs[i]); + return; + } + } + g_warning("Unknown program %d", program); + if (m->program_count) + sampler_channel_set_program_RT(c, m->programs[0]); +} + +float sampler_channel_get_expensive_cc(struct sampler_channel *c, struct sampler_voice *v, struct sampler_prevoice *pv, int cc_no) +{ + switch(cc_no) + { + case smsrc_pitchbend: + return c->pitchwheel / 8191.f; + case smsrc_lastpolyaft: // how this is defined? is it last or is it current voice's? + return sampler_channel_get_poly_pressure(c, v ? v->note : (pv ? pv->note : 0)); + case smsrc_noteonvel: + return v ? v->vel / 127.0 : (pv ? pv->vel / 127.0 : 0); + case smsrc_noteoffvel: + return v ? v->off_vel / 127.0 : 0; + case smsrc_keynotenum: + return v ? v->note / 127.0 : (pv ? pv->note / 127.0 : 0); + case smsrc_keynotegate: + return c->switchmask[0] || c->switchmask[1] || c->switchmask[2] || c->switchmask[3]; // XXXKF test interactions with sustain/sostenuto + case smsrc_chanaft_sfz2: + return c->last_chanaft / 127.0; + case smsrc_random_unipolar: + case smsrc_alternate: + return c->floatcc[cc_no]; + case smsrc_random_bipolar: + return -1 + 2 * c->floatcc[smsrc_random_unipolar]; + case smsrc_keydelta: // not supported yet + return 0; + case smsrc_keydelta_abs: + return 0; + case smsrc_tempo: + return c->module->module.engine->master->tempo; // XXXKF what scale??? + default: + assert(0); + return 0.f; + } +} diff --git a/template/calfbox/sampler_gen.c b/template/calfbox/sampler_gen.c new file mode 100644 index 0000000..79c99b7 --- /dev/null +++ b/template/calfbox/sampler_gen.c @@ -0,0 +1,509 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "dspmath.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sfzloader.h" +#include +#include +#include +#include +#include + +#define LOW_QUALITY_INTERPOLATION 0 + +struct resampler_state +{ + float *leftright; + int offset; + float lgain, rgain, lgain_delta, rgain_delta; +}; + +#if USE_NEON + +#include + +static inline void process_voice_mono_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, int endpos) +{ + static const float32x2_t shift1a = {0.f, 1.f}, shift1b = {1.f, 1.f}; + static const float32x2_t shift2a = {-1.f, -1.f}, shift2b = {0.f, 0.f}; + static const float32x2_t shift3a = {-2.f, -2.f}, shift3b = {-2.f, -1.f}; + static const float32x2_t scalinga = {-1 / 6.0, 3 / 6.0}, scalingb = {-3 / 6.0, 1 / 6.0}; + uint64x1_t pos = v->bigpos, delta = v->bigdelta; + float32x2_t gains = {rs->lgain, rs->rgain}; + const float32x2_t gaindeltas = {rs->lgain_delta, rs->rgain_delta}; + for (uint32_t i = rs->offset; i < endpos; i++) + { + float32x2_t posposf = vcvt_n_f32_u32(vreinterpret_u32_u64(pos), 32); + + int32x4_t smp = vmovl_s16(vld1_s16(&srcdata[pos >> 32])); + pos = vadd_u64(pos, delta); + + float32x2_t t2 = vdup_n_f32(posposf[0]); + float32x2_t samplesa = vcvt_f32_s32(vget_low_s32(smp)), samplesb = vcvt_f32_s32(vget_high_s32(smp)); + + float32x2_t mula = vmul_f32(vmul_f32(vadd_f32(t2, shift1a), vadd_f32(t2, shift2a)), vmul_f32(vadd_f32(t2, shift3a), scalinga)); + float32x2_t mulb = vmul_f32(vmul_f32(vadd_f32(t2, shift1b), vadd_f32(t2, shift2b)), vmul_f32(vadd_f32(t2, shift3b), scalingb)); + float32x2_t v = vmla_f32(vmul_f32(samplesa, mula), samplesb, mulb); + float32x2_t result = vmul_f32(gains, vadd_f32(v, vrev64_f32(v))); + gains = vadd_f32(gains, gaindeltas); + + rs->leftright[2 * i] = result[0]; + rs->leftright[2 * i + 1] = result[1]; + } + rs->lgain = gains[0]; + rs->rgain = gains[1]; + v->bigpos = pos; + rs->offset = endpos; +} + +static inline void process_voice_stereo_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, int endpos) +{ + static const float32x2_t shift1a = {0.f, 1.f}, shift1b = {1.f, 1.f}; + static const float32x2_t shift2a = {-1.f, -1.f}, shift2b = {0.f, 0.f}; + static const float32x2_t shift3a = {-2.f, -2.f}, shift3b = {-2.f, -1.f}; + static const float32x2_t scalinga = {-1 / 6.0, 3 / 6.0}, scalingb = {-3 / 6.0, 1 / 6.0}; + uint64x1_t pos = v->bigpos, delta = v->bigdelta; + float32x2_t gains = {rs->lgain, rs->rgain}; + const float32x2_t gaindeltas = {rs->lgain_delta, rs->rgain_delta}; + for (uint32_t i = rs->offset; i < endpos; i++) + { + float32x2_t posposf = vcvt_n_f32_u32(vreinterpret_u32_u64(pos), 32); + + int16x4x2_t pp = vld2_s16(&srcdata[(pos >> 31) &~ 1]); + pos = vadd_u64(pos, delta); + int32x4_t smp_left = vmovl_s16(pp.val[0]), smp_right = vmovl_s16(pp.val[1]); + + float32x2_t t2 = vdup_n_f32(posposf[0]); + float32x2_t samplesLa = vcvt_f32_s32(vget_low_s32(smp_left)), samplesLb = vcvt_f32_s32(vget_high_s32(smp_left)); + float32x2_t samplesRa = vcvt_f32_s32(vget_low_s32(smp_right)), samplesRb = vcvt_f32_s32(vget_high_s32(smp_right)); + + float32x2_t mula = vmul_f32(vmul_f32(vadd_f32(t2, shift1a), vadd_f32(t2, shift2a)), vmul_f32(vadd_f32(t2, shift3a), scalinga)); + float32x2_t mulb = vmul_f32(vmul_f32(vadd_f32(t2, shift1b), vadd_f32(t2, shift2b)), vmul_f32(vadd_f32(t2, shift3b), scalingb)); + float32x2_t vL = vmla_f32(vmul_f32(samplesLa, mula), samplesLb, mulb); + float32x2_t vR = vmla_f32(vmul_f32(samplesRa, mula), samplesRb, mulb); + float32x2x2_t transposed = vtrn_f32(vL, vR); + float32x2_t result = vmul_f32(gains, vadd_f32(transposed.val[0], transposed.val[1])); + gains = vadd_f32(gains, gaindeltas); + + rs->leftright[2 * i] = result[0]; + rs->leftright[2 * i + 1] = result[1]; + } + rs->lgain = gains[0]; + rs->rgain = gains[1]; + v->bigpos = pos; + rs->offset = endpos; +} + +#elif USE_SSE + +#include + +typedef __m128 V4SF; + +static const V4SF shift1 = {0, 1, 1, 1}; +static const V4SF shift2 = {-1, -1, 0, 0}; +static const V4SF shift3 = {-2, -2, -2, -1}; +static const V4SF scaling = {-1, 3, -3, 1}; +static const V4SF zero = {0, 0, 0, 0}; + + +static inline void process_voice_mono_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, int endpos) +{ + uint64_t pos = v->bigpos; + const float ffrac = 1.0f / 6.0f; + const float _scaler = 1.f / (128.f * 16777216.f); + for (int i = rs->offset; i < endpos; i++) + { + //float t = ((pos >> 8) & 0x00FFFFFF) * scaler; + const int16_t *p = &srcdata[pos >> 32]; + + V4SF t2 = __builtin_ia32_cvtsi2ss(zero, (pos & 0xFFFFFFFF) >> 1) * _scaler; + pos += v->bigdelta; + + V4SF t4 = __builtin_ia32_shufps(t2, t2, 0); + V4SF v4mul = (t4 + shift1) * (t4 + shift2) * (t4 + shift3) * scaling; + V4SF samples = {p[0], p[1], p[2], p[3]}; + v4mul = __builtin_ia32_mulps(samples, v4mul); + + float c = (v4mul[0] + v4mul[1] + v4mul[2] + v4mul[3]) * ffrac; + + rs->leftright[2 * i] = rs->lgain * c; + rs->leftright[2 * i + 1] = rs->rgain * c; + rs->lgain += rs->lgain_delta; + rs->rgain += rs->rgain_delta; + } + v->bigpos = pos; + rs->offset = endpos; +} + +static inline void process_voice_stereo_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, int endpos) +{ + uint64_t pos = v->bigpos; + const float ffrac = 1.0f / 6.0f; + const float _scaler = 1.f / (128.f * 16777216.f); + for (int i = rs->offset; i < endpos; i++) + { + //float t = ((pos >> 8) & 0x00FFFFFF) * scaler; + const int16_t *p = &srcdata[(pos >> 31) & ~1]; + + V4SF t2 = __builtin_ia32_cvtsi2ss(zero, (pos & 0xFFFFFFFF) >> 1) * _scaler; + pos += v->bigdelta; + + V4SF t4 = __builtin_ia32_shufps(t2, t2, 0); + V4SF v4mul = (t4 + shift1) * (t4 + shift2) * (t4 + shift3) * scaling; + V4SF samples_left = {p[0], p[2], p[4], p[6]}; + samples_left = __builtin_ia32_mulps(samples_left, v4mul); + V4SF samples_right = {p[1], p[3], p[5], p[7]}; + samples_right = __builtin_ia32_mulps(samples_right, v4mul); + + float cl = (samples_left[0] + samples_left[1] + samples_left[2] + samples_left[3]) * ffrac; + float cr = (samples_right[0] + samples_right[1] + samples_right[2] + samples_right[3]) * ffrac; + + rs->leftright[2 * i] = rs->lgain * cl; + rs->leftright[2 * i + 1] = rs->rgain * cr; + rs->lgain += rs->lgain_delta; + rs->rgain += rs->rgain_delta; + } + v->bigpos = pos; + rs->offset = endpos; +} + +#else + +static inline void process_voice_mono_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, int endpos) +{ + const float ffrac = 1.0f / 6.0f; + const float scaler = 1.f / 16777216.f; + + for (int i = rs->offset; i < endpos; i++) + { + float t = ((v->bigpos >> 8) & 0x00FFFFFF) * scaler; + const int16_t *p = &srcdata[v->bigpos >> 32]; +#if LOW_QUALITY_INTERPOLATION + float c = (1.f - t) * p[1] + t * p[2]; +#else + float b0 = -t*(t-1.f)*(t-2.f); + float b1 = 3.f*(t+1.f)*(t-1.f)*(t-2.f); + float c = (b0 * p[0] + b1 * p[1] - 3.f*(t+1.f)*t*(t-2.f) * p[2] + (t+1.f)*t*(t-1.f) * p[3]) * ffrac; +#endif + rs->leftright[2 * i] = rs->lgain * c; + rs->leftright[2 * i + 1] = rs->rgain * c; + rs->lgain += rs->lgain_delta; + rs->rgain += rs->rgain_delta; + v->bigpos += v->bigdelta; + } + rs->offset = endpos; +} + +static inline void process_voice_stereo_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, int endpos) +{ + const float ffrac = 1.0f / 6.0f; + const float scaler = 1.f / 16777216.f; + + for (int i = rs->offset; i < endpos; i++) + { + float t = ((v->bigpos >> 8) & 0x00FFFFFF) * scaler; + const int16_t *p = &srcdata[(v->bigpos >> 31) & ~1]; +#if LOW_QUALITY_INTERPOLATION + float c0 = (1.f - t) * p[2] + t * p[4]; + float c1 = (1.f - t) * p[3] + t * p[5]; +#else + float b0 = -t*(t-1.f)*(t-2.f); + float b1 = 3.f*(t+1.f)*(t-1.f)*(t-2.f); + float c0 = (b0 * p[0] + b1 * p[2] - 3.f*(t+1.f)*t*(t-2.f) * p[4] + (t+1.f)*t*(t-1.f) * p[6]) * ffrac; + float c1 = (b0 * p[1] + b1 * p[3] - 3.f*(t+1.f)*t*(t-2.f) * p[5] + (t+1.f)*t*(t-1.f) * p[7]) * ffrac; +#endif + rs->leftright[2 * i] = rs->lgain * c0; + rs->leftright[2 * i + 1] = rs->rgain * c1; + rs->lgain += rs->lgain_delta; + rs->rgain += rs->rgain_delta; + v->bigpos += v->bigdelta; + } + rs->offset = endpos; +} + +#endif + +static inline uint32_t process_voice_noloop(struct sampler_gen *v, struct resampler_state *rs, const int16_t *srcdata, uint32_t pos_offset, uint32_t usable_sample_end) +{ + uint32_t out_frames = CBOX_BLOCK_SIZE - rs->offset; + + uint64_t sample_end64 = ((uint64_t)usable_sample_end) << 32; + // Check how many frames can be written to output buffer without going + // past usable_sample_end. + if (__builtin_expect(v->bigpos + (out_frames - 1) * v->bigdelta >= sample_end64, 0)) + out_frames = (sample_end64 - v->bigpos) / v->bigdelta + 1; + + assert(out_frames > 0 && out_frames <= (uint32_t)(CBOX_BLOCK_SIZE - rs->offset)); + uint32_t oldpos = v->bigpos >> 32; + if (v->mode == spt_stereo16) + process_voice_stereo_noloop(v, rs, srcdata - (pos_offset << 1), rs->offset + out_frames); + else + process_voice_mono_noloop(v, rs, srcdata - pos_offset, rs->offset + out_frames); + return (v->bigpos >> 32) - oldpos; +} + +static void process_voice_withloop(struct sampler_gen *v, struct resampler_state *rs) +{ + // This is the first frame where interpolation will cross the loop boundary + uint32_t loop_end = v->loop_end; + uint32_t loop_edge = loop_end - MAX_INTERPOLATION_ORDER; + + while ( rs->offset < CBOX_BLOCK_SIZE ) { + uint64_t startframe = v->bigpos >> 32; + + int16_t *source_data = v->sample_data; + uint32_t source_offset = 0; + uint32_t usable_sample_end = loop_edge; + // if the first frame to play is already within 3 frames of loop end + // (we need consecutive 4 frames for cubic interpolation) then + // "straighten out" the area around the loop, and play that + if (__builtin_expect(startframe >= loop_edge, 0)) + { + // if fully past the loop end, then it's normal wraparound + // (or end of the sample if not looping) + if (startframe >= loop_end) + { + if (v->loop_start == (uint32_t)-1) + { + v->mode = spt_inactive; + return; + } + v->play_count++; + if (v->loop_count && v->play_count >= v->loop_count) + { + v->mode = spt_inactive; + return; + } + v->bigpos -= (uint64_t)(loop_end - v->loop_start) << 32; + continue; + } + + usable_sample_end = loop_end; + source_data = v->scratch; + source_offset = loop_edge; + } + + process_voice_noloop(v, rs, source_data, source_offset, usable_sample_end); + } +} + +static void process_voice_streaming(struct sampler_gen *v, struct resampler_state *rs, uint32_t limit) +{ + if (v->consumed_credit > 0) + { + if (v->consumed_credit >= limit) + { + v->consumed_credit -= limit; + return; + } + limit -= v->consumed_credit; + v->consumed_credit = 0; + } + // This is the first frame where interpolation will cross the loop boundary + int16_t scratch[2 * MAX_INTERPOLATION_ORDER * 2]; + + while ( limit && rs->offset < CBOX_BLOCK_SIZE ) { + uint64_t startframe = v->bigpos >> 32; + + int16_t *source_data = v->in_streaming_buffer ? v->streaming_buffer : v->sample_data; + uint32_t loop_start = v->in_streaming_buffer ? 0 : v->loop_start; + uint32_t loop_end = v->in_streaming_buffer ? v->streaming_buffer_frames : v->loop_end; + uint32_t loop_edge = loop_end - MAX_INTERPOLATION_ORDER; + uint32_t source_offset = 0; + uint32_t usable_sample_end = loop_edge; + // if the first frame to play is already within 3 frames of loop end + // (we need consecutive 4 frames for cubic interpolation) then + // "straighten out" the area around the loop, and play that + if (startframe >= loop_edge) + { + // if fully past the loop end, then it's normal wraparound + // (or end of the sample if not looping) + if (startframe >= loop_end) + { + if (v->loop_start == (uint32_t)-1) + { + v->mode = spt_inactive; + return; + } + v->bigpos -= (uint64_t)(loop_end - loop_start) << 32; + if (v->prefetch_only_loop) + v->consumed -= (loop_end - loop_start); + else + v->in_streaming_buffer = TRUE; + continue; + } + + int shift = (v->mode == spt_stereo16) ? 1 : 0; + + // 'linearize' the virtual circular buffer - write 3 (or N) frames before end of the loop + // and 3 (N) frames at the start of the loop, and play it; in rare cases this will need to be + // repeated twice if output write pointer is close to CBOX_BLOCK_SIZE or playback rate is very low, + // but that's OK. + uint32_t halfscratch = MAX_INTERPOLATION_ORDER << shift; + memcpy(&scratch[0], &source_data[loop_edge << shift], halfscratch * sizeof(int16_t) ); + if (v->loop_start == (uint32_t)-1) + memset(scratch + halfscratch, 0, halfscratch * sizeof(int16_t)); + else + memcpy(scratch + halfscratch, &v->streaming_buffer[v->loop_start << shift], halfscratch * sizeof(int16_t)); + + usable_sample_end = loop_end; + source_data = scratch; + source_offset = loop_edge; + } + if (limit != (uint32_t)-1 && usable_sample_end - startframe > limit) + usable_sample_end = startframe + limit; + + uint32_t consumed = process_voice_noloop(v, rs, source_data, source_offset, usable_sample_end); + if (consumed > limit) + { + // The number of frames 'consumed' may be greater than the amount + // available because of sample-skipping (at least that's the only + // *legitimate* reason). This should be accounted for in the, + // consumed sample counter (hence temporary storage of the + // 'buffer overconsumption' in the consumed_credit field), but is not + // actually causing any use of missing data, as the missing samples + // have been skipped. + assert(v->consumed_credit == 0); + v->consumed_credit = consumed - limit; + assert (v->consumed_credit <= 1 + (v->bigdelta >> 32)); + consumed = limit; + } + v->consumed += consumed; + if (consumed < limit) + limit -= consumed; + else + break; + } +} + +void sampler_gen_reset(struct sampler_gen *v) +{ + v->mode = spt_inactive; + v->bigpos = 0; + v->last_lgain = 0.f; + v->last_rgain = 0.f; + v->play_count = 0; + v->consumed = 0; + v->consumed_credit = 0; + v->streaming_buffer = NULL; + v->in_streaming_buffer = FALSE; + v->prefetch_only_loop = FALSE; + v->fadein_counter = -1.f; +} + +uint32_t sampler_gen_sample_playback(struct sampler_gen *v, float *leftright, uint32_t limit) +{ + struct resampler_state rs; + rs.leftright = leftright; + rs.offset = 0; + rs.lgain = v->last_lgain; + rs.rgain = v->last_rgain; + rs.lgain_delta = (v->lgain - v->last_lgain) * (1.f / CBOX_BLOCK_SIZE); + rs.rgain_delta = (v->rgain - v->last_rgain) * (1.f / CBOX_BLOCK_SIZE); + if (v->streaming_buffer) + process_voice_streaming(v, &rs, limit); + else + { + process_voice_withloop(v, &rs); + } + uint32_t written = rs.offset; + + if (!v->streaming_buffer) + { + v->virtpos += written * v->virtdelta; + if (v->virtpos != v->bigpos) + { + while ((v->virtpos >> 32) >= v->loop_end && v->loop_start != SAMPLER_NO_LOOP) + v->virtpos -= ((uint64_t)(v->loop_end - v->loop_start)) << 32; + } + // XXXKF looping + if (v->fadein_counter == -1 && fabs((v->bigpos - v->virtpos) / (65536.0 * 65536.0)) > v->stretching_jump) + { + int64_t jump = (int64_t)(v->stretching_jump * 65536.0 * 65536.0); + int64_t newpos = v->bigpos > v->virtpos ? v->bigpos - jump : v->bigpos + jump; + if (newpos < 0) + newpos = 0; + // XXXKF beware of extremely short loops + while ((newpos >> 32) >= v->loop_end && v->loop_start != SAMPLER_NO_LOOP) + newpos -= ((uint64_t)(v->loop_end - v->loop_start)) << 32; + if ((newpos >> 32) >= v->cur_sample_end - 4) + newpos = ((uint64_t)v->cur_sample_end - 4)<< 32; + v->fadein_pos = newpos; + v->fadein_counter = 0; + } + else if (v->fadein_counter != -1) + { + float leftright_fadein[2 * CBOX_BLOCK_SIZE]; + + rs.offset = 0; + rs.leftright = leftright_fadein; + rs.lgain = v->last_lgain; + rs.rgain = v->last_rgain; + + uint64_t oldpos = v->bigpos; + v->bigpos = v->fadein_pos; + process_voice_withloop(v, &rs); + v->fadein_pos = v->bigpos; + v->bigpos = oldpos; + + uint32_t written2 = rs.offset; + + // XXXKF not the best set of special cases + uint32_t i; + if (written2 > written) + { + for (i = 2 * written; i < 2 * written2; i += 2) + leftright[i] = leftright[i + 1] = 0.f; + written = written2; + } + if (written2 < written) + { + for (i = 2 * written2; i < 2 * written; i += 2) + leftright_fadein[i] = leftright_fadein[i + 1] = 0.f; + written2 = written; + } + float cnt = v->fadein_counter; + float scl = v->bigdelta / (v->stretching_crossfade * v->virtdelta); + for (i = 0; i < 2 * written2; i += 2) + { + leftright[i] += (leftright_fadein[i] - leftright[i]) * cnt; + leftright[i + 1] += (leftright_fadein[i + 1] - leftright[i + 1]) * cnt; + cnt += scl; + if (cnt > 1.f) + cnt = 1.f; + } + if (cnt >= 1.f) + { + cnt = -1.f; + v->bigpos = v->fadein_pos; + } + v->fadein_counter = cnt; + } + } + v->last_lgain = v->lgain; + v->last_rgain = v->rgain; + return written; +} + diff --git a/template/calfbox/sampler_impl.h b/template/calfbox/sampler_impl.h new file mode 100644 index 0000000..4e28748 --- /dev/null +++ b/template/calfbox/sampler_impl.h @@ -0,0 +1,68 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SAMPLER_IMPL_H +#define CBOX_SAMPLER_IMPL_H + +extern void sampler_gen_reset(struct sampler_gen *v); +extern uint32_t sampler_gen_sample_playback(struct sampler_gen *v, float *leftright, uint32_t limit); +extern void sampler_program_change_byidx(struct sampler_module *m, struct sampler_channel *c, int program_idx); +extern void sampler_program_change(struct sampler_module *m, struct sampler_channel *c, int program); + +static inline int sfz_note_from_string(const char *note) +{ + static const int semis[] = {9, 11, 0, 2, 4, 5, 7}; + int pos; + int nn = tolower(note[0]); + int nv; + if ((nn >= '0' && nn <= '9') || nn == '-') + { + int nv = atoi(note); + if (nv >= -1 && nv <= 127) + return nv; + return -2; + } + if (nn < 'a' || nn > 'g') + return -2; + nv = semis[nn - 'a']; + + for (pos = 1; tolower(note[pos]) == 'b' || note[pos] == '#'; pos++) + nv += (note[pos] != '#') ? -1 : +1; + + if ((note[pos] == '-' && note[pos + 1] == '1' && note[pos + 2] == '\0') || (note[pos] >= '0' && note[pos] <= '9' && note[pos + 1] == '\0')) + { + return nv + 12 * (1 + atoi(note + pos)); + } + + return -2; +} + +static inline gboolean atof_C_verify(const char *key, const char *value, double *result, GError **error) +{ + char *endptr = NULL; + double res = g_ascii_strtod(value, &endptr); + if (endptr && !*endptr && endptr != value) + { + *result = res; + return TRUE; + } + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "'%s' is not a correct numeric value for %s", value, key); + return FALSE; +} + +#endif diff --git a/template/calfbox/sampler_layer.c b/template/calfbox/sampler_layer.c new file mode 100644 index 0000000..4637d3d --- /dev/null +++ b/template/calfbox/sampler_layer.c @@ -0,0 +1,2020 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "dspmath.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_impl.h" +#include "sfzloader.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static inline gboolean sampler_modulation_key_equal(const struct sampler_modulation_key *k1, const struct sampler_modulation_key *k2) +{ + return (k1->src == k2->src && k1->src2 == k2->src2 && k1->dest == k2->dest); +} + +static inline void sampler_modulation_dump_one(const struct sampler_modulation *sm) +{ + printf("%d x %d -> %d : %f : %d\n", sm->key.src, sm->key.src2, sm->key.dest, sm->value.amount, sm->value.curve_id); +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +static inline gboolean sampler_noteinitfunc_key_equal(const struct sampler_noteinitfunc_key *k1, const struct sampler_noteinitfunc_key *k2) +{ + return (k1->notefunc_voice == k2->notefunc_voice && k1->variant == k2->variant); +} + +static inline void sampler_noteinitfunc_dump_one(const struct sampler_noteinitfunc *nif) +{ + printf("%p(%d) = %f\n", nif->key.notefunc_voice, nif->key.variant, nif->value.value); +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +static inline gboolean sampler_cc_range_key_equal(const struct sampler_cc_range_key *k1, const struct sampler_cc_range_key *k2) +{ + return k1->cc_number == k2->cc_number; +} + +static inline void sampler_cc_range_dump_one(const struct sampler_cc_range *ccrange) +{ + printf("CC%d in [%c%d, %c%d]\n", (int)ccrange->key.cc_number, ccrange->value.has_locc ? '!' : '.', (int)ccrange->value.locc, ccrange->value.has_hicc ? '!' : '.', (int)ccrange->value.hicc); +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +static inline gboolean sampler_flex_lfo_key_equal(const struct sampler_flex_lfo_key *k1, const struct sampler_flex_lfo_key *k2) +{ + return k1->id == k2->id; +} + +static inline void sampler_flex_lfo_dump_one(const struct sampler_flex_lfo *lfo) +{ + printf("LFO%d (freq %s %f, delay %s %f, fade %s %f, wave %s %d)\n", + (int)lfo->key.id, + lfo->value.has_freq ? "(local)" : "(inherited)", lfo->value.freq, + lfo->value.has_delay ? "(local)" : "(inherited)", lfo->value.delay, + lfo->value.has_fade ? "(local)" : "(inherited)", lfo->value.fade, + lfo->value.has_wave ? "(local)" : "(inherited)", lfo->value.wave + ); +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +#define SAMPLER_COLL_FUNC_DUMP(sname) \ + void sname##_dump(const struct sname *p) \ + { \ + for(; p; p = p->next) \ + sname##_dump_one(p); \ + } + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_DUMP) + +#define SAMPLER_COLL_FUNC_FIND(sname) \ + static struct sname *sname##_find(struct sname *list, const struct sname##_key *key) \ + { \ + for(struct sname *p = list; p; p = p->next) \ + { \ + struct sname##_key *dkey = &p->key; \ + if (sname##_key_equal(dkey, key)) \ + return p; \ + } \ + return NULL; \ + } \ + static struct sname *sname##_find2(struct sname **list_ptr, const struct sname##_key *key, struct sname ***link_ptr) \ + { \ + for(struct sname **pp = list_ptr; *pp; pp = &(*pp)->next) \ + { \ + struct sname##_key *dkey = &(*pp)->key; \ + if (sname##_key_equal(dkey, key)) \ + { \ + if (link_ptr) \ + *link_ptr = pp; \ + return *pp; \ + } \ + } \ + return NULL; \ + } + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_FIND) + +#define SAMPLER_COLL_FIELD_INIT(name, has_name, type, init_value) \ + d->value.name = init_value; \ + d->value.has_name = FALSE; + +#define SAMPLER_COLL_FUNC_ADD(sname) \ +static struct sname *sname##_add(struct sname **list_ptr, const struct sname##_key *key) \ +{ \ + struct sname *d = sname##_find(*list_ptr, key); \ + if (d) \ + return d; \ + d = g_malloc0(sizeof(struct sname)); \ + d->key = *key; \ + SAMPLER_COLL_FIELD_LIST_##sname(SAMPLER_COLL_FIELD_INIT)\ + d->next = *list_ptr; \ + *list_ptr = d; \ + return d; \ +} + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_ADD) + +#define SAMPLER_COLL_FUNC_DESTROY(sname) \ +static void sname##s_destroy(struct sname *list_ptr) \ +{ \ + while(list_ptr) \ + { \ + struct sname *p = list_ptr->next; \ + g_free(list_ptr); \ + list_ptr = p; \ + } \ +} + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_DESTROY) + +#define SAMPLER_COLL_FIELD_ISNULL(name, has_name, type, init_value) \ + if (d->name != init_value || d->has_name) \ + return FALSE; + +#define SAMPLER_COLL_FUNC_ISNULL(sname) \ + static inline gboolean sname##_is_null(const struct sname##_value *d) \ + { \ + SAMPLER_COLL_FIELD_LIST_##sname(SAMPLER_COLL_FIELD_ISNULL) \ + return TRUE; \ + } + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_ISNULL) + +// sampler_modulation_set_amount etc. +#define SAMPLER_COLL_FIELD_SETTER(name, has_name, type, init_value, sname) \ + struct sname *sname##_set_##name##_by_offset(struct sampler_layer *l, uint32_t offset, const struct sname##_key *key, gboolean set_local_value, type value) \ + { \ + void *vl = &l->data; \ + struct sname **list_ptr = vl + offset; \ + struct sname *dstm = sname##_add(list_ptr, key); \ + if (!set_local_value && dstm->value.has_name) \ + return dstm; \ + dstm->value.has_name = set_local_value; \ + dstm->value.name = value; \ + return dstm; \ + } + +#define SAMPLER_COLL_FUNC_SETTERS(sname) \ + SAMPLER_COLL_FIELD_LIST_##sname(SAMPLER_COLL_FIELD_SETTER, sname) + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_SETTERS) + +#define SAMPLER_COLL_FIELD_UNSET(name, has_name, type, init_value, sname) \ + if ((unset_mask & (1 << sname##_value_field_##name)) && d->value.has_name == remove_local) \ + { \ + d->value.name = parent ? parent->value.name : init_value; \ + d->value.has_name = FALSE; \ + } \ + +#define SAMPLER_COLL_FIELD_KEY_ENUM_VALUE(name, has_name, type, init_value, sname) \ + sname##_value_field_##name, + +#define SAMPLER_COLL_FUNC_UNSET(sname) \ + enum { \ + SAMPLER_COLL_FIELD_LIST_##sname(SAMPLER_COLL_FIELD_KEY_ENUM_VALUE, sname) \ + }; \ + static gboolean sname##_unset_by_offset(struct sampler_layer *l, uint32_t offset, const struct sname##_key *key, gboolean remove_local, uint32_t unset_mask) \ + { \ + void *vl = &l->data, *vp = l->parent ? &l->parent->data : NULL; \ + struct sname **list_ptr = vl + offset; \ + struct sname **parent_list_ptr = vp ? vp + offset : NULL; \ + struct sname **link_ptr = NULL; \ + struct sname *d = sname##_find2(list_ptr, key, &link_ptr); \ + if (!d) \ + return FALSE; \ + struct sname *parent = remove_local && *parent_list_ptr != NULL ? sname##_find(*parent_list_ptr, key) : NULL; \ + SAMPLER_COLL_FIELD_LIST_##sname(SAMPLER_COLL_FIELD_UNSET, sname) \ + /* Delete if it's all default values and it's not overriding anything */ \ + if (sname##_is_null(&d->value)) {\ + *link_ptr = d->next; \ + g_free(d); \ + } \ + return TRUE; \ + } + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_UNSET) + +#define SAMPLER_COLL_FIELD_ZEROHASATTR(name, has_name, type, init_value) \ + dstv->value.has_name = FALSE; +#define SAMPLER_COLL_FUNC_CLONE(sname) \ + static struct sname *sname##_clone(struct sname *src, gboolean copy_hasattr) \ + { \ + struct sname *dst = NULL, **last = &dst;\ + for(const struct sname *srcv = src; srcv; srcv = srcv->next) \ + { \ + struct sname *dstv = g_malloc(sizeof(struct sname)); \ + memcpy(dstv, srcv, sizeof(struct sname)); \ + if (!copy_hasattr) \ + { \ + SAMPLER_COLL_FIELD_LIST_##sname(SAMPLER_COLL_FIELD_ZEROHASATTR) \ + } \ + *last = dstv; \ + dstv->next = NULL; \ + last = &dstv->next; \ + } \ + return dst; \ + } + +SAMPLER_COLL_LIST(SAMPLER_COLL_FUNC_CLONE) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +enum sampler_layer_param_type +{ + slpt_invalid, + slpt_alias, + slpt_int, + slpt_uint32_t, + slpt_float, + slpt_dBamp, + slpt_midi_note_t, + slpt_enum, + slpt_string, + slpt_midicurve, + slpt_ccrange, + // modulation matrix + slpt_mod_amount, // src (or CC) * src2 (or CC) -> dest + slpt_mod_curveid, + slpt_mod_smooth, + slpt_mod_step, + slpt_generic_modulation, + // note init functions + slpt_voice_nif, + slpt_prevoice_nif, + slpt_flex_lfo, + slpt_reserved, +}; + +struct sampler_layer_param_entry +{ + const char *name; + size_t offset; + enum sampler_layer_param_type type; + double def_value; + uint64_t extra_int; + void *extra_ptr; + void (*set_has_value)(struct sampler_layer_data *, gboolean); + gboolean (*get_has_value)(struct sampler_layer_data *); +}; + +#define MODSRC_CC 0xFFF +#define smsrc_CC MODSRC_CC + +#define ENCODE_MOD(src, src2, dst) ((((uint32_t)(src) & 0xFFFU) | (((uint32_t)(src2) & 0xFFFU) << 12U) | ((uint32_t)(dst) << 24U))) + +#define PROC_SUBSTRUCT_FIELD_SETHASFUNC(name, index, def_value, parent) \ + static void sampler_layer_data_##parent##_set_has_##name(struct sampler_layer_data *l, gboolean value) { l->has_##parent.name = value; } \ + static gboolean sampler_layer_data_##parent##_get_has_##name(struct sampler_layer_data *l) { return l->has_##parent.name; } + +#define PROC_FIELD_SETHASFUNC(type, name, default_value) \ + static void sampler_layer_data_set_has_##name(struct sampler_layer_data *l, gboolean value) { l->has_##name = value; } \ + static gboolean sampler_layer_data_get_has_##name(struct sampler_layer_data *l) { return l->has_##name; } +#define PROC_FIELD_SETHASFUNC_string(name) \ + static void sampler_layer_data_set_has_##name(struct sampler_layer_data *l, gboolean value) { l->has_##name = value; } \ + static gboolean sampler_layer_data_get_has_##name(struct sampler_layer_data *l) { return l->has_##name; } +#define PROC_FIELD_SETHASFUNC_dBamp(type, name, default_value) \ + PROC_FIELD_SETHASFUNC(type, name, default_value) +#define PROC_FIELD_SETHASFUNC_enum(type, name, default_value) \ + PROC_FIELD_SETHASFUNC(type, name, default_value) +#define PROC_FIELD_SETHASFUNC_dahdsr(field, name, default_value) \ + DAHDSR_FIELDS(PROC_SUBSTRUCT_FIELD_SETHASFUNC, field) +#define PROC_FIELD_SETHASFUNC_lfo(field, name, default_value) \ + LFO_FIELDS(PROC_SUBSTRUCT_FIELD_SETHASFUNC, field) +#define PROC_FIELD_SETHASFUNC_eq(field, name, default_value) \ + EQ_FIELDS(PROC_SUBSTRUCT_FIELD_SETHASFUNC, field) +#define PROC_FIELD_SETHASFUNC_ccrange(name, parname) +#define PROC_FIELD_SETHASFUNC_midicurve(name) \ + static gboolean sampler_layer_data_set_get_has_##name(struct sampler_layer_data *l, uint32_t index, int value) \ + { \ + if (value != -1) \ + l->name.has_values[index] = value; \ + return l->name.has_values[index]; \ + } + +SAMPLER_FIXED_FIELDS(PROC_FIELD_SETHASFUNC) + +#define LOFS(field) offsetof(struct sampler_layer_data, field) + +#define FIELD_MOD(name, param, src, src2, dest) \ + { name, LOFS(modulations), slpt_mod_##param, 0, ENCODE_MOD(smsrc_##src, smsrc_##src2, smdest_##dest), NULL, NULL, NULL }, +#define FIELD_AMOUNT(name, src, dest) \ + FIELD_MOD(name, amount, src, none, dest) +#define FIELD_AMOUNT_CC(name, dest) \ + FIELD_ALIAS(name "cc#", name "_oncc#") \ + FIELD_MOD(name "_oncc#", amount, CC, none, dest) \ + FIELD_MOD(name "_curvecc#", curveid, CC, none, dest) \ + FIELD_MOD(name "_smoothcc#", smooth, CC, none, dest) \ + FIELD_MOD(name "_stepcc#", step, CC, none, dest) +#define FIELD_AMOUNT_CC_(name, dest) \ + FIELD_ALIAS(name "_cc#", name "_oncc#") \ + FIELD_MOD(name "_oncc#", amount, CC, none, dest) \ + FIELD_MOD(name "_curvecc#", curveid, CC, none, dest) \ + FIELD_MOD(name "_smoothcc#", smooth, CC, none, dest) \ + FIELD_MOD(name "_stepcc#", step, CC, none, dest) +#define FIELD_VOICE_NIF(name, nif, variant) \ + { name, LOFS(voice_nifs), slpt_voice_nif, 0, variant, nif, NULL, NULL }, +#define FIELD_PREVOICE_NIF(name, nif, variant) \ + { name, LOFS(prevoice_nifs), slpt_prevoice_nif, 0, variant, nif, NULL, NULL }, +#define FIELD_ALIAS(alias, name) \ + { alias, -1, slpt_alias, 0, 0, name, NULL, NULL }, + +#define PROC_SUBSTRUCT_FIELD_DESCRIPTOR(name, index, def_value, parent, parent_name, parent_index, parent_struct) \ + { #parent_name "_" #name, offsetof(struct sampler_layer_data, parent) + offsetof(struct parent_struct, name), slpt_float, def_value, parent_index * 100 + index, NULL, sampler_layer_data_##parent##_set_has_##name, sampler_layer_data_##parent##_get_has_##name }, \ + +#define PROC_SUBSTRUCT_FIELD_DESCRIPTOR_DAHDSR(name, index, def_value, parent, parent_name, parent_index, parent_struct) \ + { #parent_name "_" #name, offsetof(struct sampler_layer_data, parent) + offsetof(struct parent_struct, name), slpt_float, def_value, parent_index * 100 + index, NULL, sampler_layer_data_##parent##_set_has_##name, sampler_layer_data_##parent##_get_has_##name }, \ + FIELD_VOICE_NIF(#parent_name "_vel2" #name, sampler_nif_vel2env, (parent_index << 4) + snif_env_##name) \ + FIELD_AMOUNT_CC(#parent_name "_" #name, ampeg_stage + (parent_index << 4) + snif_env_##name) \ + +#define PROC_FIELD_DESCRIPTOR(type, name, default_value) \ + { #name, LOFS(name), slpt_##type, default_value, 0, NULL, sampler_layer_data_set_has_##name, sampler_layer_data_get_has_##name }, +#define PROC_FIELD_DESCRIPTOR_dBamp(type, name, default_value) \ + { #name, LOFS(name), slpt_##type, default_value, 0, NULL, sampler_layer_data_set_has_##name, sampler_layer_data_get_has_##name }, +#define PROC_FIELD_DESCRIPTOR_string(name) \ + { #name, LOFS(name), slpt_string, 0, LOFS(name##_changed), NULL, sampler_layer_data_set_has_##name, sampler_layer_data_get_has_##name }, +#define PROC_FIELD_DESCRIPTOR_enum(enumtype, name, default_value) \ + { #name, LOFS(name), slpt_enum, (double)(enum enumtype)default_value, 0, enumtype##_from_string, sampler_layer_data_set_has_##name, sampler_layer_data_get_has_##name }, +#define PROC_FIELD_DESCRIPTOR_midicurve(name) \ + { #name "_#", LOFS(name), slpt_midicurve, 0, 0, (void *)sampler_layer_data_set_get_has_##name, NULL, NULL }, + +#define FIELD_DEPTH_SET(name, dest, attrib) \ + FIELD_ALIAS(#name attrib "cc#", #name attrib "_oncc#") \ + FIELD_MOD(#name attrib "_oncc#", amount, name, CC, dest) \ + FIELD_MOD(#name attrib "_curvecc#", curveid, name, CC, dest) \ + FIELD_MOD(#name attrib "_smoothcc#", smooth, name, CC, dest) \ + FIELD_MOD(#name attrib "_stepcc#", step, name, CC, dest) \ + FIELD_MOD(#name attrib "polyaft", amount, name, polyaft, dest) \ + FIELD_MOD(#name attrib "chanaft", amount, name, chanaft, dest) \ + FIELD_MOD(#name attrib, amount, name, none, dest) \ + +#define PROC_FIELD_DESCRIPTOR_dahdsr(field, name, index) \ + DAHDSR_FIELDS(PROC_SUBSTRUCT_FIELD_DESCRIPTOR_DAHDSR, field, name, index, cbox_dahdsr) \ + FIELD_DEPTH_SET(name, from_##name, "_depth") \ + FIELD_MOD(#name "_vel2depth", amount, name, vel, from_##name) + +#define PROC_FIELD_DESCRIPTOR_lfo(field, name, index) \ + LFO_FIELDS(PROC_SUBSTRUCT_FIELD_DESCRIPTOR, field, name, index, sampler_lfo_params) \ + FIELD_AMOUNT(#name "_freqpolyaft", polyaft, name##_freq) \ + FIELD_AMOUNT(#name "_freqchanaft", chanaft, name##_freq) \ + FIELD_AMOUNT_CC(#name "_freq", name##_freq) \ + FIELD_DEPTH_SET(name, from_##name, "_depth") + +#define PROC_FIELD_DESCRIPTOR_eq(field, name, index) \ + EQ_FIELDS(PROC_SUBSTRUCT_FIELD_DESCRIPTOR, field, name, index, sampler_eq_params) \ + FIELD_AMOUNT_CC(#name "_freq", name##_freq) \ + FIELD_AMOUNT_CC(#name "_bw", name##_bw) \ + FIELD_AMOUNT_CC(#name "_gain", name##_gain) + +#define PROC_FIELD_DESCRIPTOR_ccrange(field, parname) \ + { #parname "locc#", LOFS(field), slpt_ccrange, 0, 0, NULL, NULL, NULL }, \ + { #parname "hicc#", LOFS(field), slpt_ccrange, 127, 1, NULL, NULL, NULL }, + +#define FIELD_FLEX_LFO(name, field) \ + { name, LOFS(flex_lfos), slpt_flex_lfo, 0, sampler_flex_lfo_value_field_##field, NULL, NULL, NULL }, + +#define NIF_VARIANT_CC 0x01000000 +#define NIF_VARIANT_CURVECC 0x02000000 +#define NIF_VARIANT_STEPCC 0x03000000 +#define NIF_VARIANT_MASK 0xFF000000 + +struct sampler_layer_param_entry sampler_layer_params[] = { + SAMPLER_FIXED_FIELDS(PROC_FIELD_DESCRIPTOR) + + FIELD_AMOUNT("cutoff_chanaft", chanaft, cutoff) + FIELD_AMOUNT("resonance_chanaft", chanaft, resonance) + FIELD_AMOUNT("cutoff_polyaft", polyaft, cutoff) + FIELD_AMOUNT("resonance_polyaft", polyaft, resonance) + + FIELD_DEPTH_SET(fileg, cutoff2, "_depth2") + FIELD_MOD("fileg_vel2depth2", amount, fileg, vel, cutoff2) + + FIELD_DEPTH_SET(fillfo, cutoff2, "_depth2") + + FIELD_AMOUNT("cutoff2_chanaft", chanaft, cutoff2) + FIELD_AMOUNT("resonance2_chanaft", chanaft, resonance2) + FIELD_AMOUNT("cutoff2_polyaft", polyaft, cutoff2) + FIELD_AMOUNT("resonance2_polyaft", polyaft, resonance2) + + FIELD_AMOUNT_CC_("gain", gain) + FIELD_AMOUNT_CC_("cutoff", cutoff) + FIELD_AMOUNT_CC_("resonance", resonance) + FIELD_AMOUNT_CC_("cutoff2", cutoff2) + FIELD_AMOUNT_CC_("resonance2", resonance2) + FIELD_AMOUNT_CC_("pitch", pitch) + FIELD_AMOUNT_CC_("tune", pitch) + FIELD_AMOUNT_CC_("tonectl", tonectl) + FIELD_AMOUNT_CC_("pan", pan) + FIELD_AMOUNT_CC_("amplitude", amplitude) + + FIELD_VOICE_NIF("amp_random", sampler_nif_addrandom, 0) + FIELD_VOICE_NIF("fil_random", sampler_nif_addrandom, 1) + FIELD_VOICE_NIF("pitch_random", sampler_nif_addrandom, 2) + FIELD_VOICE_NIF("pitch_veltrack", sampler_nif_vel2pitch, 0) + FIELD_VOICE_NIF("offset_veltrack", sampler_nif_vel2offset, 0) + FIELD_VOICE_NIF("reloffset_veltrack", sampler_nif_vel2reloffset, 0) + FIELD_PREVOICE_NIF("delay_random", sampler_nif_addrandomdelay, 0) + FIELD_PREVOICE_NIF("sync_beats", sampler_nif_syncbeats, 0) + FIELD_PREVOICE_NIF("delay_cc#", sampler_nif_cc2delay, NIF_VARIANT_CC) + FIELD_PREVOICE_NIF("delay_curvecc#", sampler_nif_cc2delay, NIF_VARIANT_CURVECC) + FIELD_PREVOICE_NIF("delay_stepcc#", sampler_nif_cc2delay, NIF_VARIANT_STEPCC) + FIELD_VOICE_NIF("reloffset_cc#", sampler_nif_cc2reloffset, NIF_VARIANT_CC) + FIELD_VOICE_NIF("reloffset_curvecc#", sampler_nif_cc2reloffset, NIF_VARIANT_CURVECC) + FIELD_VOICE_NIF("reloffset_stepcc#", sampler_nif_cc2reloffset, NIF_VARIANT_STEPCC) + FIELD_VOICE_NIF("offset_cc#", sampler_nif_cc2offset, NIF_VARIANT_CC) + FIELD_VOICE_NIF("offset_curvecc#", sampler_nif_cc2offset, NIF_VARIANT_CURVECC) + FIELD_VOICE_NIF("offset_stepcc#", sampler_nif_cc2offset, NIF_VARIANT_STEPCC) + + FIELD_FLEX_LFO("lfo#_freq", freq) + FIELD_FLEX_LFO("lfo#_delay", delay) + FIELD_FLEX_LFO("lfo#_fade", fade) + FIELD_FLEX_LFO("lfo#_wave", wave) + + FIELD_ALIAS("hilev", "hivel") + FIELD_ALIAS("lolev", "lovel") + FIELD_ALIAS("loopstart", "loop_start") + FIELD_ALIAS("loopend", "loop_end") + FIELD_ALIAS("loopmode", "loop_mode") + FIELD_ALIAS("bendup", "bend_up") + FIELD_ALIAS("benddown", "bend_down") + FIELD_ALIAS("offby", "off_by") + FIELD_ALIAS("offset_oncc#", "offset_cc#") + FIELD_ALIAS("reloffset_oncc#", "reloffset_cc#") + FIELD_ALIAS("delay_oncc#", "delay_cc#") + + { "genericmod_#_#_#_#", -1, slpt_generic_modulation, 0, 0, NULL, NULL, NULL }, +}; +#define NPARAMS (sizeof(sampler_layer_params) / sizeof(sampler_layer_params[0])) + +static int compare_entries(const void *p1, const void *p2) +{ + const struct sampler_layer_param_entry *e1 = p1, *e2 = p2; + return strcmp(e1->name, e2->name); +} + +void sampler_layer_prepare_params(void) +{ + qsort(sampler_layer_params, NPARAMS, sizeof(struct sampler_layer_param_entry), compare_entries); + for (size_t i = 0; i < NPARAMS; ++i) + { + struct sampler_layer_param_entry *e = &sampler_layer_params[i]; + if (e->type == slpt_alias) + { + struct sampler_layer_param_entry prototype; + prototype.name = e->extra_ptr; + void *found = bsearch(&prototype, sampler_layer_params, NPARAMS, sizeof(sampler_layer_params[0]), compare_entries); + if (!found) + printf("Alias %s redirects to non-existent name (%s)\n", e->name, prototype.name); + assert(found); + e->extra_ptr = found; + } + if (i) + { + struct sampler_layer_param_entry *prev_e = &sampler_layer_params[i - 1]; + if (!strcmp(e->name, prev_e->name)) + { + printf("Duplicate parameter %s\n", e->name); + assert(FALSE); + } + } + } +} + +// This only works for setting. Unsetting is slightly different. +static gboolean override_logic(gboolean is_equal, gboolean has_value, gboolean set_local_value) +{ + if (!set_local_value && has_value) // Do not override locally set values + return FALSE; + // Override if a value or a inherited value is replaced with a local setting. + return (!is_equal) || (set_local_value != has_value); +} + +static inline void mod_key_decode(uint64_t extra_int, const uint32_t *args, struct sampler_modulation_key *mod_key) +{ + uint32_t modsrc = (extra_int & 0xFFF); + uint32_t modsrc2 = ((extra_int >> 12) & 0xFFF); + if (modsrc == MODSRC_CC) + modsrc = args[0]; + if (modsrc2 == MODSRC_CC) + modsrc2 = args[0]; + mod_key->src = modsrc; + mod_key->src2 = modsrc2; + mod_key->dest = ((extra_int >> 24) & 0xFF); +} + +static inline void nif_key_decode(uint64_t extra_int, void *extra_ptr, const uint32_t *args, struct sampler_noteinitfunc_key *nif_key) +{ + uint32_t variant = extra_int &~ NIF_VARIANT_MASK; + nif_key->notefunc_voice = extra_ptr; + if (extra_int & NIF_VARIANT_MASK) + { + int cc = args[0] & 255; + variant = cc + (variant << 8); + } + nif_key->variant = variant; +} + +static inline void flex_lfo_key_decode(const uint32_t *args, struct sampler_flex_lfo_key *flex_lfo_key) +{ + flex_lfo_key->id = args[0]; +} + +#define OVERRIDE_LOGIC(type) override_logic(!memcmp(p, data_ptr, sizeof(type)), e->get_has_value(&l->data), set_local_value) + +#define CAST_FLOAT_VALUE fvalue = *(double *)data_ptr + +gboolean sampler_layer_param_entry_set_from_ptr(const struct sampler_layer_param_entry *e, struct sampler_layer *l, gboolean set_local_value, const void *data_ptr, const uint32_t *args, GError **error) +{ + void *p = ((uint8_t *)&l->data) + e->offset; + uint32_t cc = 0; + double fvalue = 0; + struct sampler_modulation_key mod_key = {0, 0, 0}; + struct sampler_noteinitfunc_key nif_key = {{NULL}, 0}; + struct sampler_flex_lfo_key flex_lfo_key = {0}; + + switch(e->type) + { + case slpt_midi_note_t: + if (!OVERRIDE_LOGIC(midi_note_t)) + return TRUE; + memcpy(p, data_ptr, sizeof(midi_note_t)); + break; + case slpt_int: + if (!OVERRIDE_LOGIC(int)) + return TRUE; + memcpy(p, data_ptr, sizeof(int)); + break; + case slpt_enum: + case slpt_uint32_t: + if (!OVERRIDE_LOGIC(uint32_t)) + return TRUE; + memcpy(p, data_ptr, sizeof(uint32_t)); + break; + case slpt_string: + { + char **pc = p; + gboolean str_differs = (!*pc != !data_ptr) || strcmp(*pc, data_ptr); + if (!override_logic(!str_differs, e->get_has_value(&l->data), set_local_value)) + return TRUE; + if (str_differs) + { + free(*pc); + *pc = g_strdup(data_ptr); + gboolean *changed_ptr = (gboolean *)(((uint8_t *)&l->data) + e->extra_int); + *changed_ptr = 1; + } + } + break; + case slpt_float: + case slpt_dBamp: + fvalue = *(double *)data_ptr; + if (!override_logic((float)fvalue == *(float *)p, e->get_has_value(&l->data), set_local_value)) + return TRUE; + *(float *)p = fvalue; + break; + case slpt_midicurve: + CAST_FLOAT_VALUE; + if (args[0] >= 0 && args[0] <= 127) + { + gboolean (*setgethasfunc)(struct sampler_layer_data *, uint32_t, int) = e->extra_ptr; + float *dst = &((struct sampler_midi_curve *)p)->values[args[0]]; + if (!override_logic(*dst == fvalue, setgethasfunc(&l->data, args[0], -1), set_local_value)) + return TRUE; + *dst = fvalue; + setgethasfunc(&l->data, args[0], set_local_value); + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Curve entry index (%u) is out of range for %s", (unsigned)args[0], e->name); + return FALSE; + } + break; + case slpt_ccrange: + { + int number = *(int *)data_ptr; + cc = args[0]; + switch(e->extra_int) { + case 0: + sampler_cc_range_set_locc_by_offset(l, e->offset, &(struct sampler_cc_range_key){cc}, set_local_value, number); + break; + case 1: + sampler_cc_range_set_hicc_by_offset(l, e->offset, &(struct sampler_cc_range_key){cc}, set_local_value, number); + break; + default: assert(0); + } + break; + } + case slpt_mod_amount: + CAST_FLOAT_VALUE; + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_set_amount_by_offset(l, e->offset, &mod_key, set_local_value, fvalue); + break; + case slpt_mod_curveid: + CAST_FLOAT_VALUE; + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_set_curve_id_by_offset(l, e->offset, &mod_key, set_local_value, (int)fvalue); + break; + case slpt_mod_smooth: + CAST_FLOAT_VALUE; + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_set_smooth_by_offset(l, e->offset, &mod_key, set_local_value, fvalue); + break; + case slpt_mod_step: + CAST_FLOAT_VALUE; + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_set_step_by_offset(l, e->offset, &mod_key, set_local_value, fvalue); + break; + case slpt_generic_modulation: + CAST_FLOAT_VALUE; + sampler_modulation_set_amount_by_offset(l, e->offset, &(struct sampler_modulation_key){args[0], args[1], args[2]}, set_local_value, fvalue); + sampler_modulation_set_curve_id_by_offset(l, e->offset, &(struct sampler_modulation_key){args[0], args[1], args[2]}, set_local_value, (int)args[3]); + break; + case slpt_voice_nif: + case slpt_prevoice_nif: + CAST_FLOAT_VALUE; + nif_key_decode(e->extra_int, e->extra_ptr, args, &nif_key); + switch(e->extra_int & NIF_VARIANT_MASK) + { + case 0: + case NIF_VARIANT_CC: + sampler_noteinitfunc_set_value_by_offset(l, e->offset, &nif_key, set_local_value, fvalue); + break; + case NIF_VARIANT_CURVECC: + sampler_noteinitfunc_set_curve_id_by_offset(l, e->offset, &nif_key, set_local_value, (int)fvalue); + break; + case NIF_VARIANT_STEPCC: + sampler_noteinitfunc_set_step_by_offset(l, e->offset, &nif_key, set_local_value, fvalue); + break; + } + break; + case slpt_flex_lfo: + CAST_FLOAT_VALUE; + flex_lfo_key_decode(args, &flex_lfo_key); + switch(e->extra_int) + { + case sampler_flex_lfo_value_field_freq: + sampler_flex_lfo_set_freq_by_offset(l, e->offset, &flex_lfo_key, set_local_value, fvalue); + break; + case sampler_flex_lfo_value_field_delay: + sampler_flex_lfo_set_delay_by_offset(l, e->offset, &flex_lfo_key, set_local_value, fvalue); + break; + case sampler_flex_lfo_value_field_fade: + sampler_flex_lfo_set_fade_by_offset(l, e->offset, &flex_lfo_key, set_local_value, fvalue); + break; + case sampler_flex_lfo_value_field_wave: + sampler_flex_lfo_set_wave_by_offset(l, e->offset, &flex_lfo_key, set_local_value, (int)fvalue); + break; + } + break; + case slpt_reserved: + case slpt_invalid: + case slpt_alias: + printf("Unhandled parameter type of parameter %s\n", e->name); + assert(0); + return FALSE; + } + if (e->set_has_value) + e->set_has_value(&l->data, set_local_value); + if (l->child_layers) { + /* Propagate to children */ + GHashTableIter iter; + g_hash_table_iter_init(&iter, l->child_layers); + gpointer key, value; + while(g_hash_table_iter_next(&iter, &key, &value)) + { + struct sampler_layer *child = value; + if (!sampler_layer_param_entry_set_from_ptr(e, child, FALSE, data_ptr, args, error)) + return FALSE; + } + } + return TRUE; +} + +#define VERIFY_FLOAT_VALUE do { if (!atof_C_verify(e->name, value, &fvalue, error)) return FALSE; } while(0) + +gboolean sampler_layer_param_entry_set_from_string(const struct sampler_layer_param_entry *e, struct sampler_layer *l, gboolean set_local_value, const char *value, const uint32_t *args, GError **error) +{ + double fvalue; + switch(e->type) + { + case slpt_midi_note_t: + { + midi_note_t note = sfz_note_from_string(value); + if (note < -1) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "'%s' is not a valid note name for %s", value, e->name); + return FALSE; + } + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, ¬e, args, error); + } + case slpt_int: + { + char *endptr; + errno = 0; + int number = strtol(value, &endptr, 10); + if (errno || *endptr || endptr == value) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "'%s' is not a correct integer value for %s", value, e->name); + return FALSE; + } + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, &number, args, error); + } + case slpt_enum: + { + gboolean (*func)(const char *, uint32_t *value); + func = e->extra_ptr; + uint32_t data = 0; + if (!func(value, &data)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "'%s' is not a correct value for %s", value, e->name); + return FALSE; + } + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, &data, args, error); + } + case slpt_uint32_t: + { + char *endptr; + errno = 0; + uint32_t number = (uint32_t)strtoul(value, &endptr, 10); + if (errno || *endptr || endptr == value || value[0] == '-') + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "'%s' is not a correct unsigned integer value for %s", value, e->name); + return FALSE; + } + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, &number, args, error); + } + case slpt_string: + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, value, args, error); + case slpt_ccrange: + { + char *endptr; + errno = 0; + int number = strtol(value, &endptr, 10); + if (errno || *endptr || endptr == value || number < 0 || number > 127) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "'%s' is not a correct control change value for %s", value, e->name); + return FALSE; + } + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, &number, args, error); + } + case slpt_float: + case slpt_dBamp: + default: + VERIFY_FLOAT_VALUE; + return sampler_layer_param_entry_set_from_ptr(e, l, set_local_value, &fvalue, args, error); + } +} + +#define COPY_NUM_FROM_PARENT(case_value, type) \ + case case_value: \ + if (!unset_local_value && e->get_has_value(&l->data)) \ + return TRUE; \ + *(type *)p = pp ? *(type *)pp : (type)e->def_value; \ + e->set_has_value(&l->data, 0); \ + break; +gboolean sampler_layer_param_entry_unset(const struct sampler_layer_param_entry *e, struct sampler_layer *l, gboolean unset_local_value, const uint32_t *args, GError **error) +{ + void *p = ((uint8_t *)&l->data) + e->offset; + void *pp = l->parent ? ((uint8_t *)&l->parent->data) + e->offset : NULL; + uint32_t cc; + struct sampler_modulation_key mod_key = {0, 0, 0}; + struct sampler_noteinitfunc_key nif_key = {{NULL}, 0}; + struct sampler_flex_lfo_key flex_lfo_key = {0}; + + switch(e->type) + { + COPY_NUM_FROM_PARENT(slpt_midi_note_t, midi_note_t) + COPY_NUM_FROM_PARENT(slpt_int, int) + COPY_NUM_FROM_PARENT(slpt_enum, uint32_t) // XXXKF that's a kludge, enums are not guaranteed to be uint32_t (but they should be on common platforms) + COPY_NUM_FROM_PARENT(slpt_uint32_t, uint32_t) + COPY_NUM_FROM_PARENT(slpt_float, float) + COPY_NUM_FROM_PARENT(slpt_dBamp, float) + case slpt_string: + { + if (!unset_local_value && e->get_has_value(&l->data)) + return TRUE; + char **pc = p; + free(*pc); + *pc = pp ? g_strdup(*(const char **)pp) : NULL; + e->set_has_value(&l->data, 0); + gboolean *changed_ptr = (gboolean *)(((uint8_t *)&l->data) + e->extra_int); + *changed_ptr = 1; + } + return TRUE; + case slpt_midicurve: + if (args[0] >= 0 && args[0] <= 127) + { + struct sampler_midi_curve *curve = p, *parent_curve = pp; + gboolean (*setgethasfunc)(struct sampler_layer_data *, uint32_t, gboolean value) = e->extra_ptr; + if (setgethasfunc(&l->data, args[0], -1) && !unset_local_value) + return TRUE; + curve->values[args[0]] = parent_curve ? parent_curve->values[args[0]] : SAMPLER_CURVE_GAP; + setgethasfunc(&l->data, args[0], 0); + break; + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Curve entry index (%u) is out of range for %s", (unsigned)args[0], e->name); + return FALSE; + } + case slpt_ccrange: + cc = args[0]; + if (!sampler_cc_range_unset_by_offset(l, e->offset, &(struct sampler_cc_range_key){cc}, unset_local_value, 1 << e->extra_int)) + { + if (!unset_local_value) + return TRUE; + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Controller number %d not used for %s", cc, e->name); + return FALSE; + } + break; + case slpt_mod_amount: + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_unset_by_offset(l, e->offset, &mod_key, unset_local_value, 1 << sampler_modulation_value_field_amount); + break; + case slpt_mod_curveid: + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_unset_by_offset(l, e->offset, &mod_key, unset_local_value, 1 << sampler_modulation_value_field_curve_id); + break; + case slpt_mod_smooth: + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_unset_by_offset(l, e->offset, &mod_key, unset_local_value, 1 << sampler_modulation_value_field_smooth); + break; + case slpt_mod_step: + mod_key_decode(e->extra_int, args, &mod_key); + sampler_modulation_unset_by_offset(l, e->offset, &mod_key, unset_local_value, 1 << sampler_modulation_value_field_step); + break; + case slpt_generic_modulation: + mod_key = (struct sampler_modulation_key){args[0], args[1], args[2]}; + sampler_modulation_unset_by_offset(l, e->offset, &mod_key, unset_local_value, (1 << sampler_modulation_value_field_amount) | (1 << sampler_modulation_value_field_curve_id)); + break; + case slpt_voice_nif: + case slpt_prevoice_nif: + { + nif_key_decode(e->extra_int, e->extra_ptr, args, &nif_key); + static const uint32_t value_fields[] = { + sampler_noteinitfunc_value_field_value, sampler_noteinitfunc_value_field_value, + sampler_noteinitfunc_value_field_curve_id, sampler_noteinitfunc_value_field_step, + }; + if (!sampler_noteinitfunc_unset_by_offset(l, e->offset, &nif_key, unset_local_value, 1 << value_fields[e->extra_int >> 24])) + { + if (!unset_local_value) + return TRUE; + if (e->extra_int & NIF_VARIANT_MASK) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Controller number %d not used for %s", args[0], e->name); + else + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "%s not set", e->name); + return FALSE; + } + break; + } + case slpt_flex_lfo: + flex_lfo_key_decode(args, &flex_lfo_key); + switch(e->extra_int) + { + case sampler_flex_lfo_value_field_freq: + sampler_flex_lfo_unset_by_offset(l, e->offset, &flex_lfo_key, unset_local_value, 1 << sampler_flex_lfo_value_field_freq); + break; + case sampler_flex_lfo_value_field_delay: + sampler_flex_lfo_unset_by_offset(l, e->offset, &flex_lfo_key, unset_local_value, 1 << sampler_flex_lfo_value_field_delay); + break; + case sampler_flex_lfo_value_field_fade: + sampler_flex_lfo_unset_by_offset(l, e->offset, &flex_lfo_key, unset_local_value, 1 << sampler_flex_lfo_value_field_fade); + break; + case sampler_flex_lfo_value_field_wave: + sampler_flex_lfo_unset_by_offset(l, e->offset, &flex_lfo_key, unset_local_value, 1 << sampler_flex_lfo_value_field_wave); + break; + } + break; + case slpt_invalid: + case slpt_reserved: + case slpt_alias: + default: + printf("Unhandled parameter type of parameter %s\n", e->name); + assert(0); + return FALSE; + } + if (l->child_layers) { + /* Propagate to children */ + GHashTableIter iter; + g_hash_table_iter_init(&iter, l->child_layers); + gpointer key, value; + while(g_hash_table_iter_next(&iter, &key, &value)) + { + struct sampler_layer *child = value; + if (!sampler_layer_param_entry_unset(e, child, FALSE, args, error)) + return FALSE; + } + } + return TRUE; +} +#undef COPY_NUM_FROM_PARENT + +// Compare against a template that uses # to represent a number, extract +// any such numbers. +static int templcmp(const char *key, const char *templ, uint32_t *numbers) +{ + while(*key && *templ) + { + if (*templ == '#') + { + if (isdigit(*key)) { + uint32_t num = 0; + do { + num = num * 10 + (unsigned char)(*key - '0'); + key++; + } while (isdigit(*key)); + *numbers++ = num; + templ++; + continue; + } + } + else if (*key == *templ) + { + templ++, key++; + continue; + } + if (*key < *templ) + return -1; + else + return +1; + } + if (*key) + return +1; + if (*templ) + return -1; + return 0; +} + +const struct sampler_layer_param_entry *sampler_layer_param_find(const char *key, uint32_t *args) +{ + static int prepared = 0; + if (!prepared) + { + sampler_layer_prepare_params(); + prepared = 1; + } + int niter = 0; + uint32_t lo = 0, hi = NPARAMS; + while(lo < hi) { + ++niter; + uint32_t mid = (lo + hi) >> 1; + const struct sampler_layer_param_entry *e = &sampler_layer_params[mid]; + int res = templcmp(key, e->name, args); + if (res == 0) + { + // printf("%s found in %d iterations\n", key, niter); + if (e->type == slpt_alias) + return (const struct sampler_layer_param_entry *)e->extra_ptr; + return e; + } + if (res < 0) + hi = mid; + else + lo = mid + 1; + } + return NULL; +} + +int sampler_layer_apply_fixed_param(struct sampler_layer *l, const char *key, const char *value, GError **error) +{ + uint32_t args[10]; + const struct sampler_layer_param_entry *e = sampler_layer_param_find(key, args); + if (e) + return sampler_layer_param_entry_set_from_string(e, l, TRUE, value, args, error); + else + return -1; +} + +int sampler_layer_unapply_fixed_param(struct sampler_layer *l, const char *key, GError **error) +{ + uint32_t args[10]; + const struct sampler_layer_param_entry *e = sampler_layer_param_find(key, args); + if (e) + return sampler_layer_param_entry_unset(e, l, TRUE, args, error); + else + return -1; +} + +static gboolean sampler_layer_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct sampler_layer *layer = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (!((!layer->parent_program || cbox_execute_on(fb, NULL, "/parent_program", "o", error, layer->parent_program)) && + (!layer->parent || cbox_execute_on(fb, NULL, "/parent", "o", error, layer->parent)) && + CBOX_OBJECT_DEFAULT_STATUS(layer, fb, error))) + return FALSE; + return TRUE; + } + if ((!strcmp(cmd->command, "/as_string") || !strcmp(cmd->command, "/as_string_full")) && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + gchar *res = sampler_layer_to_string(layer, !strcmp(cmd->command, "/as_string_full")); + gboolean result = cbox_execute_on(fb, NULL, "/value", "s", error, res[0] == ' ' ? res + 1 : res); + g_free(res); + return result; + } + if (!strcmp(cmd->command, "/set_param") && !strcmp(cmd->arg_types, "ss")) + { + const char *key = CBOX_ARG_S(cmd, 0); + const char *value = CBOX_ARG_S(cmd, 1); + if (sampler_layer_apply_param(layer, key, value, error)) + { + sampler_layer_update(layer); + sampler_program_update_layers(layer->parent_program); + return TRUE; + } + return FALSE; + } + if (!strcmp(cmd->command, "/unset_param") && !strcmp(cmd->arg_types, "s")) + { + const char *key = CBOX_ARG_S(cmd, 0); + if (sampler_layer_unapply_param(layer, key, error)) + { + sampler_layer_update(layer); + sampler_program_update_layers(layer->parent_program); + return TRUE; + } + return FALSE; + } + if (!strcmp(cmd->command, "/new_child") && !strcmp(cmd->arg_types, "")) + { + // XXXKF needs a string argument perhaps + if (layer->parent && layer->parent->parent && layer->parent->parent->parent) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot create a region within a region"); + return FALSE; + } + struct sampler_layer *l = sampler_layer_new(layer->module, layer->parent_program, layer); + sampler_layer_data_finalize(&l->data, l->parent ? &l->parent->data : NULL, layer->parent_program); + sampler_layer_reset_switches(l, l->module); + sampler_layer_update(l); + + if (l->parent && l->parent->parent && l->parent->parent->parent) + { + sampler_program_add_layer(layer->parent_program, l); + sampler_program_update_layers(layer->parent_program); + } + + return cbox_execute_on(fb, NULL, "/uuid", "o", error, l); + } + if (!strcmp(cmd->command, "/get_children") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + GHashTableIter iter; + g_hash_table_iter_init(&iter, layer->child_layers); + gpointer key, value; + while(g_hash_table_iter_next(&iter, &key, &value)) + { + if (!cbox_execute_on(fb, NULL, "/child", "o", error, key)) + return FALSE; + } + return TRUE; + } + // otherwise, treat just like an command on normal (non-aux) output + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + + +#define PROC_FIELDS_INITIALISER(type, name, def_value) \ + ld->name = def_value; \ + ld->has_##name = 0; +#define PROC_FIELDS_INITIALISER_string(name) \ + ld->name = NULL; \ + ld->name##_changed = FALSE; \ + ld->has_##name = 0; +#define PROC_FIELDS_INITIALISER_midicurve(name) \ + sampler_midi_curve_init(&ld->name); +#define PROC_FIELDS_INITIALISER_enum(type, name, def_value) \ + PROC_FIELDS_INITIALISER(type, name, def_value) +#define PROC_FIELDS_INITIALISER_dBamp(type, name, def_value) \ + ld->name = def_value; \ + ld->name##_linearized = -1; \ + ld->has_##name = 0; +#define PROC_FIELDS_INITIALISER_dahdsr(name, parname, index) \ + DAHDSR_FIELDS(PROC_SUBSTRUCT_RESET_FIELD, name, ld); \ + DAHDSR_FIELDS(PROC_SUBSTRUCT_RESET_HAS_FIELD, name, ld) +#define PROC_FIELDS_INITIALISER_lfo(name, parname, index) \ + LFO_FIELDS(PROC_SUBSTRUCT_RESET_FIELD, name, ld); \ + LFO_FIELDS(PROC_SUBSTRUCT_RESET_HAS_FIELD, name, ld) +#define PROC_FIELDS_INITIALISER_eq(name, parname, index) \ + EQ_FIELDS(PROC_SUBSTRUCT_RESET_FIELD, name, ld); \ + EQ_FIELDS(PROC_SUBSTRUCT_RESET_HAS_FIELD, name, ld) +#define PROC_FIELDS_INITIALISER_ccrange(name, parname) \ + ld->name = NULL; + +CBOX_CLASS_DEFINITION_ROOT(sampler_layer) + +struct sampler_layer *sampler_layer_new(struct sampler_module *m, struct sampler_program *parent_program, struct sampler_layer *parent) +{ + struct sampler_layer *l = calloc(1, sizeof(struct sampler_layer)); + struct cbox_document *doc = CBOX_GET_DOCUMENT(parent_program); + memset(l, 0, sizeof(struct sampler_layer)); + CBOX_OBJECT_HEADER_INIT(l, sampler_layer, doc); + cbox_command_target_init(&l->cmd_target, sampler_layer_process_cmd, l); + + l->module = m; + l->child_layers = g_hash_table_new(NULL, NULL); + if (parent) + { + sampler_layer_data_clone(&l->data, &parent->data, FALSE); + l->parent_program = parent_program; + l->parent = parent; + g_hash_table_replace(parent->child_layers, l, l); + l->runtime = NULL; + CBOX_OBJECT_REGISTER(l); + return l; + } + l->parent_program = parent_program; + + struct sampler_layer_data *ld = &l->data; + SAMPLER_FIXED_FIELDS(PROC_FIELDS_INITIALISER) + + ld->computed.eff_waveform = NULL; + ld->computed.eff_freq = 44100; + ld->modulations = NULL; + ld->voice_nifs = NULL; + ld->prevoice_nifs = NULL; + + ld->computed.eff_use_keyswitch = 0; + if (!parent) + { + // Systemwide default instead? + uint32_t mod_offset = LOFS(modulations); + sampler_modulation_set_amount_by_offset(l, mod_offset, &(struct sampler_modulation_key){74, smsrc_none, smdest_cutoff}, TRUE, 9600); + sampler_modulation_set_curve_id_by_offset(l, mod_offset, &(struct sampler_modulation_key){74, smsrc_none, smdest_cutoff}, TRUE, 1); + sampler_modulation_set_amount_by_offset(l, mod_offset, &(struct sampler_modulation_key){71, smsrc_none, smdest_resonance}, TRUE, 12); + sampler_modulation_set_curve_id_by_offset(l, mod_offset, &(struct sampler_modulation_key){71, smsrc_none, smdest_resonance}, TRUE, 1); + sampler_modulation_set_amount_by_offset(l, mod_offset, &(struct sampler_modulation_key){smsrc_pitchlfo, 1, smdest_pitch}, TRUE, 100); + } + l->runtime = NULL; + l->unknown_keys = NULL; + CBOX_OBJECT_REGISTER(l); + return l; +} + +#define PROC_FIELDS_CLONE(type, name, def_value) \ + dst->name = src->name; \ + dst->has_##name = copy_hasattr ? src->has_##name : FALSE; +#define PROC_FIELDS_CLONE_string(name) \ + dst->name = src->name ? g_strdup(src->name) : NULL; \ + dst->name##_changed = src->name##_changed; \ + dst->has_##name = copy_hasattr ? src->has_##name : FALSE; +#define PROC_FIELDS_CLONE_midicurve(name) \ + memcpy(dst->name.values, src->name.values, sizeof(float) * 128); \ + if(copy_hasattr) \ + memcpy(dst->name.has_values, src->name.has_values, sizeof(src->name.has_values)); +#define PROC_FIELDS_CLONE_dBamp PROC_FIELDS_CLONE +#define PROC_FIELDS_CLONE_enum PROC_FIELDS_CLONE +#define PROC_FIELDS_CLONE_dahdsr(name, parname, index) \ + DAHDSR_FIELDS(PROC_SUBSTRUCT_CLONE, name, dst, src) \ + if (!copy_hasattr) \ + DAHDSR_FIELDS(PROC_SUBSTRUCT_RESET_HAS_FIELD, name, dst) +#define PROC_FIELDS_CLONE_lfo(name, parname, index) \ + LFO_FIELDS(PROC_SUBSTRUCT_CLONE, name, dst, src) \ + if (!copy_hasattr) \ + LFO_FIELDS(PROC_SUBSTRUCT_RESET_HAS_FIELD, name, dst) +#define PROC_FIELDS_CLONE_eq(name, parname, index) \ + EQ_FIELDS(PROC_SUBSTRUCT_CLONE, name, dst, src) \ + if (!copy_hasattr) \ + EQ_FIELDS(PROC_SUBSTRUCT_RESET_HAS_FIELD, name, dst) +#define PROC_FIELDS_CLONE_ccrange(name, parname) \ + dst->name = sampler_cc_range_clone(src->name, copy_hasattr); + +void sampler_layer_data_clone(struct sampler_layer_data *dst, const struct sampler_layer_data *src, gboolean copy_hasattr) +{ + SAMPLER_FIXED_FIELDS(PROC_FIELDS_CLONE) + dst->modulations = sampler_modulation_clone(src->modulations, copy_hasattr); + dst->voice_nifs = sampler_noteinitfunc_clone(src->voice_nifs, copy_hasattr); + dst->prevoice_nifs = sampler_noteinitfunc_clone(src->prevoice_nifs, copy_hasattr); + dst->flex_lfos = sampler_flex_lfo_clone(src->flex_lfos, copy_hasattr); + dst->computed.eff_waveform = src->computed.eff_waveform; + if (dst->computed.eff_waveform) + cbox_waveform_ref(dst->computed.eff_waveform); +} + +void sampler_midi_curve_init(struct sampler_midi_curve *curve) +{ + for (uint32_t i = 0; i < 128; ++i) + curve->values[i] = SAMPLER_CURVE_GAP; + memset(curve->has_values, 0, 128); +} + +void sampler_midi_curve_interpolate(const struct sampler_midi_curve *curve, float dest[128], float def_start, float def_end, gboolean is_quadratic) +{ + const float *src = curve->values; + int start = 0; + float sv = src[start]; + if (sv == SAMPLER_CURVE_GAP) + sv = def_start; + if (is_quadratic && sv >= 0) + sv = sqrtf(sv); + for (int i = 1; i < 128; i++) + { + float ev = src[i]; + if (ev == SAMPLER_CURVE_GAP) + { + if (i < 127) + continue; + else + ev = def_end; + } + if (is_quadratic && ev >= 0) + ev = sqrtf(ev); + if (is_quadratic) + { + for (int j = start; j <= i; j++) + dest[j] = powf(sv + (ev - sv) * (j - start) / (i - start), 2.f); + } + else + { + for (int j = start; j <= i; j++) + dest[j] = sv + (ev - sv) * (j - start) / (i - start); + } + start = i; + sv = ev; + } +} + +static inline int sampler_filter_num_stages(float cutoff, enum sampler_filter_type fil_type) +{ + if (cutoff == -1) + return 0; + if (fil_type == sft_lp24hybrid || fil_type == sft_lp24 || fil_type == sft_lp24nr || fil_type == sft_hp24 || fil_type == sft_hp24nr || fil_type == sft_bp12) + return 2; + if (fil_type == sft_lp36) + return 3; + return 1; +} + + +// If veltrack > 0, then the default range goes from -84dB to 0dB +// If veltrack == 0, then the default range is all 0dB +// If veltrack < 0, then the default range goes from 0dB to -84dB +#define START_VALUE_amp_velcurve (l->amp_veltrack > 0 ? dB2gain(-l->amp_veltrack * 84.0 / 100.0) : 1) +#define END_VALUE_amp_velcurve (l->amp_veltrack < 0 ? dB2gain(l->amp_veltrack * 84.0 / 100.0) : 1) +#define IS_QUADRATIC_amp_velcurve l->velcurve_quadratic + +#define PROC_FIELDS_FINALISER(type, name, def_value) +#define PROC_FIELDS_FINALISER_string(name) +#define PROC_FIELDS_FINALISER_midicurve(name) \ + sampler_midi_curve_interpolate(&l->name, l->computed.eff_##name, START_VALUE_##name, END_VALUE_##name, IS_QUADRATIC_##name); +#define PROC_FIELDS_FINALISER_enum(type, name, def_value) +#define PROC_FIELDS_FINALISER_dBamp(type, name, def_value) \ + l->name##_linearized = dB2gain(l->name); +#define PROC_FIELDS_FINALISER_dahdsr(name, parname, index) \ + cbox_envelope_init_dahdsr(&l->name##_shape, &l->name, m->module.srate / CBOX_BLOCK_SIZE, 100.f, &l->name##_shape == &l->amp_env_shape); +#define PROC_FIELDS_FINALISER_lfo(name, parname, index) /* no finaliser required */ +#define PROC_FIELDS_FINALISER_eq(name, parname, index) l->name.effective_freq = (l->name.freq ? l->name.freq : 5 * powf(10.f, 1 + (index))); +#define PROC_FIELDS_FINALISER_ccrange(name, parname) /* no finaliser required */ + +void sampler_layer_data_finalize(struct sampler_layer_data *l, struct sampler_layer_data *parent, struct sampler_program *p) +{ + struct sampler_module *m = p->module; + SAMPLER_FIXED_FIELDS(PROC_FIELDS_FINALISER) + + // Handle change of sample in the parent group without override on region level + if (parent && (l->sample_changed || parent->sample_changed)) + { + struct cbox_waveform *oldwf = l->computed.eff_waveform; + if (l->sample && *l->sample) + { + GError *error = NULL; + l->computed.eff_waveform = cbox_wavebank_get_waveform(p->name, p->tarfile, p->sample_dir, l->sample, &error); + if (!l->computed.eff_waveform) + { + g_warning("Cannot load waveform %s: %s", l->sample, error ? error->message : "unknown error"); + g_error_free(error); + } + } + else + l->computed.eff_waveform = NULL; + if (oldwf) + cbox_waveform_unref(oldwf); + l->computed.eff_is_silent = !l->sample || !strcmp(l->sample, "*silence"); + l->sample_changed = FALSE; + } + + l->computed.eff_use_keyswitch = ((l->sw_down != -1) || (l->sw_up != -1) || (l->sw_last != -1) || (l->sw_previous != -1)); + l->computed.eff_use_simple_trigger_logic = + (l->seq_length == 1 && l->seq_position == 1) && + (l->trigger != stm_first && l->trigger != stm_legato) && + (l->lochan == 1 && l->hichan == 16) && + (l->lorand == 0 && l->hirand == 1) && + (l->lobend == -8192 && l->hibend == 8192) && + (l->lochanaft == 0 && l->hichanaft == 127) && + (l->lopolyaft == 0 && l->hipolyaft == 127) && + (l->lobpm == 0 && l->hibpm == NO_HI_BPM_VALUE) && + !l->cc && !l->computed.eff_use_keyswitch; + l->computed.eff_use_xfcc = l->xfin_cc || l->xfout_cc; + l->computed.eff_use_channel_mixer = l->position != 0 || l->width != 100; + l->computed.eff_freq = (l->computed.eff_waveform && l->computed.eff_waveform->info.samplerate) ? l->computed.eff_waveform->info.samplerate : 44100; + l->computed.eff_loop_mode = l->loop_mode; + l->computed.eff_use_filter_mods = l->cutoff != -1 || l->cutoff2 != -1; + if (l->loop_mode == slm_unknown) + { + if (l->computed.eff_waveform && l->computed.eff_waveform->has_loop) + l->computed.eff_loop_mode = slm_loop_continuous; + else + if (l->computed.eff_waveform) + l->computed.eff_loop_mode = l->loop_end == 0 ? slm_no_loop : slm_loop_continuous; + } + + l->computed.eff_loop_start = l->loop_start; + l->computed.eff_loop_end = l->loop_end; + if (l->computed.eff_loop_mode == slm_one_shot || l->computed.eff_loop_mode == slm_no_loop || l->computed.eff_loop_mode == slm_one_shot_chokeable) + l->computed.eff_loop_start = SAMPLER_NO_LOOP; + if ((l->computed.eff_loop_mode == slm_loop_continuous || l->computed.eff_loop_mode == slm_loop_sustain) && l->computed.eff_loop_start == SAMPLER_NO_LOOP) + l->computed.eff_loop_start = 0; + if ((l->computed.eff_loop_mode == slm_loop_continuous || l->computed.eff_loop_mode == slm_loop_sustain) && l->computed.eff_loop_start == 0 && l->computed.eff_waveform && l->computed.eff_waveform->has_loop) + l->computed.eff_loop_start = l->computed.eff_waveform->loop_start; + if (l->loop_end == 0 && l->computed.eff_waveform != NULL) + l->computed.eff_loop_end = l->computed.eff_waveform->has_loop ? l->computed.eff_waveform->loop_end : l->computed.eff_waveform->info.frames; + + if (l->off_mode == som_unknown) + l->off_mode = l->off_by != 0 ? som_fast : som_normal; + + // XXXKF this is dodgy, needs to convert to use 'programmed vs effective' values pattern + if (l->key >= 0 && l->key <= 127) + l->lokey = l->hikey = l->pitch_keycenter = l->key; + + // 'linearize' the virtual circular buffer - write 3 (or N) frames before end of the loop + // and 3 (N) frames at the start of the loop, and play it; in rare cases this will need to be + // repeated twice if output write pointer is close to CBOX_BLOCK_SIZE or playback rate is very low, + // but that's OK. + if (l->computed.eff_waveform && l->computed.eff_waveform->preloaded_frames == (size_t)l->computed.eff_waveform->info.frames) + { + int shift = l->computed.eff_waveform->info.channels == 2 ? 1 : 0; + uint32_t halfscratch = MAX_INTERPOLATION_ORDER << shift; + memcpy(&l->computed.scratch_loop[0], &l->computed.eff_waveform->data[(l->computed.eff_loop_end - MAX_INTERPOLATION_ORDER) << shift], halfscratch * sizeof(int16_t) ); + memcpy(&l->computed.scratch_end[0], &l->computed.eff_waveform->data[(l->computed.eff_loop_end - MAX_INTERPOLATION_ORDER) << shift], halfscratch * sizeof(int16_t) ); + memset(l->computed.scratch_end + halfscratch, 0, halfscratch * sizeof(int16_t)); + if (l->computed.eff_loop_start != (uint32_t)-1) + memcpy(l->computed.scratch_loop + halfscratch, &l->computed.eff_waveform->data[l->computed.eff_loop_start << shift], halfscratch * sizeof(int16_t)); + else + memset(l->computed.scratch_loop + halfscratch, 0, halfscratch * sizeof(int16_t)); + } + if (l->cutoff < 20) + l->computed.logcutoff = -1; + else + l->computed.logcutoff = 1200.0 * log(l->cutoff / 440.0) / log(2) + 5700.0; + + if (l->cutoff2 < 20) + l->computed.logcutoff2 = -1; + else + l->computed.logcutoff2 = 1200.0 * log(l->cutoff2 / 440.0) / log(2) + 5700.0; + + l->computed.eq_bitmask = ((l->eq1.gain != 0 || l->eq1.vel2gain != 0) ? 1 : 0) + | ((l->eq2.gain != 0 || l->eq2.vel2gain != 0) ? 2 : 0) + | ((l->eq3.gain != 0 || l->eq3.vel2gain != 0) ? 4 : 0); + l->computed.mod_bitmask = 0; + for(struct sampler_modulation *mod = l->modulations; mod; mod = mod->next) + { + const struct sampler_modulation_key *mk = &mod->key; + if (mk->dest >= smdest_eg_stage_start && mk->dest <= smdest_eg_stage_end) + l->computed.mod_bitmask |= slmb_ampeg_cc << ((mk->dest >> 4) & 3); + } + + l->computed.eff_use_prevoice = (l->delay || l->prevoice_nifs); + l->computed.eff_num_stages = sampler_filter_num_stages(l->cutoff, l->fil_type); + l->computed.eff_num_stages2 = sampler_filter_num_stages(l->cutoff2, l->fil2_type); + + l->computed.resonance_scaled = pow(l->resonance_linearized, 1.f / l->computed.eff_num_stages); + l->computed.resonance2_scaled = pow(l->resonance2_linearized, 1.f / l->computed.eff_num_stages2); +} + +void sampler_layer_reset_switches(struct sampler_layer *l, struct sampler_module *m) +{ + l->current_seq_position = l->data.seq_position; +} + +struct layer_foreach_struct +{ + struct sampler_layer *layer; + const char *cfg_section; +}; + +static void layer_foreach_func(void *user_data, const char *key) +{ + if (!strcmp(key, "file")) + key = "sample"; + // import is handled in sampler_load_layer_overrides + if (!strcmp(key, "import")) + return; + // layer%d should be ignored, it's handled by sampler_program_new_from_cfg + if (!strncmp(key, "layer", 5) && isdigit(key[5])) + return; + struct layer_foreach_struct *lfs = user_data; + const char *value = cbox_config_get_string(lfs->cfg_section, key); + GError *error = NULL; + if (!sampler_layer_apply_param(lfs->layer, key, value, &error)) + { + if (error) + g_warning("Error '%s', context: %s in section %s", error->message, key, lfs->cfg_section); + else + g_warning("Unknown sample layer parameter: %s in section %s", key, lfs->cfg_section); + } +} + +void sampler_layer_load_overrides(struct sampler_layer *l, const char *cfg_section) +{ + char *imp = cbox_config_get_string(cfg_section, "import"); + if (imp) + sampler_layer_load_overrides(l, imp); + + struct layer_foreach_struct lfs = { + .layer = l, + .cfg_section = cfg_section + }; + cbox_config_foreach_key(layer_foreach_func, cfg_section, &lfs); +} + +struct sampler_layer *sampler_layer_new_from_section(struct sampler_module *m, struct sampler_program *parent_program, struct sampler_layer *parent, const char *cfg_section) +{ + struct sampler_layer *l = sampler_layer_new(m, parent_program, parent ? parent : parent_program->global->default_child->default_child); + sampler_layer_load_overrides(l, cfg_section); + sampler_layer_data_finalize(&l->data, l->parent ? &l->parent->data : NULL, parent_program); + sampler_layer_reset_switches(l, m); + return l; +} + +static void sampler_layer_apply_unknown(struct sampler_layer *l, const char *key, const char *value) +{ + if (!l->unknown_keys) + l->unknown_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + + g_hash_table_insert(l->unknown_keys, g_strdup(key), g_strdup(value)); +} + +gboolean sampler_layer_apply_param(struct sampler_layer *l, const char *key, const char *value, GError **error) +{ + int res = sampler_layer_apply_fixed_param(l, key, value, error); + if (res >= 0) + return res; + sampler_layer_apply_unknown(l, key, value); + //g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown SFZ property key: '%s'", key); + //return FALSE; + g_warning("Unknown SFZ property key: '%s'", key); + return TRUE; +} + +gboolean sampler_layer_unapply_param(struct sampler_layer *layer, const char *key, GError **error) +{ + int res = sampler_layer_unapply_fixed_param(layer, key, error); + if (res >= 0) + return res; + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown SFZ property key: '%s'", key); + return FALSE; +} + +#define TYPE_PRINTF_uint32_t(name, def_value) \ + if (show_inherited || l->has_##name) \ + g_string_append_printf(outstr, " %s=%u", #name, (unsigned)(l->name)); +#define TYPE_PRINTF_int(name, def_value) \ + if (show_inherited || l->has_##name) \ + g_string_append_printf(outstr, " %s=%d", #name, (int)(l->name)); +#define TYPE_PRINTF_midi_note_t(name, def_value) \ + if (show_inherited || l->has_##name) { \ + int val = l->name; \ + if (val == -1) \ + g_string_append_printf(outstr, " %s=-1", #name); \ + else \ + g_string_append_printf(outstr, " %s=%c%s%d", #name, "ccddeffggaab"[val%12], "\000#\000#\000\000#\000#\000#\000#\000"+(val%12), (val/12-1)); \ + } else {} +#define TYPE_PRINTF_float(name, def_value) \ + if (show_inherited || l->has_##name) \ + g_string_append_printf(outstr, " %s=%s", #name, g_ascii_dtostr(floatbuf, floatbufsize, l->name)); + +#define PROC_FIELDS_TO_FILEPTR(type, name, def_value) \ + TYPE_PRINTF_##type(name, def_value) +#define PROC_FIELDS_TO_FILEPTR_string(name) \ + if (show_inherited || l->has_##name) \ + g_string_append_printf(outstr, " %s=%s", #name, l->name ? l->name : ""); +#define PROC_FIELDS_TO_FILEPTR_midicurve(name) \ + for (uint32_t i = 0; i < 128; ++i) { \ + if ((show_inherited || l->name.has_values[i]) && l->name.values[i] != SAMPLER_CURVE_GAP) \ + g_string_append_printf(outstr, " %s_%u=%s", #name, (unsigned)i, g_ascii_dtostr(floatbuf, floatbufsize, l->name.values[i])); \ + } +#define PROC_FIELDS_TO_FILEPTR_dBamp(type, name, def_value) \ + if (show_inherited || l->has_##name) \ + g_string_append_printf(outstr, " %s=%s", #name, g_ascii_dtostr(floatbuf, floatbufsize, l->name)); +#define PROC_FIELDS_TO_FILEPTR_enum(enumtype, name, def_value) \ + if ((show_inherited || l->has_##name) && (tmpstr = enumtype##_to_string(l->name)) != NULL) \ + g_string_append_printf(outstr, " %s=%s", #name, tmpstr); + +#define ENV_PARAM_OUTPUT(param, index, def_value, env, envfield, envname) \ + if (show_inherited || l->has_##envfield.param) \ + g_string_append_printf(outstr, " " #envname "_" #param "=%s", g_ascii_dtostr(floatbuf, floatbufsize, env.param)); + +#define PROC_FIELDS_TO_FILEPTR_dahdsr(name, parname, index) \ + DAHDSR_FIELDS(ENV_PARAM_OUTPUT, l->name, name, parname) +#define PROC_FIELDS_TO_FILEPTR_lfo(name, parname, index) \ + LFO_FIELDS(ENV_PARAM_OUTPUT, l->name, name, parname) +#define PROC_FIELDS_TO_FILEPTR_eq(name, parname, index) \ + EQ_FIELDS(ENV_PARAM_OUTPUT, l->name, name, parname) +#define PROC_FIELDS_TO_FILEPTR_ccrange(name, parname) \ + { \ + struct sampler_cc_range *range = l->name; \ + while (range) { \ + if (show_inherited || range->value.has_locc) \ + g_string_append_printf(outstr, " " #parname "locc%d=%d", range->key.cc_number, range->value.locc); \ + if (show_inherited || range->value.has_hicc) \ + g_string_append_printf(outstr, " " #parname "hicc%d=%d", range->key.cc_number, range->value.hicc); \ + range = range->next; \ + } \ + } + +static const char *addrandom_variants[] = { "amp", "fil", "pitch" }; +static const char *env_stages[] = { "delay", "attack", "hold", "decay", "sustain", "release", "start" }; +static const char *modsrc_names[] = { "vel", "chanaft", "polyaft", "pitch", "pitcheg", "fileg", "ampeg", "pitchlfo", "fillfo", "amplfo", "" }; +static const char *moddest_names[] = { "gain", "pitch", "cutoff", "resonance", "tonectl", "pan", "amplitude", "cutoff2", "resonance2", "pitchlfo_freq", "fillfo_freq", "amplfo_freq", + "eq1_freq", "eq1_bw", "eq1_gain", + "eq2_freq", "eq2_bw", "eq2_gain", + "eq3_freq", "eq3_bw", "eq3_gain", + }; + +static void mod_cc_attrib_to_string(GString *outstr, const char *attrib, const struct sampler_modulation_key *md, const char *floatbuf) +{ + if (md->dest >= smdest_eg_stage_start && md->dest <= smdest_eg_stage_end) + { + uint32_t param = md->dest - smdest_eg_stage_start; + g_string_append_printf(outstr, " %seg_%s%s%d=%s", addrandom_variants[(param >> 4) & 3], env_stages[param & 15], attrib, md->src, floatbuf); + } + else if (md->src < smsrc_perchan_count) + { + g_string_append_printf(outstr, " %s%s%d=%s", moddest_names[md->dest], attrib, md->src, floatbuf); + } + else if ((md->src == smsrc_amplfo && md->dest == smdest_gain) || + (md->src == smsrc_fillfo && md->dest == smdest_cutoff) || + (md->src == smsrc_pitchlfo && md->dest == smdest_pitch)) + { + if (md->src2 < EXT_CC_COUNT) + g_string_append_printf(outstr, " %s_depth%s%d=%s", modsrc_names[md->src - smsrc_perchan_count], attrib, md->src2, floatbuf); + } + else if ((md->src == smsrc_ampenv && md->dest == smdest_gain) || + (md->src == smsrc_filenv && md->dest == smdest_cutoff) || + (md->src == smsrc_pitchenv && md->dest == smdest_pitch)) + { + if (md->src2 < EXT_CC_COUNT) + g_string_append_printf(outstr, " %s_depth%s%d=%s", modsrc_names[md->src - smsrc_perchan_count], attrib, md->src2, floatbuf); + } + else if ((md->src == smsrc_filenv && md->dest == smdest_cutoff2) || + (md->src == smsrc_fillfo && md->dest == smdest_cutoff2)) + { + if (md->src2 < EXT_CC_COUNT) + g_string_append_printf(outstr, " %s_depth2%s%d=%s", modsrc_names[md->src - smsrc_perchan_count], attrib, md->src2, floatbuf); + } + else + assert(md->src2 >= EXT_CC_COUNT); +} + +static void nif_attrib_to_string(GString *outstr, const char *attrib, const struct sampler_noteinitfunc *nd, const char *floatbuf) +{ + int v = nd->key.variant; + if (nd->value.value) + g_string_append_printf(outstr, " %s_cc%d=%s", attrib, v, floatbuf); + if (nd->value.curve_id) + g_string_append_printf(outstr, " %s_curvecc%d=%d", attrib, v, nd->value.curve_id); + if (nd->value.step) + { + char floatbuf2[G_ASCII_DTOSTR_BUF_SIZE]; + int floatbufsize = G_ASCII_DTOSTR_BUF_SIZE; + g_ascii_dtostr(floatbuf2, floatbufsize, nd->value.step); + g_string_append_printf(outstr, " %s_stepcc%d=%s", attrib, v, floatbuf2); + } +} + +gchar *sampler_layer_to_string(struct sampler_layer *lr, gboolean show_inherited) +{ + struct sampler_layer_data *l = &lr->data; + GString *outstr = g_string_sized_new(200); + const char *tmpstr; + char floatbuf[G_ASCII_DTOSTR_BUF_SIZE]; + int floatbufsize = G_ASCII_DTOSTR_BUF_SIZE; + SAMPLER_FIXED_FIELDS(PROC_FIELDS_TO_FILEPTR) + + for(struct sampler_noteinitfunc *nd = l->voice_nifs; nd; nd = nd->next) + { + if (!nd->value.has_value && !nd->value.has_curve && !nd->value.has_step && !show_inherited) + continue; + #define PROC_ENVSTAGE_NAME(name, index, def_value) #name, + static const char *env_stages[] = { DAHDSR_FIELDS(PROC_ENVSTAGE_NAME) "start" }; + uint32_t v = nd->key.variant; + g_ascii_dtostr(floatbuf, floatbufsize, nd->value.value); + + if (nd->key.notefunc_voice == sampler_nif_addrandom && v >= 0 && v <= 2) + g_string_append_printf(outstr, " %s_random=%s", addrandom_variants[v], floatbuf); + else if (nd->key.notefunc_voice == sampler_nif_vel2pitch) + g_string_append_printf(outstr, " pitch_veltrack=%s", floatbuf); + else if (nd->key.notefunc_voice == sampler_nif_vel2reloffset) + g_string_append_printf(outstr, " reloffset_veltrack=%s", floatbuf); + else if (nd->key.notefunc_voice == sampler_nif_cc2reloffset) + nif_attrib_to_string(outstr, "reloffset", nd, floatbuf); + else if (nd->key.notefunc_voice == sampler_nif_vel2offset) + g_string_append_printf(outstr, " offset_veltrack=%s", floatbuf); + else if (nd->key.notefunc_voice == sampler_nif_cc2offset) + nif_attrib_to_string(outstr, "offset", nd, floatbuf); + else if (nd->key.notefunc_voice == sampler_nif_vel2env && (v & 15) >= snif_env_delay && (v & 15) <= snif_env_start && ((v >> 4) & 3) < 3) + g_string_append_printf(outstr, " %seg_vel2%s=%s", addrandom_variants[v >> 4], env_stages[1 + (v & 15)], floatbuf); + else + assert(0); // unknown NIF + } + for(struct sampler_noteinitfunc *nd = l->prevoice_nifs; nd; nd = nd->next) + { + if (!nd->value.has_value && !nd->value.has_curve && !nd->value.has_step && !show_inherited) + continue; + g_ascii_dtostr(floatbuf, floatbufsize, nd->value.value); + + if (nd->key.notefunc_prevoice == sampler_nif_cc2delay) + nif_attrib_to_string(outstr, "delay", nd, floatbuf); + else if (nd->key.notefunc_prevoice == sampler_nif_addrandomdelay) + g_string_append_printf(outstr, " delay_random=%s", floatbuf); + else + assert(0); // unknown NIF + } + for(struct sampler_flex_lfo *flfo = l->flex_lfos; flfo; flfo = flfo->next) + { + if (flfo->value.has_freq || show_inherited) + { + g_ascii_dtostr(floatbuf, floatbufsize, flfo->value.freq); + g_string_append_printf(outstr, " lfo%d_freq=%s", (int)flfo->key.id, floatbuf); + } + if (flfo->value.has_delay || show_inherited) + { + g_ascii_dtostr(floatbuf, floatbufsize, flfo->value.delay); + g_string_append_printf(outstr, " lfo%d_delay=%s", (int)flfo->key.id, floatbuf); + } + if (flfo->value.has_fade || show_inherited) + { + g_ascii_dtostr(floatbuf, floatbufsize, flfo->value.fade); + g_string_append_printf(outstr, " lfo%d_fade=%s", (int)flfo->key.id, floatbuf); + } + if (flfo->value.has_wave || show_inherited) + g_string_append_printf(outstr, " lfo%d_wave=%d", (int)flfo->key.id, flfo->value.wave); + } + for(struct sampler_modulation *md = l->modulations; md; md = md->next) + { + const struct sampler_modulation_key *mk = &md->key; + const struct sampler_modulation_value *mv = &md->value; + if (mv->has_curve || show_inherited) + { + g_ascii_dtostr(floatbuf, floatbufsize, mv->curve_id); + mod_cc_attrib_to_string(outstr, "_curvecc", mk, floatbuf); + } + if (mv->has_smooth || show_inherited) + { + g_ascii_dtostr(floatbuf, floatbufsize, mv->smooth); + mod_cc_attrib_to_string(outstr, "_smoothcc", mk, floatbuf); + } + if (mv->has_step || show_inherited) + { + g_ascii_dtostr(floatbuf, floatbufsize, mv->step); + mod_cc_attrib_to_string(outstr, "_stepcc", mk, floatbuf); + } + if (mv->has_amount || show_inherited) + { + gboolean is_egcc = mk->dest >= smdest_eg_stage_start && mk->dest <= smdest_eg_stage_end; + gboolean is_lfofreq = mk->dest >= smdest_pitchlfo_freq && mk->dest <= smdest_eq3_gain; + g_ascii_dtostr(floatbuf, floatbufsize, mv->amount); + + if (mk->src2 == smsrc_none) + { + if (is_egcc) + { + uint32_t param = mk->dest - smdest_eg_stage_start; + g_string_append_printf(outstr, " %seg_%scc%d=%s", addrandom_variants[(param >> 4) & 3], env_stages[param & 15], mk->src, floatbuf); + continue; + } + if (mk->src < smsrc_perchan_count) + { + // Inconsistency: cutoff_cc5 but amplfo_freqcc5 + if (is_lfofreq) + g_string_append_printf(outstr, " %scc%d=%s", moddest_names[mk->dest], mk->src, floatbuf); + else + g_string_append_printf(outstr, " %s_cc%d=%s", moddest_names[mk->dest], mk->src, floatbuf); + continue; + } + if (mk->src < smsrc_perchan_count + sizeof(modsrc_names) / sizeof(modsrc_names[0])) + { + if ((mk->src == smsrc_filenv && mk->dest == smdest_cutoff) || + (mk->src == smsrc_pitchenv && mk->dest == smdest_pitch) || + (mk->src == smsrc_amplfo && mk->dest == smdest_gain) || + (mk->src == smsrc_fillfo && mk->dest == smdest_cutoff) || + (mk->src == smsrc_pitchlfo && mk->dest == smdest_pitch)) + g_string_append_printf(outstr, " %s_depth=%s", modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + else if ((mk->src == smsrc_filenv && mk->dest == smdest_cutoff2) || + (mk->src == smsrc_fillfo && mk->dest == smdest_cutoff2)) + g_string_append_printf(outstr, " %s_depth2=%s", modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + else if (is_lfofreq) + g_string_append_printf(outstr, " %s%s=%s", moddest_names[mk->dest], modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + else + g_string_append_printf(outstr, " %s_%s=%s", moddest_names[mk->dest], modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + continue; + } + } + if ((mk->src == smsrc_amplfo && mk->dest == smdest_gain) || + (mk->src == smsrc_fillfo && mk->dest == smdest_cutoff) || + (mk->src == smsrc_pitchlfo && mk->dest == smdest_pitch)) + { + switch(mk->src2) + { + case smsrc_chanaft: + case smsrc_polyaft: + g_string_append_printf(outstr, " %s_depth%s=%s", modsrc_names[mk->src - smsrc_perchan_count], modsrc_names[mk->src2 - smsrc_perchan_count], floatbuf); + continue; + case smsrc_none: + g_string_append_printf(outstr, " %s_depth=%s", modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + continue; + default: + if (mk->src2 < EXT_CC_COUNT) + { + g_string_append_printf(outstr, " %s_depthcc%d=%s", modsrc_names[mk->src - smsrc_perchan_count], mk->src2, floatbuf); + continue; + } + break; + } + } + if ((mk->src == smsrc_ampenv && mk->dest == smdest_gain) || + (mk->src == smsrc_filenv && mk->dest == smdest_cutoff) || + (mk->src == smsrc_pitchenv && mk->dest == smdest_pitch)) + { + if (mk->src2 == smsrc_vel) + { + g_string_append_printf(outstr, " %s_vel2depth=%s", modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + continue; + } + if (mk->src2 == smsrc_none) + { + g_string_append_printf(outstr, " %s_depth=%s", modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + continue; + } + if (mk->src2 < EXT_CC_COUNT) + { + g_string_append_printf(outstr, " %s_depthcc%d=%s", modsrc_names[mk->src - smsrc_perchan_count], mk->src2, floatbuf); + continue; + } + } + if (mk->src == smsrc_filenv && mk->dest == smdest_cutoff2) + { + if (mk->src2 == smsrc_vel) + { + g_string_append_printf(outstr, " %s_vel2depth2=%s", modsrc_names[mk->src - smsrc_perchan_count], floatbuf); + continue; + } + assert(mk->src2 != smsrc_none); + if (mk->src2 < EXT_CC_COUNT) + { + g_string_append_printf(outstr, " %s_depth2cc%d=%s", modsrc_names[mk->src - smsrc_perchan_count], mk->src2, floatbuf); + continue; + } + } + if (mk->src == smsrc_fillfo && mk->dest == smdest_cutoff2) + { + assert(mk->src2 != smsrc_none); + if (mk->src2 < EXT_CC_COUNT) + { + g_string_append_printf(outstr, " %s_depth2cc%d=%s", modsrc_names[mk->src - smsrc_perchan_count], mk->src2, floatbuf); + continue; + } + } + g_string_append_printf(outstr, " genericmod_%d_%d_%d_%d=%s", mk->src, mk->src2, mk->dest, mv->curve_id, floatbuf); + } + } + + if (lr->unknown_keys) + { + GHashTableIter hti; + gchar *key, *value; + g_hash_table_iter_init(&hti, lr->unknown_keys); + while(g_hash_table_iter_next(&hti, (gpointer *)&key, (gpointer *)&value)) + g_string_append_printf(outstr, " %s=%s", key, value); + } + + gchar *res = outstr->str; + g_string_free(outstr, FALSE); + return res; +} + +void sampler_layer_dump(struct sampler_layer *l, FILE *f) +{ + gchar *str = sampler_layer_to_string(l, FALSE); + fprintf(f, "%s\n", str); +} + +void sampler_layer_data_close(struct sampler_layer_data *l) +{ + sampler_flex_lfos_destroy(l->flex_lfos); + sampler_cc_ranges_destroy(l->cc); + sampler_cc_ranges_destroy(l->on_cc); + sampler_cc_ranges_destroy(l->xfin_cc); + sampler_cc_ranges_destroy(l->xfout_cc); + sampler_noteinitfuncs_destroy(l->voice_nifs); + sampler_noteinitfuncs_destroy(l->prevoice_nifs); + sampler_modulations_destroy(l->modulations); + if (l->computed.eff_waveform) + { + cbox_waveform_unref(l->computed.eff_waveform); + l->computed.eff_waveform = NULL; + } + g_free(l->sample); +} + +void sampler_layer_data_destroy(struct sampler_layer_data *l) +{ + sampler_layer_data_close(l); + free(l); +} + +struct sampler_layer *sampler_layer_new_clone(struct sampler_layer *layer, + struct sampler_module *m, struct sampler_program *parent_program, struct sampler_layer *parent) +{ + struct sampler_layer *l = sampler_layer_new(m, parent_program, parent); + sampler_layer_data_clone(&l->data, &layer->data, TRUE); + sampler_layer_reset_switches(l, m); + if (layer->unknown_keys) + { + GHashTableIter iter; + g_hash_table_iter_init(&iter, layer->unknown_keys); + gpointer key, value; + while(g_hash_table_iter_next(&iter, &key, &value)) + sampler_layer_apply_param(l, (gchar *)key, (gchar *)value, NULL); + } + + GHashTableIter iter; + g_hash_table_iter_init(&iter, layer->child_layers); + gpointer key, value; + gboolean is_child_a_region = layer->parent && layer->parent->parent; + while(g_hash_table_iter_next(&iter, &key, &value)) + { + struct sampler_layer *chl = sampler_layer_new_clone(key, m, parent_program, l); + g_hash_table_insert(l->child_layers, chl, NULL); + if (key == layer->default_child) + l->default_child = chl; + if (is_child_a_region) + sampler_program_add_layer(parent_program, chl); + } + + return l; +} + +void sampler_layer_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct sampler_layer *l = CBOX_H2O(objhdr); + struct sampler_program *prg = l->parent_program; + assert(g_hash_table_size(l->child_layers) == 0); + + if (l->parent) + { + g_hash_table_remove(l->parent->child_layers, l); + if (prg && prg->rll) + { + sampler_program_delete_layer(prg, l); + sampler_program_update_layers(l->parent_program); + } + l->parent = NULL; + } + sampler_layer_data_close(&l->data); + if (l->runtime) + sampler_layer_data_destroy(l->runtime); + if (l->unknown_keys) + g_hash_table_destroy(l->unknown_keys); + if (l->child_layers) + g_hash_table_destroy(l->child_layers); + + free(l); +} + +////////////////////////////////////////////////////////////////////////// + +struct sampler_layer_update_cmd +{ + struct sampler_module *module; + struct sampler_layer *layer; + struct sampler_layer_data *new_data; + struct sampler_layer_data *old_data; +}; + +static int sampler_layer_update_cmd_prepare(void *data) +{ + struct sampler_layer_update_cmd *cmd = data; + cmd->old_data = cmd->layer->runtime; + cmd->new_data = calloc(1, sizeof(struct sampler_layer_data)); + + sampler_layer_data_clone(cmd->new_data, &cmd->layer->data, TRUE); + sampler_layer_data_finalize(cmd->new_data, cmd->layer->parent ? &cmd->layer->parent->data : NULL, cmd->layer->parent_program); + if (cmd->layer->runtime == NULL) + { + // initial update of the layer, so none of the voices need updating yet + // because the layer hasn't been allocated to any voice + cmd->layer->runtime = cmd->new_data; + free(cmd); + return 1; + } + return 0; +} + +static int sampler_layer_update_cmd_execute(void *data) +{ + struct sampler_layer_update_cmd *cmd = data; + + for (int i = 0; i < 16; i++) + { + FOREACH_VOICE(cmd->module->channels[i].voices_running, v) + { + if (v->layer == cmd->layer->runtime) + { + v->layer = cmd->new_data; + v->layer_changed = TRUE; + sampler_voice_update_params_from_layer(v); + } + } + } + FOREACH_PREVOICE(cmd->module->prevoices_running, pv) + { + if (pv->layer_data == cmd->layer->runtime) + { + pv->layer_data = cmd->new_data; + // XXXKF when need arises + // pv->layer_changed = TRUE; + // sampler_prevoice_update_params_from_layer(v); + } + } + cmd->old_data = cmd->layer->runtime; + cmd->layer->runtime = cmd->new_data; + return 10; +} + +static void sampler_layer_update_cmd_cleanup(void *data) +{ + struct sampler_layer_update_cmd *cmd = data; + + sampler_layer_data_destroy(cmd->old_data); + free(cmd); +} + +void sampler_layer_update(struct sampler_layer *l) +{ + // if changing a group, update all child regions instead + if (g_hash_table_size(l->child_layers)) + { + GHashTableIter iter; + g_hash_table_iter_init(&iter, l->child_layers); + gpointer key, value; + while(g_hash_table_iter_next(&iter, &key, &value)) + { + sampler_layer_data_finalize(&((struct sampler_layer *)key)->data, &l->data, l->parent_program); + sampler_layer_update((struct sampler_layer *)key); + } + return; + } + static struct cbox_rt_cmd_definition rtcmd = { + .prepare = sampler_layer_update_cmd_prepare, + .execute = sampler_layer_update_cmd_execute, + .cleanup = sampler_layer_update_cmd_cleanup, + }; + + struct sampler_layer_update_cmd *lcmd = malloc(sizeof(struct sampler_layer_update_cmd)); + lcmd->module = l->module; + lcmd->layer = l; + lcmd->new_data = NULL; + lcmd->old_data = NULL; + + cbox_rt_execute_cmd_async(l->module->module.rt, &rtcmd, lcmd); +} + diff --git a/template/calfbox/sampler_layer.h b/template/calfbox/sampler_layer.h new file mode 100644 index 0000000..0a1b5bc --- /dev/null +++ b/template/calfbox/sampler_layer.h @@ -0,0 +1,718 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SAMPLER_LAYER_H +#define CBOX_SAMPLER_LAYER_H + +#include "dom.h" +#include "wavebank.h" +#include +#include + +// arbitrary value that doesn't collide with a useful range +#define SAMPLER_CURVE_GAP -100000 +#define NO_HI_BPM_VALUE 10000 +#define CC_COUNT 128 +#define EXT_CC_COUNT 143 + +struct sampler_program; +struct sampler_voice; +struct sampler_prevoice; +struct sampler_noteinitfunc; +struct sampler_module; + +enum sampler_player_type +{ + spt_inactive, + spt_mono16, + spt_stereo16, + spt_finished +}; + +enum sampler_loop_mode +{ + slm_unknown, + slm_no_loop, + slm_one_shot, + slm_loop_continuous, + slm_loop_sustain, // unsupported + slm_one_shot_chokeable, + slmcount +}; + +#define ENUM_VALUES_sampler_loop_mode(MACRO) \ + MACRO("no_loop", slm_no_loop) \ + MACRO("one_shot", slm_one_shot) \ + MACRO("loop_continuous", slm_loop_continuous) \ + MACRO("loop_sustain", slm_loop_sustain) \ + MACRO("one_shot_chokeable", slm_one_shot_chokeable) + +enum sampler_off_mode +{ + som_unknown, + som_normal, + som_fast +}; + +#define ENUM_VALUES_sampler_off_mode(MACRO) \ + MACRO("normal", som_normal) \ + MACRO("fast", som_fast) + +enum sampler_vel_mode +{ + svm_unknown, + svm_current, + svm_previous +}; + +#define ENUM_VALUES_sampler_vel_mode(MACRO) \ + MACRO("current", svm_current) \ + MACRO("previous", svm_previous) + +enum sampler_trigger +{ + stm_attack, + stm_release, + stm_first, + stm_legato, +}; + +#define ENUM_VALUES_sampler_trigger(MACRO) \ + MACRO("attack", stm_attack) \ + MACRO("release", stm_release) \ + MACRO("first", stm_first) \ + MACRO("legato", stm_legato) + +enum sampler_filter_type +{ + sft_unknown, + sft_lp12, + sft_hp12, + sft_bp6, + sft_lp24, + sft_hp24, + sft_bp12, + sft_lp6, + sft_hp6, + sft_lp12nr, + sft_hp12nr, + sft_lp24nr, + sft_hp24nr, + sft_lp24hybrid, + sft_lp36, +}; + +#define ENUM_VALUES_sampler_filter_type(MACRO) \ + MACRO("lpf_2p", sft_lp12) \ + MACRO("hpf_2p", sft_hp12) \ + MACRO("bpf_2p", sft_bp6) \ + MACRO("lpf_4p", sft_lp24) \ + MACRO("hpf_4p", sft_hp24) \ + MACRO("bpf_4p", sft_bp12) \ + MACRO("lpf_1p", sft_lp6) \ + MACRO("hpf_1p", sft_hp6) \ + MACRO("lpf_2p_nores", sft_lp12nr) \ + MACRO("hpf_2p_nores", sft_hp12nr) \ + MACRO("lpf_4p_nores", sft_lp24nr) \ + MACRO("hpf_4p_nores", sft_hp24nr) \ + MACRO("lpf_4p_hybrid", sft_lp24hybrid) \ + MACRO("lpf_6p", sft_lp36) \ + +enum sampler_xf_curve +{ + stxc_power, + stxc_gain, +}; + +#define ENUM_VALUES_sampler_xf_curve(MACRO) \ + MACRO("power", stxc_power) \ + MACRO("gain", stxc_gain) + +#define ENUM_LIST(MACRO) \ + MACRO(sampler_loop_mode) \ + MACRO(sampler_off_mode) \ + MACRO(sampler_vel_mode) \ + MACRO(sampler_trigger) \ + MACRO(sampler_filter_type) \ + MACRO(sampler_xf_curve) \ + +#define MAKE_FROM_TO_STRING_EXTERN(enumtype) \ + extern const char *enumtype##_to_string(enum enumtype value); \ + extern gboolean enumtype##_from_string(const char *name, enum enumtype *value); + +ENUM_LIST(MAKE_FROM_TO_STRING_EXTERN) + +enum sampler_modsrc +{ + smsrc_cc0 = 0, + + smsrc_pitchbend = 128, + smsrc_chanaft_sfz2 = 129, + smsrc_lastpolyaft = 130, // ? + smsrc_noteonvel = 131, + smsrc_noteoffvel = 132, + smsrc_keynotenum = 133, + smsrc_keynotegate = 134, + smsrc_random_unipolar = 135, + smsrc_random_bipolar = 136, + smsrc_alternate = 137, + smsrc_keydelta = 140, + smsrc_keydelta_abs = 141, + smsrc_tempo = 142, + + // those are per-note, not per-channel + smsrc_vel, + smsrc_chanaft, + smsrc_polyaft, + smsrc_pitch, + smsrc_pitchenv, + smsrc_filenv, + smsrc_ampenv, + smsrc_pitchlfo, + smsrc_fillfo, + smsrc_amplfo, + smsrc_none, + + smsrccount, + smsrc_perchan_offset = 0, + smsrc_perchan_count = smsrc_vel, + smsrc_pernote_offset = smsrc_vel, + smsrc_pernote_count = smsrccount - smsrc_pernote_offset, + + smsrc_ampeg = smsrc_ampenv, + smsrc_fileg = smsrc_filenv, + smsrc_pitcheg = smsrc_pitchenv, +}; + +enum sampler_moddest +{ + smdest_gain, + smdest_pitch, + smdest_cutoff, + smdest_resonance, + smdest_tonectl, + smdest_pan, + smdest_amplitude, + smdest_cutoff2, + smdest_resonance2, + smdest_pitchlfo_freq, + smdest_fillfo_freq, + smdest_amplfo_freq, + + smdest_eq1_freq, + smdest_eq1_bw, + smdest_eq1_gain, + smdest_eq2_freq, + smdest_eq2_bw, + smdest_eq2_gain, + smdest_eq3_freq, + smdest_eq3_bw, + smdest_eq3_gain, + + smdestcount, + + smdest_from_amplfo = smdest_gain, + smdest_from_fillfo = smdest_cutoff, + smdest_from_pitchlfo = smdest_pitch, + smdest_from_ampeg = smdest_gain, + smdest_from_fileg = smdest_cutoff, + smdest_from_pitcheg = smdest_pitch, + + smdest_ampeg_stage = 0x80, + smdest_fileg_stage = 0x90, + smdest_pitcheg_stage = 0xA0, + + smdest_eg_stage_start = 0x80, + smdest_eg_stage_end = 0xAF, +}; + +struct sampler_modulation_key +{ + enum sampler_modsrc src; + enum sampler_modsrc src2; + enum sampler_moddest dest; +}; + +struct sampler_modulation_value +{ + float amount; + float smooth; + float step; + unsigned int curve_id:12; + unsigned int has_amount:1; + unsigned int has_curve:1; + unsigned int has_smooth:1; + unsigned int has_step:1; +}; + +#define SAMPLER_COLL_FIELD_LIST_sampler_modulation(MACRO, ...) \ + MACRO(amount, has_amount, float, 0, ## __VA_ARGS__) \ + MACRO(curve_id, has_curve, uint32_t, 0, ## __VA_ARGS__) \ + MACRO(smooth, has_smooth, float, 0, ## __VA_ARGS__) \ + MACRO(step, has_step, float, 0, ## __VA_ARGS__) + +#define SAMPLER_COLL_CHAIN_LIST_sampler_modulation(MACRO, ...) \ + MACRO(modulations, modulation, ## __VA_ARGS__) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef void (*SamplerNoteInitFunc)(struct sampler_noteinitfunc *nif, struct sampler_voice *voice); +typedef void (*SamplerNoteInitFunc2)(struct sampler_noteinitfunc *nif, struct sampler_prevoice *prevoice); + +struct sampler_noteinitfunc_key +{ + union { + SamplerNoteInitFunc notefunc_voice; + SamplerNoteInitFunc2 notefunc_prevoice; + }; + int variant; +}; + +struct sampler_noteinitfunc_value +{ + float value; + uint32_t curve_id; + float step; + unsigned int has_value:1; + unsigned int has_curve:1; + unsigned int has_step:1; +}; + +enum sampler_noteinitfunc_envelope_variant +{ + snif_env_delay = 0, + snif_env_attack = 1, + snif_env_hold = 2, + snif_env_decay = 3, + snif_env_sustain = 4, + snif_env_release = 5, + snif_env_start = 6, +}; + +#define SAMPLER_COLL_FIELD_LIST_sampler_noteinitfunc(MACRO, ...) \ + MACRO(value, has_value, float, 0, ## __VA_ARGS__) \ + MACRO(curve_id, has_curve, int, 0, ## __VA_ARGS__) \ + MACRO(step, has_step, float, 0, ## __VA_ARGS__) + +#define SAMPLER_COLL_CHAIN_LIST_sampler_noteinitfunc(MACRO, ...) \ + MACRO(voice_nifs, voice_nif, ## __VA_ARGS__) \ + MACRO(prevoice_nifs, prevoice_nif, ## __VA_ARGS__) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +struct sampler_cc_range_key +{ + uint8_t cc_number; +}; + +struct sampler_cc_range_value +{ + uint8_t locc; + uint8_t hicc; + uint8_t has_locc:1; + uint8_t has_hicc:1; +}; + +#define SAMPLER_COLL_FIELD_LIST_sampler_cc_range(MACRO, ...) \ + MACRO(locc, has_locc, uint8_t, 0, ## __VA_ARGS__) \ + MACRO(hicc, has_hicc, uint8_t, 127, ## __VA_ARGS__) + +#define SAMPLER_COLL_CHAIN_LIST_sampler_cc_range(MACRO, ...) \ + MACRO(on_cc, on_cc, ## __VA_ARGS__) \ + MACRO(cc, cc, ## __VA_ARGS__) \ + MACRO(xfin_cc, xfin_cc, ## __VA_ARGS__) \ + MACRO(xfout_cc, xfout_cc, ## __VA_ARGS__) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +struct sampler_flex_lfo_key +{ + uint32_t id; +}; + +struct sampler_flex_lfo_value +{ + // Note: layout/order must be identical to sampler_lfo_params + float freq; + float delay; + float fade; + int wave; + + uint8_t has_freq:1; + uint8_t has_delay:1; + uint8_t has_fade:1; + uint8_t has_wave:1; +}; + +#define SAMPLER_COLL_FIELD_LIST_sampler_flex_lfo(MACRO, ...) \ + MACRO(freq, has_freq, float, 0, ## __VA_ARGS__) \ + MACRO(delay, has_delay, float, 0, ## __VA_ARGS__) \ + MACRO(fade, has_fade, float, 0, ## __VA_ARGS__) \ + MACRO(wave, has_wave, int, 0, ## __VA_ARGS__) + +#define SAMPLER_COLL_CHAIN_LIST_sampler_flex_lfo(MACRO, ...) \ + MACRO(flex_lfos, flex_lfos, ## __VA_ARGS__) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +#define SAMPLER_COLL_LIST(MACRO) \ + MACRO(sampler_modulation) \ + MACRO(sampler_noteinitfunc) \ + MACRO(sampler_cc_range) \ + MACRO(sampler_flex_lfo) + +#define SAMPLER_COLL_DEFINITION(sname) \ + struct sname \ + { \ + struct sname##_key key; \ + struct sname##_value value; \ + struct sname *next; \ + }; + +SAMPLER_COLL_LIST(SAMPLER_COLL_DEFINITION) + +////////////////////////////////////////////////////////////////////////////////////////////////// + +struct sampler_lfo_params +{ + float freq; + float delay; + float fade; + int wave; +}; + +struct sampler_eq_params +{ + float freq; + float bw; + float gain; + float effective_freq; + float vel2freq; + float vel2gain; +}; + +typedef int midi_note_t; + +/* + * Transforms: + * notransform - self-explanatory + * dBamp - amplitude/gain stored as dB + */ + +#define SAMPLER_FIXED_FIELDS(MACRO) \ + MACRO##_string(sample) \ + MACRO(uint32_t, offset, 0) \ + MACRO(uint32_t, offset_random, 0) \ + MACRO(uint32_t, loop_start, 0) \ + MACRO(uint32_t, loop_end, 0) \ + MACRO(uint32_t, end, 0) \ + MACRO(uint32_t, loop_overlap, (uint32_t)-1) \ + MACRO##_enum(sampler_loop_mode, loop_mode, slm_unknown) \ + MACRO##_enum(sampler_trigger, trigger, stm_attack) \ + MACRO##_dBamp(float, volume, 0) \ + MACRO(float, amplitude, 100) \ + MACRO(float, pan, 0) \ + MACRO(float, position, 0) \ + MACRO(float, width, 100) \ + MACRO(float, tune, 0) \ + MACRO(int, transpose, 0) \ + MACRO(int, lochan, 1) \ + MACRO(int, hichan, 16) \ + MACRO(float, lorand, 0) \ + MACRO(float, hirand, 1) \ + MACRO(midi_note_t, key, -1) \ + MACRO(midi_note_t, lokey, 0) \ + MACRO(midi_note_t, hikey, 127) \ + MACRO(midi_note_t, pitch_keycenter, 60) \ + MACRO(int, pitch_keytrack, 100) \ + MACRO(midi_note_t, fil_keycenter, 60) \ + MACRO(int, fil_keytrack, 0) \ + MACRO(midi_note_t, fil2_keycenter, 60) \ + MACRO(int, fil2_keytrack, 0) \ + MACRO(midi_note_t, amp_keycenter, 60) \ + MACRO(int, amp_keytrack, 0) \ + MACRO(int, fil_veltrack, 0) \ + MACRO(int, fil2_veltrack, 0) \ + MACRO(int, amp_veltrack, 100) \ + MACRO(int, lovel, 0) \ + MACRO(int, hivel, 127) \ + MACRO(int, lobend, -8192) \ + MACRO(int, hibend, 8192) \ + MACRO(int, lochanaft, 0) \ + MACRO(int, hichanaft, 127) \ + MACRO(int, lopolyaft, 0) \ + MACRO(int, hipolyaft, 127) \ + MACRO(float, lobpm, 0.0) \ + MACRO(float, hibpm, NO_HI_BPM_VALUE) \ + MACRO(int, velcurve_quadratic, 1) \ + MACRO##_enum(sampler_filter_type, fil_type, sft_lp12) \ + MACRO##_enum(sampler_filter_type, fil2_type, sft_lp12) \ + MACRO##_enum(sampler_off_mode, off_mode, som_unknown) \ + MACRO##_enum(sampler_vel_mode, vel_mode, svm_current) \ + MACRO(float, cutoff, -1) \ + MACRO##_dBamp(float, resonance, 0) \ + MACRO(float, cutoff2, -1) \ + MACRO##_dBamp(float, resonance2, 0) \ + MACRO(midi_note_t, sw_lokey, 0) \ + MACRO(midi_note_t, sw_hikey, 127) \ + MACRO(midi_note_t, sw_last, -1) \ + MACRO(midi_note_t, sw_down, -1) \ + MACRO(midi_note_t, sw_up, -1) \ + MACRO(midi_note_t, sw_previous, -1) \ + MACRO(midi_note_t, sw_default, -1) \ + MACRO(int, seq_position, 1) \ + MACRO(int, seq_length, 1) \ + MACRO(float, sync_offset, 0) \ + MACRO(int, effect1bus, 1) \ + MACRO(int, effect2bus, 2) \ + MACRO(float, effect1, 0) \ + MACRO(float, effect2, 0) \ + MACRO(float, delay, 0) \ + MACRO(int, output, 0) \ + MACRO(int, group, 0) \ + MACRO(int, off_by, 0) \ + MACRO(int, count, 0) \ + MACRO(int, bend_up, 200) \ + MACRO(int, bend_down, -200) \ + MACRO(int, bend_step, 1) \ + MACRO(int, timestretch, 0) \ + MACRO(float, timestretch_jump, 500) \ + MACRO(float, timestretch_crossfade, 100) \ + MACRO(float, rt_decay, 0) \ + MACRO(float, tonectl, 0) \ + MACRO(float, tonectl_freq, 0) \ + MACRO(float, reloffset, 0) \ + MACRO(float, xfin_lokey, 0) \ + MACRO(float, xfin_hikey, 0) \ + MACRO(float, xfout_lokey, 127) \ + MACRO(float, xfout_hikey, 127) \ + MACRO##_enum(sampler_xf_curve, xf_keycurve, stxc_power) \ + MACRO(float, xfin_lovel, 0) \ + MACRO(float, xfin_hivel, 0) \ + MACRO(float, xfout_lovel, 127) \ + MACRO(float, xfout_hivel, 127) \ + MACRO##_enum(sampler_xf_curve, xf_velcurve, stxc_power) \ + MACRO##_ccrange(xfin_cc, xfin_) \ + MACRO##_ccrange(xfout_cc, xfout_) \ + MACRO##_enum(sampler_xf_curve, xf_cccurve, stxc_power) \ + MACRO##_dahdsr(amp_env, ampeg, 0) \ + MACRO##_dahdsr(filter_env, fileg, 1) \ + MACRO##_dahdsr(pitch_env, pitcheg, 2) \ + MACRO##_lfo(amp_lfo, amplfo, 0) \ + MACRO##_lfo(filter_lfo, fillfo, 1) \ + MACRO##_lfo(pitch_lfo, pitchlfo, 2) \ + MACRO##_eq(eq1, eq1, 0) \ + MACRO##_eq(eq2, eq2, 1) \ + MACRO##_eq(eq3, eq3, 2) \ + MACRO##_ccrange(on_cc, on_) \ + MACRO##_ccrange(cc, ) \ + MACRO##_midicurve(amp_velcurve) \ + +// XXXKF: consider making send1gain the dBamp type... except it's +// a linear percentage value in SFZ spec - bit weird! + +#define DAHDSR_FIELDS(MACRO, ...) \ + MACRO(start, 0, 0, ## __VA_ARGS__) \ + MACRO(delay, 1, 0, ## __VA_ARGS__) \ + MACRO(attack, 2, 0, ## __VA_ARGS__) \ + MACRO(hold, 3, 0, ## __VA_ARGS__) \ + MACRO(decay, 4, 0, ## __VA_ARGS__) \ + MACRO(sustain, 5, 100, ## __VA_ARGS__) \ + MACRO(release, 6, 0.05, ## __VA_ARGS__) \ + +#define LFO_FIELDS(MACRO, ...) \ + MACRO(freq, 0, 0, ## __VA_ARGS__) \ + MACRO(delay, 1, 0, ## __VA_ARGS__) \ + MACRO(fade, 2, 0, ## __VA_ARGS__) \ + MACRO(wave, 3, 1, ## __VA_ARGS__) \ + +#define EQ_FIELDS(MACRO, ...) \ + MACRO(freq, 0, 0, ## __VA_ARGS__) \ + MACRO(bw, 1, 1, ## __VA_ARGS__) \ + MACRO(gain, 2, 0, ## __VA_ARGS__) \ + MACRO(vel2freq, 3, 0, ## __VA_ARGS__) \ + MACRO(vel2gain, 5, 0, ## __VA_ARGS__) \ + +#define PROC_SUBSTRUCT_HAS_FIELD(name, index, param, def_value) \ + unsigned int name:1; +#define PROC_SUBSTRUCT_RESET_FIELD(name, index, def_value, param, dst) \ + dst->param.name = def_value; +#define PROC_SUBSTRUCT_RESET_HAS_FIELD(name, index, def_value, param, dst) \ + dst->has_##param.name = 0; +#define PROC_SUBSTRUCT_CLONE(name, index, def_value, param, dst, src) \ + dst->param.name = src->param.name; \ + dst->has_##param.name = src->has_##param.name; + +struct sampler_dahdsr_has_fields +{ + DAHDSR_FIELDS(PROC_SUBSTRUCT_HAS_FIELD, name) +}; + +struct sampler_lfo_has_fields +{ + LFO_FIELDS(PROC_SUBSTRUCT_HAS_FIELD, name) +}; + +struct sampler_eq_has_fields +{ + EQ_FIELDS(PROC_SUBSTRUCT_HAS_FIELD, name) +}; + +struct sampler_midi_curve +{ + float values[128]; + uint8_t has_values[128]; +}; + +#define PROC_FIELDS_TO_STRUCT(type, name, def_value) \ + type name; +#define PROC_FIELDS_TO_STRUCT_string(name) \ + gchar *name; \ + gboolean name##_changed; +#define PROC_FIELDS_TO_STRUCT_dBamp(type, name, def_value) \ + type name; \ + type name##_linearized; +#define PROC_FIELDS_TO_STRUCT_enum(enumtype, name, def_value) \ + enum enumtype name; +#define PROC_FIELDS_TO_STRUCT_dahdsr(name, parname, index) \ + struct cbox_dahdsr name; \ + struct cbox_envelope_shape name##_shape; +#define PROC_FIELDS_TO_STRUCT_lfo(name, parname, index) \ + struct sampler_lfo_params name; +#define PROC_FIELDS_TO_STRUCT_eq(name, parname, index) \ + struct sampler_eq_params name; +#define PROC_FIELDS_TO_STRUCT_ccrange(name, parname) \ + struct sampler_cc_range *name; +#define PROC_FIELDS_TO_STRUCT_midicurve(name) \ + struct sampler_midi_curve name; + +#define PROC_HAS_FIELD(type, name, def_value) \ + unsigned int has_##name:1; +#define PROC_HAS_FIELD_string(name) \ + unsigned int has_##name:1; +#define PROC_HAS_FIELD_dBamp(type, name, def_value) \ + PROC_HAS_FIELD(type, name, def_value) +#define PROC_HAS_FIELD_enum(enumtype, name, def_value) \ + PROC_HAS_FIELD(type, name, def_value) + +#define PROC_HAS_FIELD_dahdsr(name, parname, index) \ + struct sampler_dahdsr_has_fields has_##name; +#define PROC_HAS_FIELD_lfo(name, parname, index) \ + struct sampler_lfo_has_fields has_##name; +#define PROC_HAS_FIELD_eq(name, parname, index) \ + struct sampler_eq_has_fields has_##name; +#define PROC_HAS_FIELD_ccrange(name, parname) +#define PROC_HAS_FIELD_midicurve(name) + +CBOX_EXTERN_CLASS(sampler_layer) + +enum sampler_layer_mod_bitmasks +{ + slmb_ampeg_cc = 0x01, + slmb_fileg_cc = 0x02, + slmb_pitcheg_cc = 0x04, +}; + +struct sampler_layer_computed +{ + // computed values: + struct cbox_waveform *eff_waveform; + enum sampler_loop_mode eff_loop_mode; + uint32_t eff_loop_start, eff_loop_end; + float eff_freq; + gboolean eff_use_keyswitch:1; + gboolean eff_use_simple_trigger_logic:1; + gboolean eff_use_xfcc:1; + gboolean eff_use_prevoice:1; + gboolean eff_use_channel_mixer:1; + gboolean eff_use_filter_mods:1; + gboolean eff_is_silent:1; + int16_t scratch_loop[2 * MAX_INTERPOLATION_ORDER * 2]; + int16_t scratch_end[2 * MAX_INTERPOLATION_ORDER * 2]; + float resonance_scaled, resonance2_scaled; + float logcutoff, logcutoff2; + uint32_t eq_bitmask, mod_bitmask; + int eff_num_stages, eff_num_stages2; + + float eff_amp_velcurve[128]; +}; + +struct sampler_layer_data +{ + SAMPLER_FIXED_FIELDS(PROC_FIELDS_TO_STRUCT) + SAMPLER_FIXED_FIELDS(PROC_HAS_FIELD) + + struct sampler_modulation *modulations; + struct sampler_noteinitfunc *voice_nifs, *prevoice_nifs; + struct sampler_flex_lfo *flex_lfos; + + struct sampler_layer_computed computed; +}; + +struct sampler_layer +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + + struct sampler_layer_data data, *runtime; + + struct sampler_module *module; + struct sampler_program *parent_program; + struct sampler_layer *parent, *default_child; + + int current_seq_position; + + GHashTable *unknown_keys; + GHashTable *child_layers; +}; + +extern struct sampler_layer *sampler_layer_new(struct sampler_module *m, struct sampler_program *parent_program, struct sampler_layer *parent_group); +extern struct sampler_layer *sampler_layer_new_from_section(struct sampler_module *m, struct sampler_program *parent_program, struct sampler_layer *parent_group, const char *cfg_section); +extern struct sampler_layer *sampler_layer_new_clone(struct sampler_layer *layer, struct sampler_module *m, struct sampler_program *parent_program, struct sampler_layer *parent_group); +extern void sampler_layer_set_modulation(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, float amount, int flags); +extern void sampler_layer_set_modulation1(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_moddest dest, float amount, int flags); +extern void sampler_layer_add_nif(struct sampler_layer *l, SamplerNoteInitFunc notefunc_voice, SamplerNoteInitFunc2 notefunc_prevoice, int variant, float param); +extern void sampler_layer_load_overrides(struct sampler_layer *l, const char *cfg_section); +extern void sampler_layer_data_finalize(struct sampler_layer_data *l, struct sampler_layer_data *parent, struct sampler_program *p); +extern void sampler_layer_reset_switches(struct sampler_layer *l, struct sampler_module *m); +extern gboolean sampler_layer_apply_param(struct sampler_layer *l, const char *key, const char *value, GError **error); +extern gboolean sampler_layer_unapply_param(struct sampler_layer *l, const char *key, GError **error); +extern gchar *sampler_layer_to_string(struct sampler_layer *l, gboolean show_inherited); +extern void sampler_layer_dump(struct sampler_layer *l, FILE *f); +extern void sampler_layer_update(struct sampler_layer *l); + +extern void sampler_layer_data_clone(struct sampler_layer_data *dst, const struct sampler_layer_data *src, gboolean copy_hasattr); +extern void sampler_layer_data_close(struct sampler_layer_data *l); +extern void sampler_layer_data_destroy(struct sampler_layer_data *l); + +extern void sampler_nif_vel2pitch(struct sampler_noteinitfunc *nif, struct sampler_voice *v); +extern void sampler_nif_vel2offset(struct sampler_noteinitfunc *nif, struct sampler_voice *v); +extern void sampler_nif_vel2reloffset(struct sampler_noteinitfunc *nif, struct sampler_voice *v); +extern void sampler_nif_vel2env(struct sampler_noteinitfunc *nif, struct sampler_voice *v); +extern void sampler_nif_cc2offset(struct sampler_noteinitfunc *nif, struct sampler_voice *v); +extern void sampler_nif_cc2reloffset(struct sampler_noteinitfunc *nif, struct sampler_voice *v); +extern void sampler_nif_addrandom(struct sampler_noteinitfunc *nif, struct sampler_voice *v); + +extern void sampler_nif_cc2delay(struct sampler_noteinitfunc *nif, struct sampler_prevoice *pv); +extern void sampler_nif_addrandomdelay(struct sampler_noteinitfunc *nif, struct sampler_prevoice *pv); +extern void sampler_nif_syncbeats(struct sampler_noteinitfunc *nif, struct sampler_prevoice *pv); + +extern void sampler_midi_curve_init(struct sampler_midi_curve *curve); +extern void sampler_midi_curve_interpolate(const struct sampler_midi_curve *curve, float dest[128], float def_start, float def_end, gboolean is_quadratic); + +#endif diff --git a/template/calfbox/sampler_nif.c b/template/calfbox/sampler_nif.c new file mode 100644 index 0000000..b8c06c2 --- /dev/null +++ b/template/calfbox/sampler_nif.c @@ -0,0 +1,115 @@ +#include "config-api.h" +#include "dspmath.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_impl.h" + +////////////////////////////////////////////////////////////////////////// +// Note initialisation functions + +void sampler_nif_cc2delay(struct sampler_noteinitfunc *nif, struct sampler_prevoice *pv) +{ + float cc = sampler_channel_getcc_prevoice(pv->channel, pv, nif->key.variant, nif->value.curve_id, nif->value.step); + pv->delay_computed += nif->value.value * cc; +} + +void sampler_nif_addrandomdelay(struct sampler_noteinitfunc *nif, struct sampler_prevoice *pv) +{ + pv->delay_computed += nif->value.value * rand() * (1.0 / RAND_MAX); +} + +void sampler_nif_syncbeats(struct sampler_noteinitfunc *nif, struct sampler_prevoice *pv) +{ + if (nif->value.value > 0) + { + pv->sync_beats = nif->value.value; + double cur_beat = sampler_get_current_beat(pv->channel->module); + pv->sync_initial_time = cur_beat; + double cur_rel_beat = fmod(cur_beat, pv->sync_beats); + double bar_start = cur_beat - cur_rel_beat; + if (pv->layer_data->sync_offset <= cur_rel_beat) // trigger in next bar + pv->sync_trigger_time = bar_start + pv->sync_beats + pv->layer_data->sync_offset; + else // trigger in the same bar + pv->sync_trigger_time = bar_start + pv->layer_data->sync_offset; + // printf("cur_beat %f trigger %f offset %f\n", cur_beat, pv->sync_trigger_time, pv->layer_data->sync_offset); + } +} + +void sampler_nif_vel2pitch(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + v->pitch_shift += nif->value.value * v->vel * (1.0 / 127.0); +} + +void sampler_nif_vel2offset(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + v->offset += nif->value.value * v->vel * (1.0 / 127.0); +} + +void sampler_nif_cc2offset(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + v->offset += nif->value.value * sampler_channel_getcc_mod(v->channel, v, nif->key.variant, nif->value.curve_id, nif->value.step); +} + +void sampler_nif_vel2reloffset(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + v->reloffset += nif->value.value * v->vel * (1.0 / 127.0); +} + +void sampler_nif_cc2reloffset(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + v->reloffset += nif->value.value * sampler_channel_getcc_mod(v->channel, v, nif->key.variant, nif->value.curve_id, nif->value.step); +} + +void sampler_nif_addrandom(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + float rnd = rand() * 1.0 / RAND_MAX; + switch(nif->key.variant) + { + case 0: + v->gain_shift += rnd * nif->value.value; + break; + case 1: + v->cutoff_shift += rnd * nif->value.value; + break; + case 2: + v->pitch_shift += rnd * nif->value.value; // this is in cents + break; + } +} + +static void modify_env_stage_by_nif(struct sampler_noteinitfunc *nif, struct sampler_voice *v, uint32_t variant, float value) +{ + int env_type = variant >> 4; + struct cbox_envelope *env = NULL; + switch(env_type) + { + case 0: + env = &v->amp_env; + break; + case 1: + env = &v->filter_env; + break; + case 2: + env = &v->pitch_env; + break; + default: + assert(0); + } + if (env->shape != &v->vel_envs[env_type]) + { + memcpy(&v->vel_envs[env_type], env->shape, sizeof(struct cbox_envelope_shape)); + env->shape = &v->vel_envs[env_type]; + } + float param = nif->value.value * value; + if ((variant & 15) == snif_env_sustain || (variant & 15) == snif_env_start) + param *= 0.01; + cbox_envelope_modify_dahdsr(env->shape, variant & 15, param, v->channel->module->module.srate * (1.0 / CBOX_BLOCK_SIZE)); +} + +void sampler_nif_vel2env(struct sampler_noteinitfunc *nif, struct sampler_voice *v) +{ + modify_env_stage_by_nif(nif, v, nif->key.variant, v->vel * (1.0 / 127.0)); +} diff --git a/template/calfbox/sampler_prevoice.c b/template/calfbox/sampler_prevoice.c new file mode 100644 index 0000000..7089550 --- /dev/null +++ b/template/calfbox/sampler_prevoice.c @@ -0,0 +1,81 @@ +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_impl.h" + +void sampler_prevoice_start(struct sampler_prevoice *pv, struct sampler_channel *channel, struct sampler_layer_data *l, int note, int vel) +{ + pv->channel = channel; + pv->layer_data = l; + pv->note = note; + pv->vel = vel; + pv->age = 0; + pv->delay_computed = 0.f; + pv->sync_beats = -1; + pv->sync_initial_time = -1; + pv->sync_trigger_time = -1; + + for(struct sampler_noteinitfunc *nif = pv->layer_data->prevoice_nifs; nif; nif = nif->next) + nif->key.notefunc_prevoice(nif, pv); + sampler_prevoice_unlink(&channel->module->prevoices_free, pv); + sampler_prevoice_link(&channel->module->prevoices_running, pv); +} + +void sampler_prevoice_link(struct sampler_prevoice **pv, struct sampler_prevoice *v) +{ + v->prev = NULL; + v->next = *pv; + if (*pv) + (*pv)->prev = v; + *pv = v; +} + +void sampler_prevoice_unlink(struct sampler_prevoice **pv, struct sampler_prevoice *v) +{ + if (*pv == v) + *pv = v->next; + if (v->prev) + v->prev->next = v->next; + if (v->next) + v->next->prev = v->prev; + v->prev = NULL; + v->next = NULL; +} + +int sampler_prevoice_process(struct sampler_prevoice *pv, struct sampler_module *m) +{ + struct sampler_layer_data *layer_data = pv->layer_data; + if (pv->sync_beats != -1) + { + double cur_beat = sampler_get_current_beat(m); + + if (cur_beat < pv->sync_initial_time - 0.001 || cur_beat >= pv->sync_trigger_time + 1) + { + gboolean backward_jump = cur_beat < pv->sync_initial_time; + // printf("Recalc: time %f, initial %f, delta %f, trigger %f\n", cur_beat, pv->sync_initial_time, cur_beat - pv->sync_initial_time, pv->sync_trigger_time); + // Recalculate after seek/looping etc + pv->sync_initial_time = cur_beat; + double cur_rel_beat = fmod(cur_beat, pv->sync_beats); + double bar_start = cur_beat - cur_rel_beat; + if (pv->layer_data->sync_offset <= cur_rel_beat && !backward_jump) // trigger in next bar + pv->sync_trigger_time = bar_start + pv->sync_beats + pv->layer_data->sync_offset; + else // trigger in the same bar + pv->sync_trigger_time = bar_start + pv->layer_data->sync_offset; + } + if (cur_beat < pv->sync_trigger_time) + return 0; + // Let the other logic (note delay etc.) take over + pv->sync_beats = -1; + } + pv->age += CBOX_BLOCK_SIZE; + if (pv->age >= (layer_data->delay + pv->delay_computed) * m->module.srate) + return 1; + + return 0; +} + diff --git a/template/calfbox/sampler_prg.c b/template/calfbox/sampler_prg.c new file mode 100644 index 0000000..c8cc81c --- /dev/null +++ b/template/calfbox/sampler_prg.c @@ -0,0 +1,595 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "blob.h" +#include "engine.h" +#include "instr.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_prg.h" +#include "sfzloader.h" +#include "tarfile.h" + +#include + +CBOX_CLASS_DEFINITION_ROOT(sampler_program) + +// GSList *sampler_channel_get_next_layer(struct sampler_channel *c, GSList *next_layer, int note, int vel, float random, gboolean is_first, gboolean is_release) + +struct sampler_layer *sampler_rll_iterator_next(struct sampler_rll_iterator *iter) +{ +retry: + while(iter->next_layer) + { + struct sampler_layer *lr = iter->next_layer->data; + struct sampler_layer_data *l = lr->runtime; + iter->next_layer = g_slist_next(iter->next_layer); + if (!l->computed.eff_waveform) + continue; + + if (l->computed.eff_use_simple_trigger_logic) + { + if (iter->note >= l->lokey && iter->note <= l->hikey && + iter->vel >= l->lovel && iter->vel <= l->hivel) + return lr; + else + continue; + } + + if ((l->trigger == stm_first && !iter->is_first) || + (l->trigger == stm_legato && iter->is_first) || + (l->trigger == stm_release && !iter->is_release)) // sw_last keyswitches are still added to the note-on list in RLL + continue; + struct sampler_channel *c = iter->channel; + struct sampler_module *m = c->module; + if (iter->note >= l->lokey && iter->note <= l->hikey && + iter->vel >= l->lovel && iter->vel <= l->hivel && + c >= &m->channels[l->lochan - 1] && c <= &m->channels[l->hichan - 1] && + iter->random >= l->lorand && iter->random < l->hirand && + c->pitchwheel >= l->lobend && c->pitchwheel < l->hibend && + c->last_chanaft >= l->lochanaft && c->last_chanaft <= l->hichanaft && + c->last_polyaft >= l->lopolyaft && c->last_polyaft <= l->hipolyaft && + c->module->module.engine->master->tempo >= l->lobpm && c->module->module.engine->master->tempo < l->hibpm && + sampler_cc_range_is_in(l->cc, c)) + { + if (!l->computed.eff_use_keyswitch || + ((l->sw_down == -1 || (c->switchmask[l->sw_down >> 5] & (1 << (l->sw_down & 31)))) && + (l->sw_up == -1 || !(c->switchmask[l->sw_up >> 5] & (1 << (l->sw_up & 31)))) && + (l->sw_previous == -1 || l->sw_previous == c->previous_note))) + { + gboolean play = lr->current_seq_position == 1; + lr->current_seq_position++; + if (lr->current_seq_position > l->seq_length) + lr->current_seq_position = 1; + if (play) + return lr; + } + } + } + while(iter->next_keyswitch_index < iter->rll->keyswitch_group_count && + iter->next_keyswitch_index < MAX_KEYSWITCH_GROUPS) + { + struct sampler_rll *rll = iter->rll; + uint32_t ks_group = iter->next_keyswitch_index++; + + uint8_t ks_state = iter->channel->keyswitch_state[ks_group]; + if (ks_state == 255) // nothing defined for this switch state + continue; + uint8_t key_range = rll->ranges_by_key[iter->note]; + if (key_range != 255) + { + GSList **layers_by_range = iter->is_release ? rll->release_layers_by_range : rll->layers_by_range; + layers_by_range += (rll->keyswitch_groups[ks_group]->group_offset + ks_state) * rll->layers_by_range_count; + iter->next_layer = layers_by_range[key_range]; + if (iter->next_layer) + goto retry; + } + } + return NULL; +} + +void sampler_rll_iterator_init(struct sampler_rll_iterator *iter, struct sampler_rll *rll, struct sampler_channel *c, int note, int vel, float random, gboolean is_first, gboolean is_release) +{ + iter->channel = c; + iter->note = note; + iter->vel = vel; + iter->random = random; + iter->is_first = is_first; + iter->is_release = is_release; + iter->rll = rll; + iter->next_keyswitch_index = 0; + + if (note >= rll->lokey && note <= rll->hikey) + { + assert(note >= 0 && note <= 127); + GSList **layers_by_range = is_release ? rll->release_layers_by_range : rll->layers_by_range; + if (layers_by_range) + { + uint8_t key_range = rll->ranges_by_key[note]; + if (key_range != 255) + iter->next_layer = layers_by_range[key_range]; + } + else + iter->next_layer = NULL; + } + else + iter->next_layer = NULL; +} + +static gboolean return_layers(GSList *layers, const char *keyword, struct cbox_command_target *fb, GError **error) +{ + for (GSList *p = layers; p; p = g_slist_next(p)) + { + if (!cbox_execute_on(fb, NULL, keyword, "o", error, p->data)) + return FALSE; + } + return TRUE; +} + +static gboolean sampler_program_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct sampler_program *program = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (!((!program->name || cbox_execute_on(fb, NULL, "/name", "s", error, program->name)) && + cbox_execute_on(fb, NULL, "/sample_dir", "s", error, program->sample_dir) && + cbox_execute_on(fb, NULL, "/source_file", "s", error, program->source_file) && + cbox_execute_on(fb, NULL, "/program_no", "i", error, program->prog_no) && + cbox_execute_on(fb, NULL, "/in_use", "i", error, program->in_use) && + CBOX_OBJECT_DEFAULT_STATUS(program, fb, error))) + return FALSE; + return TRUE; + } + if (!strcmp(cmd->command, "/regions") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return return_layers(program->all_layers, "/region", fb, error); + } + if (!strcmp(cmd->command, "/global") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/uuid", "o", error, program->global); + } + if (!strcmp(cmd->command, "/control_inits") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (GSList *p = program->ctrl_init_list; p; p = p->next) + { + const struct sampler_ctrlinit *cin = (const struct sampler_ctrlinit *)&p->data; + if (!cbox_execute_on(fb, NULL, "/control_init", "ii", error, (int)cin->controller, (int)cin->value)) + return FALSE; + } + return TRUE; + } + if (!strcmp(cmd->command, "/control_labels") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (GSList *p = program->ctrl_label_list; p; p = p->next) + { + const struct sampler_ctrllabel *cin = (const struct sampler_ctrllabel *)&p->data; + if (!cbox_execute_on(fb, NULL, "/control_label", "is", error, (int)cin->controller, cin->label)) + return FALSE; + } + return TRUE; + } + if (!strcmp(cmd->command, "/add_control_init") && !strcmp(cmd->arg_types, "ii")) + { + sampler_program_add_controller_init(program, CBOX_ARG_I(cmd, 0), CBOX_ARG_I(cmd, 1)); + return TRUE; + } + if (!strcmp(cmd->command, "/delete_control_init") && !strcmp(cmd->arg_types, "ii")) + { + sampler_program_remove_controller_init(program, CBOX_ARG_I(cmd, 0), CBOX_ARG_I(cmd, 1)); + return TRUE; + } + if (!strcmp(cmd->command, "/new_group") && !strcmp(cmd->arg_types, "")) + { + struct sampler_layer *l = sampler_layer_new(program->module, program, program->global->default_child); + return cbox_execute_on(fb, NULL, "/uuid", "o", error, l); + } + if (!strcmp(cmd->command, "/clone_to") && !strcmp(cmd->arg_types, "si")) + { + struct cbox_instrument *instrument = (struct cbox_instrument *)CBOX_ARG_O(cmd, 0, program, cbox_instrument, error); + if (!instrument) + return FALSE; + struct cbox_module *module = instrument->module; + if (strcmp(module->engine_name, "sampler")) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot copy sampler program to module '%s' of type '%s'", module->instance_name, module->engine_name); + return FALSE; + } + struct sampler_program *prg = sampler_program_clone(program, (struct sampler_module *)module, CBOX_ARG_I(cmd, 1), error); + if (!prg) + return FALSE; + sampler_register_program((struct sampler_module *)module, prg); + return cbox_execute_on(fb, NULL, "/uuid", "o", error, prg); + } + if (!strcmp(cmd->command, "/load_file") && !strcmp(cmd->arg_types, "si")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + struct cbox_blob *blob = cbox_blob_new_from_file(program->name, program->tarfile, program->sample_dir, CBOX_ARG_S(cmd, 0), CBOX_ARG_I(cmd, 1), error); + if (!blob) + return FALSE; + return cbox_execute_on(fb, NULL, "/data", "b", error, blob); + } + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +struct sampler_program *sampler_program_new(struct sampler_module *m, int prog_no, const char *name, struct cbox_tarfile *tarfile, const char *sample_dir, GError **error) +{ + gchar *perm_sample_dir = g_strdup(sample_dir); + if (sample_dir && !perm_sample_dir) + return NULL; + + struct cbox_document *doc = CBOX_GET_DOCUMENT(&m->module); + struct sampler_program *prg = malloc(sizeof(struct sampler_program)); + if (!prg) + { + g_free(perm_sample_dir); + return NULL; + } + memset(prg, 0, sizeof(*prg)); + CBOX_OBJECT_HEADER_INIT(prg, sampler_program, doc); + cbox_command_target_init(&prg->cmd_target, sampler_program_process_cmd, prg); + + prg->module = m; + prg->prog_no = prog_no; + prg->name = g_strdup(name); + prg->tarfile = tarfile; + prg->source_file = NULL; + prg->sample_dir = perm_sample_dir; + prg->all_layers = NULL; + prg->rll = NULL; + prg->ctrl_init_list = NULL; + prg->ctrl_label_list = NULL; + prg->global = sampler_layer_new(m, prg, NULL); + prg->global->default_child = sampler_layer_new(m, prg, prg->global); + prg->global->default_child->default_child = sampler_layer_new(m, prg, prg->global->default_child); + prg->deleting = FALSE; + prg->in_use = 0; + for (int i = 0; i < MAX_MIDI_CURVES; ++i) + { + prg->curves[i] = NULL; + prg->interpolated_curves[i] = NULL; + } + CBOX_OBJECT_REGISTER(prg); + return prg; +} + +struct sampler_program *sampler_program_new_from_cfg(struct sampler_module *m, const char *cfg_section, const char *name, int pgm_id, GError **error) +{ + int i; + + char *name2 = NULL, *sfz_path = NULL, *spath = NULL, *tar_name = NULL; + const char *sfz = NULL; + struct cbox_tarfile *tarfile = NULL; + + g_clear_error(error); + tar_name = cbox_config_get_string(cfg_section, "tar"); + if (!strncmp(cfg_section, "spgm:!", 6)) + { + sfz = cfg_section + 6; + if (!strncmp(sfz, "sbtar:", 6)) + { + sfz_path = "."; + gchar *p = strchr(sfz + 6, ';'); + if (p) + { + char *tmp = g_strndup(sfz + 6, p - sfz - 6); + tarfile = cbox_tarpool_get_tarfile(app.tarpool, tmp, error); + g_free(tmp); + if (!tarfile) + return NULL; + sfz = p + 1; + name2 = p + 1; + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot load sampler program '%s' from section '%s': missing name of a file inside a tar archive", name, cfg_section); + return NULL; + } + } + else + { + name2 = strrchr(name, '/'); + if (name2) + name2++; + } + } + else + { + if (tar_name) + { + tarfile = cbox_tarpool_get_tarfile(app.tarpool, tar_name, error); + if (!tarfile) + return NULL; + } + if (!sfz && !cbox_config_has_section(cfg_section)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot load sampler program '%s' from section '%s': section not found", name, cfg_section); + return FALSE; + } + name2 = cbox_config_get_string(cfg_section, "name"); + + sfz_path = cbox_config_get_string(cfg_section, "sfz_path"); + spath = cbox_config_get_string(cfg_section, "sample_path"); + sfz = cbox_config_get_string(cfg_section, "sfz"); + if (tarfile && !sfz_path) + sfz_path = "."; + } + + if (sfz && !sfz_path && !spath && !tarfile) + { + char *lastslash = strrchr(sfz, '/'); + if (lastslash && !sfz_path && !spath) + { + char *tmp = g_strndup(sfz, lastslash - sfz); + sfz_path = cbox_config_permify(tmp); + g_free(tmp); + sfz = lastslash + 1; + } + } + + struct sampler_program *prg = sampler_program_new( + m, + pgm_id != -1 ? pgm_id : cbox_config_get_int(cfg_section, "program", 0), + name2 ? name2 : name, + tarfile, + spath ? spath : (sfz_path ? sfz_path : ""), + error + ); + if (!prg) + return NULL; + + if (sfz) + { + if (sfz_path) + prg->source_file = g_build_filename(sfz_path, sfz, NULL); + else + { + prg->source_file = g_strdup(sfz); + } + + if (sampler_module_load_program_sfz(m, prg, prg->source_file, FALSE, error)) + return prg; + CBOX_DELETE(prg); + return NULL; + } else { + prg->source_file = g_strdup_printf("config:%s", cfg_section); + } + + for (i = 0; ; i++) + { + gchar *s = g_strdup_printf("group%d", 1 + i); + const char *group_section = cbox_config_get_string(cfg_section, s); + g_free(s); + if (!group_section) + break; + + gchar *swhere = g_strdup_printf("sgroup:%s", group_section); + struct sampler_layer *g = sampler_layer_new_from_section(m, prg, prg->global->default_child, swhere); + if (!g) + g_warning("Sample layer '%s' cannot be created - skipping", group_section); + else + { + // XXXKF + // sampler_program_add_group(prg, g); + for (int j = 0; ; j++) + { + char *where = NULL; + gchar *s = g_strdup_printf("layer%d", 1 + j); + const char *layer_section = cbox_config_get_string(swhere, s); + g_free(s); + if (!layer_section) + break; + where = g_strdup_printf("slayer:%s", layer_section); + struct sampler_layer *l = sampler_layer_new_from_section(m, prg, g, where); + if (!l) + g_warning("Sample layer '%s' cannot be created - skipping", layer_section); + else + { + sampler_layer_update(l); + if (!l->data.computed.eff_waveform) + { + g_warning("Sample layer '%s' does not have a waveform - skipping", layer_section); + CBOX_DELETE((struct sampler_layer *)l); + } + else + sampler_program_add_layer(prg, l); + } + g_free(where); + } + } + g_free(swhere); + } + for (i = 0; ; i++) + { + char *where = NULL; + gchar *s = g_strdup_printf("layer%d", 1 + i); + const char *layer_section = cbox_config_get_string(cfg_section, s); + g_free(s); + if (!layer_section) + break; + where = g_strdup_printf("slayer:%s", layer_section); + + struct sampler_layer *l = sampler_layer_new_from_section(m, prg, NULL, where); + if (!l) + g_warning("Sample layer '%s' cannot be created - skipping", layer_section); + else + { + sampler_layer_update(l); + if (!l->data.computed.eff_waveform) + { + g_warning("Sample layer '%s' does not have a waveform - skipping", layer_section); + CBOX_DELETE((struct sampler_layer *)l); + } + else + sampler_program_add_layer(prg, l); + } + g_free(where); + } + prg->all_layers = g_slist_reverse(prg->all_layers); + sampler_program_update_layers(prg); + return prg; +} + +void sampler_program_add_layer(struct sampler_program *prg, struct sampler_layer *l) +{ + // Always call sampler_update_layer before sampler_program_add_layer. + assert(l->runtime); + assert(l->parent); + assert(l->parent->parent); + assert(l->parent->parent->parent); + assert(l->parent->parent->parent == prg->global); + prg->all_layers = g_slist_prepend(prg->all_layers, l); +} + +void sampler_program_delete_layer(struct sampler_program *prg, struct sampler_layer *l) +{ + prg->all_layers = g_slist_remove(prg->all_layers, l); +} + + +void sampler_program_add_controller_init(struct sampler_program *prg, uint16_t controller, uint8_t value) +{ + union sampler_ctrlinit_union u; + u.ptr = NULL; + u.cinit.controller = controller; + u.cinit.value = value; + prg->ctrl_init_list = g_slist_append(prg->ctrl_init_list, u.ptr); +} + +static void sampler_ctrl_label_destroy(gpointer value) +{ + struct sampler_ctrllabel *label = value; + free(label->label); + free(label); +} + +void sampler_program_add_controller_label(struct sampler_program *prg, uint16_t controller, gchar *text) +{ + struct sampler_ctrllabel *label = calloc(1, sizeof(struct sampler_ctrllabel)); + label->controller = controller; + label->label = text; + prg->ctrl_label_list = g_slist_append(prg->ctrl_label_list, label); +} + +void sampler_program_remove_controller_init(struct sampler_program *prg, uint16_t controller, int which) +{ + for (GSList **p = &prg->ctrl_init_list; *p; ) + { + const struct sampler_ctrlinit *cin = (const struct sampler_ctrlinit *)&(*p)->data; + if (cin->controller != controller) + { + p = &((*p)->next); + continue; + } + if (which > 0) + which--; + GSList *q = (GSList *)cbox_rt_swap_pointers(prg->module->module.rt, (void **)p, (*p)->next); + g_slist_free1(q); + if (which == 0) + break; + } +} + +static void delete_layers_recursively(struct sampler_layer *layer) +{ + GHashTableIter iter; + g_hash_table_iter_init(&iter, layer->child_layers); + GSList *dellist = NULL; + gpointer key, value; + while(g_hash_table_iter_next(&iter, &key, &value)) + dellist = g_slist_prepend(dellist, key); + GSList *liter = dellist; + while(liter) + { + struct sampler_layer *chl = liter->data; + delete_layers_recursively(chl); + liter = liter->next; + } + g_slist_free(dellist); + CBOX_DELETE(layer); +} + +void sampler_program_destroyfunc(struct cbox_objhdr *hdr_ptr) +{ + struct sampler_program *prg = CBOX_H2O(hdr_ptr); + sampler_unselect_program(prg->module, prg); + if (prg->rll) + { + sampler_rll_destroy(prg->rll); + prg->rll = NULL; + } + for (GSList *p = prg->all_layers; p; p = g_slist_next(p)) + CBOX_DELETE((struct sampler_layer *)p->data); + delete_layers_recursively(prg->global); + + for (int i = 0; i < MAX_MIDI_CURVES; ++i) + { + g_free(prg->curves[i]); + g_free(prg->interpolated_curves[i]); + } + g_free(prg->name); + g_free(prg->sample_dir); + g_free(prg->source_file); + g_slist_free(prg->all_layers); + g_slist_free(prg->ctrl_init_list); + g_slist_free_full(prg->ctrl_label_list, sampler_ctrl_label_destroy); + if (prg->tarfile) + cbox_tarpool_release_tarfile(app.tarpool, prg->tarfile); + free(prg); +} + +void sampler_program_update_layers(struct sampler_program *prg) +{ + struct sampler_module *m = prg->module; + struct sampler_rll *new_rll = sampler_rll_new_from_program(prg); + struct sampler_rll *old_rll = cbox_rt_swap_pointers(m->module.rt, (void **)&prg->rll, new_rll); + if (old_rll) + sampler_rll_destroy(old_rll); +} + +struct sampler_program *sampler_program_clone(struct sampler_program *prg, struct sampler_module *m, int prog_no, GError **error) +{ + struct sampler_program *newprg = sampler_program_new(m, prog_no, prg->name, prg->tarfile, prg->sample_dir, error); + if (!newprg) + return NULL; + if (prg->source_file) + newprg->source_file = g_strdup(prg->source_file); + // The values are stored as a union aliased with the data pointer, so no need to deep-copy + newprg->ctrl_init_list = g_slist_copy(prg->ctrl_init_list); + // XXXKF ctrl_label_list + newprg->rll = NULL; + newprg->global = sampler_layer_new_clone(prg->global, m, newprg, NULL); + sampler_program_update_layers(newprg); + if (newprg->tarfile) + newprg->tarfile->refs++; + + return newprg; +} + diff --git a/template/calfbox/sampler_prg.h b/template/calfbox/sampler_prg.h new file mode 100644 index 0000000..3fb2c64 --- /dev/null +++ b/template/calfbox/sampler_prg.h @@ -0,0 +1,120 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SAMPLER_PRG_H +#define CBOX_SAMPLER_PRG_H + +#include "cmd.h" +#include "dom.h" + +struct cbox_tarfile; +struct sampler_channel; + +CBOX_EXTERN_CLASS(sampler_program) + +#define MAX_MIDI_CURVES 32 + +struct sampler_keyswitch_group +{ + uint8_t lo, hi, num_used, def_value; + uint32_t group_offset; + uint8_t key_offsets[]; +}; + +// Runtime layer lists; in future, I might something more clever, like a tree +struct sampler_rll +{ + GSList *layers_oncc; + uint32_t cc_trigger_bitmask[4]; // one bit per CC + uint8_t lokey, hikey; + uint8_t ranges_by_key[128]; + uint32_t layers_by_range_count; + GSList **layers_by_range, **release_layers_by_range; + struct sampler_keyswitch_group **keyswitch_groups; + uint32_t keyswitch_group_count; + uint32_t keyswitch_key_count; + gboolean has_release_layers; +}; + +struct sampler_rll_iterator +{ + struct sampler_channel *channel; + int note, vel; + float random; + gboolean is_first, is_release; + GSList *next_layer; + struct sampler_rll *rll; + uint32_t next_keyswitch_index; +}; + +struct sampler_ctrlinit +{ + uint16_t controller; + uint8_t value; +}; + +union sampler_ctrlinit_union { + gpointer ptr; + struct sampler_ctrlinit cinit; +}; + +struct sampler_ctrllabel { + uint16_t controller; + gchar *label; +}; + +struct sampler_program +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + + struct sampler_module *module; + gchar *name; + int prog_no; + struct sampler_layer *global; + GSList *all_layers; + GSList *ctrl_init_list; + GSList *ctrl_label_list; + struct sampler_rll *rll; + gchar *sample_dir; // can be empty, cannot be NULL + gchar *source_file; // can be empty, cannot be NULL + int in_use; + struct cbox_tarfile *tarfile; + gboolean deleting; + struct sampler_midi_curve *curves[MAX_MIDI_CURVES]; + float *interpolated_curves[MAX_MIDI_CURVES]; +}; + +extern struct sampler_rll *sampler_rll_new_from_program(struct sampler_program *prg); +extern void sampler_rll_destroy(struct sampler_rll *rll); + +extern void sampler_rll_iterator_init(struct sampler_rll_iterator *iter, struct sampler_rll *rll, struct sampler_channel *c, int note, int vel, float random, gboolean is_first, gboolean is_release); +extern struct sampler_layer *sampler_rll_iterator_next(struct sampler_rll_iterator *iter); + +extern struct sampler_program *sampler_program_new(struct sampler_module *m, int prog_no, const char *name, struct cbox_tarfile *tarfile, const char *sample_dir, GError **error); +extern struct sampler_program *sampler_program_new_from_cfg(struct sampler_module *m, const char *cfg_section, const char *name, int pgm_id, GError **error); +extern void sampler_program_add_layer(struct sampler_program *prg, struct sampler_layer *l); +extern void sampler_program_delete_layer(struct sampler_program *prg, struct sampler_layer *l); +extern void sampler_program_add_group(struct sampler_program *prg, struct sampler_layer *l); +extern void sampler_program_add_controller_init(struct sampler_program *prg, uint16_t controller, uint8_t value); +extern void sampler_program_add_controller_label(struct sampler_program *prg, uint16_t controller, gchar *label); // keeps ownership +extern void sampler_program_remove_controller_init(struct sampler_program *prg, uint16_t controller, int which); +extern void sampler_program_update_layers(struct sampler_program *prg); +extern struct sampler_program *sampler_program_clone(struct sampler_program *prg, struct sampler_module *m, int prog_no, GError **error); + +#endif diff --git a/template/calfbox/sampler_rll.c b/template/calfbox/sampler_rll.c new file mode 100644 index 0000000..7e8198f --- /dev/null +++ b/template/calfbox/sampler_rll.c @@ -0,0 +1,170 @@ +#include "app.h" +#include "blob.h" +#include "instr.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_prg.h" + +///////////////////////////////////////////////////////////////////////////////// + +static void add_layers(struct sampler_rll *rll, GSList **layers_by_range, struct sampler_layer *l, uint32_t lokey, uint32_t hikey) +{ + if (lokey >= 0 && lokey <= 127 && + hikey >= 0 && hikey <= 127) + { + int start = rll->ranges_by_key[lokey]; + int end = rll->ranges_by_key[hikey]; + for (int i = start; i <= end; ++i) + { + if (!layers_by_range[i] || layers_by_range[i]->data != l) + layers_by_range[i] = g_slist_prepend(layers_by_range[i], l); + } + } +} + +struct sampler_rll *sampler_rll_new_from_program(struct sampler_program *prg) +{ + struct sampler_rll *rll = g_new(struct sampler_rll, 1); + rll->layers_oncc = NULL; + for (int i = 0; i < 4; i++) + rll->cc_trigger_bitmask[i] = 0; + + GHashTable *keyswitch_groups = g_hash_table_new(g_direct_hash, g_direct_equal); + uint32_t keyswitch_group_count = 0, keyswitch_key_count = 0; + GPtrArray *keyswitch_group_array = g_ptr_array_new(); + memset(rll->ranges_by_key, 255, sizeof(rll->ranges_by_key)); + rll->has_release_layers = FALSE; + for (GSList *p = prg->all_layers; p; p = g_slist_next(p)) + { + struct sampler_layer *l = p->data; + if (l->data.trigger == stm_release) + rll->has_release_layers = TRUE; + if (l->data.sw_last >= 0 && l->data.sw_last <= 127 && + l->data.sw_lokey >= 0 && l->data.sw_lokey <= 127 && + l->data.sw_hikey >= 0 && l->data.sw_hikey <= 127 && + l->data.sw_last >= l->data.sw_lokey && l->data.sw_last <= l->data.sw_hikey) + { + int width = l->data.sw_hikey - l->data.sw_lokey + 1; + gpointer key = GINT_TO_POINTER(l->data.sw_lokey + (l->data.sw_hikey << 8)); + uint8_t value = l->data.sw_last - l->data.sw_lokey; + struct sampler_keyswitch_group *ks = g_hash_table_lookup(keyswitch_groups, key); + if (!ks) + { + ks = g_malloc(sizeof(struct sampler_keyswitch_group) + width); + ks->lo = l->data.sw_lokey; + ks->hi = l->data.sw_hikey; + ks->num_used = 0; + ks->def_value = 255; + memset(ks->key_offsets, 255, width); + + g_hash_table_insert(keyswitch_groups, (gpointer)key, ks); + g_ptr_array_add(keyswitch_group_array, ks); + keyswitch_group_count++; + } + if (l->data.sw_default >= ks->lo && l->data.sw_default <= ks->hi && ks->def_value == 255) + ks->def_value = l->data.sw_default - ks->lo; + if (ks->key_offsets[value] == 255) + { + ks->key_offsets[value] = ks->num_used; + ks->num_used++; + keyswitch_key_count++; + assert(ks->num_used <= width); + } + } + } + rll->keyswitch_groups = (gpointer)g_ptr_array_free(keyswitch_group_array, FALSE); + rll->keyswitch_group_count = keyswitch_group_count; + rll->keyswitch_key_count = keyswitch_key_count; + uint32_t offset = 0; + for (uint32_t i = 0; i < keyswitch_group_count; ++i) + { + rll->keyswitch_groups[i]->group_offset = 1 + offset; + offset += rll->keyswitch_groups[i]->num_used; + } + assert(offset == keyswitch_key_count); + + uint16_t lo_count[129], hi_count[128], low = 127, high = 0; + for (int i = 0; i < 128; i++) + lo_count[i] = hi_count[i] = 0; + + // XXXKF handle 'key' field without relying on the existing ugly hack + for (GSList *p = prg->all_layers; p; p = g_slist_next(p)) + { + struct sampler_layer *l = p->data; + if (l->data.lokey >= 0 && l->data.lokey <= 127 && + l->data.hikey >= 0 && l->data.hikey <= 127) + { + lo_count[l->data.lokey]++; + hi_count[l->data.hikey]++; + if (l->data.lokey < low) + low = l->data.lokey; + if (l->data.hikey > high) + high = l->data.hikey; + } + } + rll->lokey = low; + rll->hikey = high; + uint32_t range_count = 1; + for (int i = low + 1; i <= high; ++i) + { + rll->ranges_by_key[i - 1] = range_count - 1; + if (hi_count[i - 1] || lo_count[i]) + range_count++; + } + rll->ranges_by_key[high] = range_count - 1; + rll->layers_by_range = g_malloc0_n(range_count * (1 + keyswitch_key_count), sizeof(GSList *)); + rll->release_layers_by_range = rll->has_release_layers ? g_malloc0_n(range_count * (1 + keyswitch_key_count), sizeof(GSList *)) : NULL; + rll->layers_by_range_count = range_count; + for (GSList *p = prg->all_layers; p; p = g_slist_next(p)) + { + struct sampler_layer *l = p->data; + uint32_t ks_offset = 0; + if (l->data.sw_last >= 0 && l->data.sw_last <= 127 && + l->data.sw_lokey >= 0 && l->data.sw_lokey <= 127 && + l->data.sw_hikey >= 0 && l->data.sw_hikey <= 127 && + l->data.sw_last >= l->data.sw_lokey && l->data.sw_last <= l->data.sw_hikey) + { + gpointer key = GINT_TO_POINTER(l->data.sw_lokey + (l->data.sw_hikey << 8)); + struct sampler_keyswitch_group *ks = g_hash_table_lookup(keyswitch_groups, key); + assert(ks); + int rel_offset = ks->key_offsets[l->data.sw_last - l->data.sw_lokey]; + assert(rel_offset != -1); + ks_offset = ks->group_offset + rel_offset; + } + + struct sampler_cc_range *oncc = l->data.on_cc; + if (oncc) + { + rll->layers_oncc = g_slist_prepend(rll->layers_oncc, l); + while(oncc) + { + int cc = oncc->key.cc_number; + rll->cc_trigger_bitmask[cc >> 5] |= 1 << (cc & 31); + oncc = oncc->next; + } + } + if (l->data.trigger == stm_release) + add_layers(rll, rll->release_layers_by_range + ks_offset * range_count, l, l->data.lokey, l->data.hikey); + else + add_layers(rll, rll->layers_by_range + ks_offset * range_count, l, l->data.lokey, l->data.hikey); + } + g_hash_table_unref(keyswitch_groups); + return rll; +} + +void sampler_rll_destroy(struct sampler_rll *rll) +{ + g_slist_free(rll->layers_oncc); + for (uint32_t i = 0; i < rll->layers_by_range_count * (1 + rll->keyswitch_key_count); ++i) + { + if (rll->has_release_layers) + g_slist_free(rll->release_layers_by_range[i]); + g_slist_free(rll->layers_by_range[i]); + } + for (uint32_t i = 0; i < rll->keyswitch_group_count; ++i) + g_free(rll->keyswitch_groups[i]); + g_free(rll->keyswitch_groups); + g_free(rll->release_layers_by_range); + g_free(rll->layers_by_range); + g_free(rll); +} diff --git a/template/calfbox/sampler_voice.c b/template/calfbox/sampler_voice.c new file mode 100644 index 0000000..b31f8a5 --- /dev/null +++ b/template/calfbox/sampler_voice.c @@ -0,0 +1,995 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "errors.h" +#include "midi.h" +#include "module.h" +#include "rt.h" +#include "sampler.h" +#include "sampler_impl.h" +#include "sfzloader.h" +#include "stm.h" +#include +#include +#include +#include +#include +#include +#include +#include + + +static void lfo_update_freq(struct sampler_lfo *lfo, struct sampler_lfo_params *lfop, int srate, double srate_inv) +{ + lfo->delta = (uint32_t)(lfop->freq * 65536.0 * 65536.0 * CBOX_BLOCK_SIZE * srate_inv); + lfo->delay = (uint32_t)(lfop->delay * srate); + lfo->fade = (uint32_t)(lfop->fade * srate); + lfo->wave = (int32_t)(lfop->wave); +} + +static void lfo_init(struct sampler_lfo *lfo, struct sampler_lfo_params *lfop, int srate, double srate_inv) +{ + lfo->phase = 0; + lfo->xdelta = 0; + lfo->age = 0; + lfo->random_value = 0; // safe, less CPU intensive value + lfo_update_freq(lfo, lfop, srate, srate_inv); +} + +static inline float lfo_run(struct sampler_lfo *lfo) +{ + if (lfo->age < lfo->delay) + { + lfo->age += CBOX_BLOCK_SIZE; + return 0.f; + } + uint32_t delta = lfo->delta + lfo->xdelta; + + const int FRAC_BITS = 32 - 11; + + float v = 0; + switch(lfo->wave) { + case 0: // triangle + { + uint32_t ph = lfo->phase + 0x40000000; + uint32_t tri = (ph & 0x7FFFFFFF) ^ (((ph >> 31) - 1) & 0x7FFFFFFF); + v = -1.0 + tri * (2.0 / (1U << 31)); + break; + } + case 1: // sine (default) + default: + { + uint32_t iphase = lfo->phase >> FRAC_BITS; + float frac = (lfo->phase & ((1 << FRAC_BITS) - 1)) * (1.0 / (1 << FRAC_BITS)); + v = sampler_sine_wave[iphase] + (sampler_sine_wave[iphase + 1] - sampler_sine_wave[iphase]) * frac; + break; + } + case 2: + v = lfo->phase < 0xC0000000 ? 1 : -1; + break; + case 3: + v = lfo->phase < 0x80000000 ? 1 : -1; + break; + case 4: + v = lfo->phase < 0x40000000 ? 1 : -1; + break; + case 5: + v = lfo->phase < 0x20000000 ? 1 : -1; + break; + case 6: + v = -1 + lfo->phase * (1.0 / (1U << 31)); + break; + case 7: + v = 1 - lfo->phase * (1.0 / (1U << 31)); + break; + case 12: + if ((lfo->phase & 0x80000000) != ((lfo->phase + delta) & 0x80000000)) + lfo->random_value = -1 + 2 * rand() / (1.0 * RAND_MAX); + v = lfo->random_value; + break; + } + lfo->phase += delta; + if (lfo->fade && lfo->age < lfo->delay + lfo->fade) + { + v *= (lfo->age - lfo->delay) * 1.0 / lfo->fade; + lfo->age += CBOX_BLOCK_SIZE; + } + + return v; +} + +static gboolean is_tail_finished(struct sampler_voice *v) +{ + if (!v->layer->computed.eff_num_stages) + return TRUE; + double eps = 1.0 / 65536.0; + if (cbox_biquadf_is_audible(&v->filter.filter_left[0], eps)) + return FALSE; + if (cbox_biquadf_is_audible(&v->filter.filter_right[0], eps)) + return FALSE; + int num_stages = v->layer->computed.eff_num_stages; + if (num_stages > 1) + { + if (cbox_biquadf_is_audible(&v->filter.filter_left[num_stages - 1], eps)) + return FALSE; + if (cbox_biquadf_is_audible(&v->filter.filter_right[num_stages - 1], eps)) + return FALSE; + } + + return TRUE; +} + +#if USE_NEON + +#include + +static inline void mix_block_into_with_gain(cbox_sample_t **outputs, int oofs, float *src_leftright, float gain) +{ + float *dst_left = outputs[oofs]; + float *dst_right = outputs[oofs + 1]; + float32x2_t gain2 = {gain, gain}; + for (size_t i = 0; i < CBOX_BLOCK_SIZE; i += 2) + { + float32x2_t lr1 = vld1_f32(&src_leftright[2 * i]); + float32x2_t lr2 = vld1_f32(&src_leftright[2 * i + 2]); + float32x2x2_t lr12 = vtrn_f32(lr1, lr2); + float32x2_t dl1 = vld1_f32(&dst_left[i]); + float32x2_t dr1 = vld1_f32(&dst_right[i]); + + float32x2_t l1 = vmla_f32(dl1, lr12.val[0], gain2); + vst1_f32(&dst_left[i], l1); + float32x2_t r1 = vmla_f32(dr1, lr12.val[1], gain2); + vst1_f32(&dst_right[i], r1); + } +} + +static inline void mix_block_into(cbox_sample_t **outputs, int oofs, float *src_leftright) +{ + float *dst_left = outputs[oofs]; + float *dst_right = outputs[oofs + 1]; + for (size_t i = 0; i < CBOX_BLOCK_SIZE; i += 2) + { + float32x2_t lr1 = vld1_f32(&src_leftright[2 * i]); + float32x2_t lr2 = vld1_f32(&src_leftright[2 * i + 2]); + float32x2x2_t lr12 = vtrn_f32(lr1, lr2); + float32x2_t dl1 = vld1_f32(&dst_left[i]); + float32x2_t dr1 = vld1_f32(&dst_right[i]); + + float32x2_t l1 = vadd_f32(dl1, lr12.val[0]); + vst1_f32(&dst_left[i], l1); + float32x2_t r1 = vadd_f32(dr1, lr12.val[1]); + vst1_f32(&dst_right[i], r1); + } +} + +#else + +static inline void mix_block_into_with_gain(cbox_sample_t **outputs, int oofs, float *src_leftright, float gain) +{ + cbox_sample_t *dst_left = outputs[oofs]; + cbox_sample_t *dst_right = outputs[oofs + 1]; + for (size_t i = 0; i < CBOX_BLOCK_SIZE; i++) + { + dst_left[i] += gain * src_leftright[2 * i]; + dst_right[i] += gain * src_leftright[2 * i + 1]; + } +} + +static inline void mix_block_into(cbox_sample_t **outputs, int oofs, float *src_leftright) +{ + cbox_sample_t *dst_left = outputs[oofs]; + cbox_sample_t *dst_right = outputs[oofs + 1]; + for (size_t i = 0; i < CBOX_BLOCK_SIZE; i++) + { + dst_left[i] += src_leftright[2 * i]; + dst_right[i] += src_leftright[2 * i + 1]; + } +} + +#endif + +//////////////////////////////////////////////////////////////////////////////// + +static float sfz_crossfade(float param, float xfin_lo, float xfin_hi, float xfout_lo, float xfout_hi, enum sampler_xf_curve xfc) +{ + if (param >= xfin_hi && param <= xfout_lo) + return 1.f; + if (param < xfin_lo || param > xfout_hi) + return 0.f; + float for0 = (param < xfout_lo) ? xfin_lo : xfout_hi; + float for1 = (param < xfout_lo) ? xfin_hi : xfout_lo; + if (for0 == for1) + return 1.f; + if (xfc == stxc_gain) + return (param - for0) / (for1 - for0); + else + { + float v = (param - for0) / (for1 - for0); + return sqrtf(v); + } +} + +// One-half version for CC-based crossfades +static inline float sfz_crossfade2(float param, float xflo, float xfhi, float left, float right, enum sampler_xf_curve xfc) +{ + if (xflo > xfhi) + return sfz_crossfade2(param, xfhi, xflo, right, left, xfc); + if (param <= xflo) + return left; + if (param >= xfhi) + return right; + float res; + if (xflo == xfhi) + res = 0.5f * (left + right); + else + res = left + (right - left) * (param - xflo) / (xfhi - xflo); + if (xfc == stxc_gain) + return res; + else + return sqrtf(res); +} + +//////////////////////////////////////////////////////////////////////////////// + +void sampler_voice_activate(struct sampler_voice *v, enum sampler_player_type mode) +{ + assert(v->gen.mode == spt_inactive); + sampler_voice_unlink(&v->program->module->voices_free, v); + assert(mode != spt_inactive); + assert(v->channel); + v->gen.mode = mode; + sampler_voice_link(&v->channel->voices_running, v); +} + +void sampler_voice_start_silent(struct sampler_layer_data *l, struct sampler_released_groups *exgroupdata) +{ + if (l->group >= 1) + sampler_released_groups_add(exgroupdata, l->group); +} + +void sampler_voice_start(struct sampler_voice *v, struct sampler_channel *c, struct sampler_layer_data *l, int note, int vel, struct sampler_released_groups *exgroupdata) +{ + struct sampler_module *m = c->module; + sampler_gen_reset(&v->gen); + + v->age = 0; + if (l->trigger == stm_release) + { + // time since last 'note on' for that note + v->age = m->current_time - c->prev_note_start_time[note]; + double age = v->age * m->module.srate_inv; + // if attenuation is more than 84dB, ignore the release trigger + if (age * l->rt_decay > 84) + return; + } + uint32_t end = l->computed.eff_waveform->info.frames; + if (l->end != 0) + end = (l->end == SAMPLER_NO_LOOP) ? 0 : l->end; + v->last_waveform = l->computed.eff_waveform; + v->gen.cur_sample_end = end; + if (end > l->computed.eff_waveform->info.frames) + end = l->computed.eff_waveform->info.frames; + + assert(!v->current_pipe); + if (end > l->computed.eff_waveform->preloaded_frames) + { + if (l->computed.eff_loop_mode == slm_loop_continuous && l->computed.eff_loop_end < l->computed.eff_waveform->preloaded_frames) + { + // Everything fits in prefetch, because loop ends in prefetch and post-loop part is not being played + } + else + { + uint32_t loop_start = -1, loop_end = end; + // If in loop mode, set the loop over the looped part... unless we're doing sustain-only loop on prefetch area only. Then + // streaming will only cover the release part, and it shouldn't be looped. + if (l->computed.eff_loop_mode == slm_loop_continuous || (l->computed.eff_loop_mode == slm_loop_sustain && l->computed.eff_loop_end >= l->computed.eff_waveform->preloaded_frames)) + { + loop_start = l->computed.eff_loop_start; + loop_end = l->computed.eff_loop_end; + } + // Those are initial values only, they will be adjusted in process function + v->current_pipe = cbox_prefetch_stack_pop(m->pipe_stack, l->computed.eff_waveform, loop_start, loop_end, l->count); + if (!v->current_pipe) + { + g_warning("Prefetch pipe pool exhausted, no streaming playback will be possible"); + end = l->computed.eff_waveform->preloaded_frames; + v->gen.cur_sample_end = end; + } + } + } + + v->output_pair_no = (l->output + c->output_shift) % m->output_pairs; + v->serial_no = m->serial_no; + + v->gen.loop_overlap = l->loop_overlap; + v->gen.loop_overlap_step = l->loop_overlap > 0 ? 1.0 / l->loop_overlap : 0; + v->gain_fromvel = l->computed.eff_amp_velcurve[vel]; + v->gain_shift = (note - l->amp_keycenter) * l->amp_keytrack; + + v->gain_fromvel *= sfz_crossfade(note, l->xfin_lokey, l->xfin_hikey, l->xfout_lokey, l->xfout_hikey, l->xf_keycurve); + v->gain_fromvel *= sfz_crossfade(vel, l->xfin_lovel, l->xfin_hivel, l->xfout_lovel, l->xfout_hivel, l->xf_velcurve); + + v->note = note; + v->vel = vel; + v->off_vel = 0; + v->pitch_shift = 0; + v->released = 0; + v->released_with_sustain = 0; + v->released_with_sostenuto = 0; + v->captured_sostenuto = 0; + v->channel = c; + v->layer = l; + v->program = c->program; + v->amp_env.shape = &l->amp_env_shape; + v->filter_env.shape = &l->filter_env_shape; + v->pitch_env.shape = &l->pitch_env_shape; + + v->cutoff_shift = vel * l->fil_veltrack / 127.0 + (note - l->fil_keycenter) * l->fil_keytrack; + v->cutoff2_shift = vel * l->fil2_veltrack / 127.0 + (note - l->fil2_keycenter) * l->fil2_keytrack; + v->loop_mode = l->computed.eff_loop_mode; + v->off_by = l->off_by; + v->reloffset = l->reloffset; + int auxes = (m->module.outputs - m->module.aux_offset) / 2; + if (l->effect1bus >= 1 && l->effect1bus < 1 + auxes) + v->send1bus = l->effect1bus; + else + v->send1bus = 0; + if (l->effect2bus >= 1 && l->effect2bus < 1 + auxes) + v->send2bus = l->effect2bus; + else + v->send2bus = 0; + v->send1gain = l->effect1 * 0.01; + v->send2gain = l->effect2 * 0.01; + if (l->group >= 1) + { + sampler_released_groups_add(exgroupdata, l->group); + } + lfo_init(&v->amp_lfo, &l->amp_lfo, m->module.srate, m->module.srate_inv); + lfo_init(&v->filter_lfo, &l->filter_lfo, m->module.srate, m->module.srate_inv); + lfo_init(&v->pitch_lfo, &l->pitch_lfo, m->module.srate, m->module.srate_inv); + + for (int i = 0; i < 3; ++i) + { + cbox_biquadf_reset(&v->filter.filter_left[i]); + cbox_biquadf_reset(&v->filter.filter_right[i]); + cbox_biquadf_reset(&v->filter2.filter_left[i]); + cbox_biquadf_reset(&v->filter2.filter_right[i]); + } + cbox_onepolef_reset(&v->onepole_left); + cbox_onepolef_reset(&v->onepole_right); + // set gain later (it's a less expensive operation) + if (l->tonectl_freq != 0) + cbox_onepolef_set_highshelf_tonectl(&v->onepole_coeffs, l->tonectl_freq * M_PI * m->module.srate_inv, 1.0); + + v->offset = l->offset; + for(struct sampler_noteinitfunc *nif = v->layer->voice_nifs; nif; nif = nif->next) + nif->key.notefunc_voice(nif, v); + if (v->gain_shift) + v->gain_fromvel *= dB2gain(v->gain_shift); + + if (v->offset + v->reloffset != 0) + { + // For streamed samples, allow only half the preload period worth of offset to avoid gaps + // (maybe we can allow up to preload period minus one buffer size here?) + uint32_t maxend = v->current_pipe ? (l->computed.eff_waveform->preloaded_frames >> 1) : l->computed.eff_waveform->preloaded_frames; + int32_t pos = v->offset + v->reloffset * maxend * 0.01; + if (pos < 0) + pos = 0; + if ((uint32_t)pos > maxend) + pos = (int32_t)maxend; + v->offset = pos; + } + + cbox_envelope_reset(&v->amp_env); + cbox_envelope_reset(&v->filter_env); + cbox_envelope_reset(&v->pitch_env); + + v->last_eq_bitmask = 0; + + sampler_voice_activate(v, l->computed.eff_waveform->info.channels == 2 ? spt_stereo16 : spt_mono16); + + uint32_t pos = v->offset; + if (l->offset_random) + pos += ((uint32_t)(rand() + (rand() << 16))) % l->offset_random; + if (pos >= end) + pos = end; + v->gen.bigpos = ((uint64_t)pos) << 32; + v->gen.virtpos = ((uint64_t)pos) << 32; + + if (v->current_pipe && v->gen.bigpos) + cbox_prefetch_pipe_consumed(v->current_pipe, v->gen.bigpos >> 32); + v->layer_changed = TRUE; +} + +void sampler_voice_link(struct sampler_voice **pv, struct sampler_voice *v) +{ + v->prev = NULL; + v->next = *pv; + if (*pv) + (*pv)->prev = v; + *pv = v; +} + +void sampler_voice_unlink(struct sampler_voice **pv, struct sampler_voice *v) +{ + if (*pv == v) + *pv = v->next; + if (v->prev) + v->prev->next = v->next; + if (v->next) + v->next->prev = v->prev; + v->prev = NULL; + v->next = NULL; +} + +void sampler_voice_inactivate(struct sampler_voice *v, gboolean expect_active) +{ + assert((v->gen.mode != spt_inactive) == expect_active); + sampler_voice_unlink(&v->channel->voices_running, v); + v->gen.mode = spt_inactive; + if (v->current_pipe) + { + cbox_prefetch_stack_push(v->program->module->pipe_stack, v->current_pipe); + v->current_pipe = NULL; + } + v->channel = NULL; + sampler_voice_link(&v->program->module->voices_free, v); +} + +void sampler_voice_release(struct sampler_voice *v, gboolean is_polyaft) +{ + if ((v->loop_mode == slm_one_shot_chokeable) != is_polyaft) + return; + if (v->loop_mode != slm_one_shot && !v->layer->count) + { + v->released = 1; + if (v->loop_mode == slm_loop_sustain && v->current_pipe) + { + // Break the loop + v->current_pipe->file_loop_end = v->gen.cur_sample_end; + v->current_pipe->file_loop_start = -1; + } + } +} + +void sampler_voice_update_params_from_layer(struct sampler_voice *v) +{ + struct sampler_layer_data *l = v->layer; + struct sampler_module *m = v->program->module; + lfo_update_freq(&v->amp_lfo, &l->amp_lfo, m->module.srate, m->module.srate_inv); + lfo_update_freq(&v->filter_lfo, &l->filter_lfo, m->module.srate, m->module.srate_inv); + lfo_update_freq(&v->pitch_lfo, &l->pitch_lfo, m->module.srate, m->module.srate_inv); + cbox_envelope_update_shape(&v->amp_env, &l->amp_env_shape); + cbox_envelope_update_shape(&v->filter_env, &l->filter_env_shape); + cbox_envelope_update_shape(&v->pitch_env, &l->pitch_env_shape); +} + +static inline void lfo_update_xdelta(struct sampler_module *m, struct sampler_lfo *lfo, uint32_t modmask, uint32_t dest, const float *moddests) +{ + if (!(modmask & (1 << dest))) + lfo->xdelta = 0; + else + lfo->xdelta = (uint32_t)(moddests[dest] * 65536.0 * 65536.0 * CBOX_BLOCK_SIZE * m->module.srate_inv); +} + +static inline void sampler_filter_process_control(struct sampler_filter *f, enum sampler_filter_type fil_type, float logcutoff, float resonance_linearized, const struct cbox_sincos *sincos_base) +{ + f->second_filter = &f->filter_coeffs; + + if (logcutoff < 0) + logcutoff = 0; + if (logcutoff > 12798) + logcutoff = 12798; + //float resonance = v->resonance*pow(32.0,c->cc[71]/maxv); + float resonance = resonance_linearized; + if (resonance < 0.7f) + resonance = 0.7f; + if (resonance > 32.f) + resonance = 32.f; + const struct cbox_sincos *sincos = &sincos_base[(int)logcutoff]; + switch(fil_type) + { + case sft_lp24hybrid: + cbox_biquadf_set_lp_rbj_lookup(&f->filter_coeffs, sincos, resonance * resonance); + cbox_biquadf_set_1plp_lookup(&f->filter_coeffs_extra, sincos, 1); + f->second_filter = &f->filter_coeffs_extra; + break; + case sft_lp12: + case sft_lp24: + case sft_lp36: + cbox_biquadf_set_lp_rbj_lookup(&f->filter_coeffs, sincos, resonance); + break; + case sft_hp12: + case sft_hp24: + cbox_biquadf_set_hp_rbj_lookup(&f->filter_coeffs, sincos, resonance); + break; + case sft_bp6: + case sft_bp12: + cbox_biquadf_set_bp_rbj_lookup(&f->filter_coeffs, sincos, resonance); + break; + case sft_lp6: + case sft_lp12nr: + case sft_lp24nr: + cbox_biquadf_set_1plp_lookup(&f->filter_coeffs, sincos, fil_type != sft_lp6); + break; + case sft_hp6: + case sft_hp12nr: + case sft_hp24nr: + cbox_biquadf_set_1php_lookup(&f->filter_coeffs, sincos, fil_type != sft_hp6); + break; + default: + assert(0); + } +} + +static inline void sampler_filter_process_audio(struct sampler_filter *f, int num_stages, float *leftright) +{ + for (int i = 0; i < num_stages; ++i) + cbox_biquadf_process_stereo(&f->filter_left[i], &f->filter_right[i], i ? f->second_filter : &f->filter_coeffs, leftright); +} + +static inline void do_channel_mixing(float *leftright, uint32_t numsamples, float position, float width) +{ + float crossmix = (100.0 - width) * 0.005f; + float amtleft = position > 0 ? 1 - 0.01 * position : 1; + float amtright = position < 0 ? 1 - 0.01 * -position : 1; + if (amtleft < 0) + amtleft = 0; + if (amtright < 0) + amtright = 0; + for (uint32_t i = 0; i < 2 * numsamples; i += 2) { + float left = leftright[i], right = leftright[i + 1]; + float newleft = left + crossmix * (right - left); + float newright = right + crossmix * (left - right); + leftright[i] = newleft * amtleft; + leftright[i + 1] = newright * amtright; + } +} + +static inline uint32_t sampler_gen_sample_playback_with_pipe(struct sampler_gen *gen, float *leftright, struct cbox_prefetch_pipe *current_pipe) +{ + if (!current_pipe) + return sampler_gen_sample_playback(gen, leftright, (uint32_t)-1); + + uint32_t limit = cbox_prefetch_pipe_get_remaining(current_pipe); + if (limit <= 4) + { + gen->mode = spt_inactive; + return 0; + } + uint32_t samples = sampler_gen_sample_playback(gen, leftright, limit - 4); + cbox_prefetch_pipe_consumed(current_pipe, gen->consumed); + gen->consumed = 0; + return samples; +} + +static const float gain_for_num_stages[] = { 1, 1, 0.5, 0.33f }; + +void sampler_voice_process(struct sampler_voice *v, struct sampler_module *m, cbox_sample_t **outputs) +{ + struct sampler_layer_data *l = v->layer; + assert(v->gen.mode != spt_inactive); + + struct sampler_channel *c = v->channel; + v->age += CBOX_BLOCK_SIZE; + + const float velscl = v->vel * (1.f / 127.f); + + struct cbox_envelope_shape *pitcheg_shape = v->pitch_env.shape, *fileg_shape = v->filter_env.shape, *ampeg_shape = v->amp_env.shape; + if (__builtin_expect(l->computed.mod_bitmask, 0)) + { + #define COPY_ORIG_SHAPE(envtype, envtype2, index) \ + if (l->computed.mod_bitmask & slmb_##envtype2##eg_cc) { \ + memcpy(&v->cc_envs[index], envtype2##eg_shape, sizeof(struct cbox_envelope_shape)); \ + envtype2##eg_shape = &v->cc_envs[index]; \ + } + COPY_ORIG_SHAPE(amp, amp, 0) + COPY_ORIG_SHAPE(filter, fil, 1) + COPY_ORIG_SHAPE(pitch, pitch, 2) + + for(struct sampler_modulation *sm = l->modulations; sm; sm = sm->next) + { + // Simplified modulations for EG stages (CCs only) + if (sm->key.dest >= smdest_eg_stage_start && sm->key.dest <= smdest_eg_stage_end) + { + float value = 0.f; + if (sm->key.src < smsrc_pernote_offset) + value = sampler_channel_getcc_mod(c, v, sm->key.src, sm->value.curve_id, sm->value.step); + uint32_t param = sm->key.dest - smdest_eg_stage_start; + if (value * sm->value.amount != 0) + cbox_envelope_modify_dahdsr(&v->cc_envs[(param >> 4)], param & 0x0F, value * sm->value.amount, m->module.srate * 1.0 / CBOX_BLOCK_SIZE); + } + } + #define UPDATE_ENV_POSITION(envtype, envtype2) \ + if (l->computed.mod_bitmask & slmb_##envtype##eg_cc) \ + cbox_envelope_update_shape_after_modify(&v->envtype2##_env, envtype##eg_shape, m->module.srate * 1.0 / CBOX_BLOCK_SIZE); + UPDATE_ENV_POSITION(amp, amp) + UPDATE_ENV_POSITION(fil, filter) + UPDATE_ENV_POSITION(pitch, pitch) + } + + // if it's a DAHD envelope without sustain, consider the note finished + if (__builtin_expect(v->amp_env.cur_stage == 4 && ampeg_shape->stages[3].end_value <= 0.f, 0)) + cbox_envelope_go_to(&v->amp_env, 15); + + #define RECALC_EQ_MASK_EQ1 (7 << smdest_eq1_freq) + #define RECALC_EQ_MASK_EQ2 (7 << smdest_eq2_freq) + #define RECALC_EQ_MASK_EQ3 (7 << smdest_eq3_freq) + #define RECALC_EQ_MASK_ALL (RECALC_EQ_MASK_EQ1 | RECALC_EQ_MASK_EQ2 | RECALC_EQ_MASK_EQ3) + uint32_t recalc_eq_mask = 0; + + if (__builtin_expect(v->layer_changed, 0)) + { + v->last_level = -1; + if (v->last_waveform != v->layer->computed.eff_waveform) + { + v->last_waveform = v->layer->computed.eff_waveform; + if (v->layer->computed.eff_waveform) + { + v->gen.mode = v->layer->computed.eff_waveform->info.channels == 2 ? spt_stereo16 : spt_mono16; + v->gen.cur_sample_end = v->layer->computed.eff_waveform->info.frames; + } + else + { + sampler_voice_inactivate(v, TRUE); + return; + } + } + if (l->computed.eq_bitmask & (1 << 0)) recalc_eq_mask |= RECALC_EQ_MASK_EQ1; + if (l->computed.eq_bitmask & (1 << 1)) recalc_eq_mask |= RECALC_EQ_MASK_EQ2; + if (l->computed.eq_bitmask & (1 << 2)) recalc_eq_mask |= RECALC_EQ_MASK_EQ3; + v->last_eq_bitmask = l->computed.eq_bitmask; + v->layer_changed = FALSE; + } + + float pitch = (v->note - l->pitch_keycenter) * l->pitch_keytrack + l->tune + l->transpose * 100 + v->pitch_shift; + float modsrcs[smsrc_pernote_count]; + modsrcs[smsrc_vel - smsrc_pernote_offset] = v->vel * velscl; + modsrcs[smsrc_pitch - smsrc_pernote_offset] = pitch * (1.f / 100.f); + modsrcs[smsrc_chanaft - smsrc_pernote_offset] = c->last_chanaft * (1.f / 127.f); + modsrcs[smsrc_polyaft - smsrc_pernote_offset] = sampler_channel_get_poly_pressure(c, v->note); + modsrcs[smsrc_pitchenv - smsrc_pernote_offset] = cbox_envelope_get_value(&v->pitch_env, pitcheg_shape) * 0.01f; + modsrcs[smsrc_filenv - smsrc_pernote_offset] = l->computed.eff_use_filter_mods ? cbox_envelope_get_value(&v->filter_env, fileg_shape) * 0.01f : 0; + modsrcs[smsrc_ampenv - smsrc_pernote_offset] = cbox_envelope_get_value(&v->amp_env, ampeg_shape) * 0.01f; + + modsrcs[smsrc_amplfo - smsrc_pernote_offset] = lfo_run(&v->amp_lfo); + modsrcs[smsrc_fillfo - smsrc_pernote_offset] = l->computed.eff_use_filter_mods ? lfo_run(&v->filter_lfo) : 0; + modsrcs[smsrc_pitchlfo - smsrc_pernote_offset] = lfo_run(&v->pitch_lfo); + + float moddests[smdestcount]; + moddests[smdest_pitch] = pitch; + moddests[smdest_cutoff] = v->cutoff_shift; + moddests[smdest_cutoff2] = v->cutoff2_shift; + // These are always set + uint32_t modmask = (1 << smdest_pitch) | (1 << smdest_cutoff) | (1 << smdest_cutoff2); +#if 0 + // Those are lazy-initialized using modmask. + moddests[smdest_gain] = 0; + moddests[smdest_resonance] = 0; + moddests[smdest_tonectl] = 0; + moddests[smdest_pitchlfo_freq] = 0; + moddests[smdest_fillfo_freq] = 0; + moddests[smdest_amplfo_freq] = 0; +#endif + if (__builtin_expect(l->trigger == stm_release, 0)) + { + moddests[smdest_gain] = -v->age * l->rt_decay * m->module.srate_inv; + modmask |= (1 << smdest_gain); + } + + if (c->pitchwheel) + { + int pw = c->pitchwheel * (c->pitchwheel > 0 ? l->bend_up : -l->bend_down); + // approximate dividing by 8191 + if (pw < 0) + pw >>= 13; + else + pw = (pw + 4096) >> 13; + if (l->bend_step > 1) + pw = (pw / l->bend_step) * l->bend_step; + moddests[smdest_pitch] += pw; + } + + for (struct sampler_modulation *sm = l->modulations; sm; sm = sm->next) + { + enum sampler_modsrc src = sm->key.src; + enum sampler_modsrc src2 = sm->key.src2; + enum sampler_moddest dest = sm->key.dest; + float value = 0.f, value2 = 1.f; + if (src < smsrc_pernote_offset) + value = sampler_channel_getcc_mod(c, v, src, sm->value.curve_id, sm->value.step); + else + value = modsrcs[src - smsrc_pernote_offset]; + + if (src2 != smsrc_none) + { + if (src2 < smsrc_pernote_offset) + value2 = sampler_channel_getcc_mod(c, v, src2, sm->value.curve_id, sm->value.step); + else + value2 = modsrcs[src2 - smsrc_pernote_offset]; + + value *= value2; + } + if (dest < 32) + { + if (dest == smdest_amplitude) + { + if (!(modmask & (1 << dest))) // first value + { + moddests[dest] = value * sm->value.amount; + modmask |= (1 << dest); + } + else + moddests[dest] *= value * sm->value.amount; + } + else if (!(modmask & (1 << dest))) // first value + { + moddests[dest] = value * sm->value.amount; + modmask |= (1 << dest); + } + else + moddests[dest] += value * sm->value.amount; + } + } + lfo_update_xdelta(m, &v->pitch_lfo, modmask, smdest_pitchlfo_freq, moddests); + if (l->computed.eff_use_filter_mods) + lfo_update_xdelta(m, &v->filter_lfo, modmask, smdest_fillfo_freq, moddests); + lfo_update_xdelta(m, &v->amp_lfo, modmask, smdest_amplfo_freq, moddests); + recalc_eq_mask |= modmask; + + #define RECALC_EQ_IF(index) \ + if (recalc_eq_mask & RECALC_EQ_MASK_EQ##index) \ + { \ + float dfreq = velscl * l->eq##index.vel2freq + ((modmask & (1 << smdest_eq##index##_freq)) ? moddests[smdest_eq##index##_freq] : 0);\ + float fbw = (modmask & (1 << smdest_eq##index##_bw)) ? pow(0.5, moddests[smdest_eq##index##_bw]) : 1;\ + float dgain = velscl * l->eq##index.vel2gain + ((modmask & (1 << smdest_eq##index##_gain)) ? moddests[smdest_eq##index##_gain] : 0);\ + cbox_biquadf_set_peakeq_rbj_scaled(&v->eq_coeffs[index - 1], l->eq##index.effective_freq + dfreq, fbw / l->eq##index.bw, dB2gain(0.5 * (l->eq##index.gain + dgain)), m->module.srate); \ + if (!(v->last_eq_bitmask & (1 << (index - 1)))) \ + { \ + cbox_biquadf_reset(&v->eq_left[index-1]); \ + cbox_biquadf_reset(&v->eq_right[index-1]); \ + } \ + } + if (__builtin_expect(recalc_eq_mask, 0)) + { + RECALC_EQ_IF(1) + RECALC_EQ_IF(2) + RECALC_EQ_IF(3) + } + cbox_envelope_advance(&v->pitch_env, v->released, pitcheg_shape); + if (l->computed.eff_use_filter_mods) + cbox_envelope_advance(&v->filter_env, v->released, fileg_shape); + cbox_envelope_advance(&v->amp_env, v->released, ampeg_shape); + if (__builtin_expect(v->amp_env.cur_stage < 0, 0)) + { + if (__builtin_expect(is_tail_finished(v), 0)) + { + sampler_voice_inactivate(v, TRUE); + return; + } + } + + double maxv = 127 << 7; + double freq = l->computed.eff_freq * cent2factor(moddests[smdest_pitch]) ; + uint64_t freq64 = (uint64_t)(freq * 65536.0 * 65536.0 * m->module.srate_inv); + + gboolean playing_sustain_loop = !v->released && v->loop_mode == slm_loop_sustain; + uint32_t loop_start, loop_end; + gboolean bandlimited = FALSE; + + if (!v->current_pipe) + { + v->gen.sample_data = v->last_waveform->data; + if (v->last_waveform->levels) + { + gboolean use_cached = v->last_level > 0 && v->last_level < v->last_waveform->level_count + && freq64 > v->last_level_min_rate && freq64 <= v->last_waveform->levels[v->last_level].max_rate; + if (__builtin_expect(use_cached, 1)) + { + v->gen.sample_data = v->last_waveform->levels[v->last_level].data; + bandlimited = TRUE; + } + else + { + for (int i = 0; i < v->last_waveform->level_count; i++) + { + if (freq64 <= v->last_waveform->levels[i].max_rate) + { + v->last_level = i; + v->gen.sample_data = v->last_waveform->levels[i].data; + bandlimited = TRUE; + + break; + } + v->last_level_min_rate = v->last_waveform->levels[i].max_rate; + } + } + } + } + + // XXXKF or maybe check for on-cc being in the on-cc range instead? + gboolean play_loop = v->layer->computed.eff_loop_end && (v->loop_mode == slm_loop_continuous || playing_sustain_loop) && !v->layer->on_cc; + loop_start = play_loop ? v->layer->computed.eff_loop_start : (v->layer->count ? 0 : (uint32_t)-1); + loop_end = play_loop ? v->layer->computed.eff_loop_end : v->gen.cur_sample_end; + + if (v->current_pipe) + { + v->gen.sample_data = v->gen.loop_count ? v->current_pipe->data : v->last_waveform->data; + v->gen.streaming_buffer = v->current_pipe->data; + + v->gen.prefetch_only_loop = (loop_end < v->last_waveform->preloaded_frames); + v->gen.loop_overlap = 0; + if (v->gen.prefetch_only_loop) + { + assert(!v->gen.in_streaming_buffer); // XXXKF this won't hold true when loops are edited while sound is being played (but that's not supported yet anyway) + v->gen.loop_start = loop_start; + v->gen.loop_end = loop_end; + v->gen.streaming_buffer_frames = 0; + } + else + { + v->gen.loop_start = 0; + v->gen.loop_end = v->last_waveform->preloaded_frames; + v->gen.streaming_buffer_frames = v->current_pipe->buffer_loop_end; + } + } + else + { + v->gen.loop_count = v->layer->count; + v->gen.loop_start = loop_start; + v->gen.loop_end = loop_end; + + if (!bandlimited) + { + // Use pre-calculated join + v->gen.scratch = loop_start == (uint32_t)-1 ? v->layer->computed.scratch_end : v->layer->computed.scratch_loop; + } + else + { + // The standard waveforms have extra MAX_INTERPOLATION_ORDER of samples from the loop start added past loop_end, + // to avoid wasting time generating the joins in all the practical cases. The slow path covers custom loops + // (i.e. partial loop or no loop) over bandlimited versions of the standard waveforms, and those are probably + // not very useful anyway, as changing the loop removes the guarantee of the waveform being bandlimited and + // may cause looping artifacts or introduce DC offset (e.g. if only a positive part of a sine wave is looped). + if (loop_start == 0 && loop_end == l->computed.eff_waveform->info.frames) + v->gen.scratch = v->gen.sample_data + l->computed.eff_waveform->info.frames - MAX_INTERPOLATION_ORDER; + else + { + // Generate the join for the current wave level + // XXXKF this could be optimised further, by checking if waveform and loops are the same as the last + // time. However, this code is not likely to be used... ever, so optimising it is not the priority. + int shift = l->computed.eff_waveform->info.channels == 2 ? 1 : 0; + uint32_t halfscratch = MAX_INTERPOLATION_ORDER << shift; + + v->gen.scratch = v->gen.scratch_bandlimited; + memcpy(&v->gen.scratch_bandlimited[0], &v->gen.sample_data[(loop_end - MAX_INTERPOLATION_ORDER) << shift], halfscratch * sizeof(int16_t) ); + if (loop_start != (uint32_t)-1) + memcpy(v->gen.scratch_bandlimited + halfscratch, &v->gen.sample_data[loop_start << shift], halfscratch * sizeof(int16_t)); + else + memset(v->gen.scratch_bandlimited + halfscratch, 0, halfscratch * sizeof(int16_t)); + } + } + } + + if (l->timestretch) + { + v->gen.bigdelta = freq64; + v->gen.virtdelta = (uint64_t)(l->computed.eff_freq * 65536.0 * 65536.0 * m->module.srate_inv); + v->gen.stretching_jump = l->timestretch_jump; + v->gen.stretching_crossfade = l->timestretch_crossfade; + } + else + { + v->gen.bigdelta = freq64; + v->gen.virtdelta = freq64; + } + float gain = modsrcs[smsrc_ampenv - smsrc_pernote_offset] * l->volume_linearized * v->gain_fromvel * c->channel_volume_cc * sampler_channel_addcc(c, 11) / (maxv * maxv); + if (l->computed.eff_use_xfcc) { + for(struct sampler_cc_range *p = l->xfin_cc; p; p = p->next) + gain *= sfz_crossfade2(c->intcc[p->key.cc_number], p->value.locc, p->value.hicc, 0, 1, l->xf_cccurve); + for(struct sampler_cc_range *p = l->xfout_cc; p; p = p->next) + gain *= sfz_crossfade2(c->intcc[p->key.cc_number], p->value.locc, p->value.hicc, 1, 0, l->xf_cccurve); + } + if ((modmask & (1 << smdest_gain)) && moddests[smdest_gain] != 0.f) + gain *= dB2gain(moddests[smdest_gain]); + + float amplitude = l->amplitude; + if ((modmask & (1 << smdest_amplitude))) + amplitude *= moddests[smdest_amplitude]; + + gain *= amplitude * (1.0 / 100.0); + // http://drealm.info/sfz/plj-sfz.xhtml#amp "The overall gain must remain in the range -144 to 6 decibels." + if (gain > 2.f) + gain = 2.f; + float pan = (l->pan + ((modmask & (1 << smdest_pan) ? moddests[smdest_pan] : 0)) + 100.f) * (1.f / 200.f) + (c->channel_pan_cc * 1.f / maxv - 0.5f) * 2.f; + if (pan < 0.f) + pan = 0.f; + if (pan > 1.f) + pan = 1.f; + v->gen.lgain = gain * (1.f - pan) / 32768.f; + v->gen.rgain = gain * pan / 32768.f; + + if (l->cutoff != -1) + { + float mod_resonance = (modmask & (1 << smdest_resonance)) ? dB2gain(gain_for_num_stages[l->computed.eff_num_stages] * moddests[smdest_resonance]) : 1; + sampler_filter_process_control(&v->filter, l->fil_type, l->computed.logcutoff + moddests[smdest_cutoff], l->computed.resonance_scaled * mod_resonance, m->sincos); + } + if (l->cutoff2 != -1) + { + float mod_resonance = (modmask & (1 << smdest_resonance2)) ? dB2gain(gain_for_num_stages[l->computed.eff_num_stages2] * moddests[smdest_resonance2]) : 1; + sampler_filter_process_control(&v->filter2, l->fil2_type, l->computed.logcutoff2 + moddests[smdest_cutoff2], l->computed.resonance2_scaled * mod_resonance, m->sincos); + } + + if (__builtin_expect(l->tonectl_freq != 0, 0)) + { + float ctl = l->tonectl + (modmask & (1 << smdest_tonectl) ? moddests[smdest_tonectl] : 0); + if (fabs(ctl) > 0.0001f) + cbox_onepolef_set_highshelf_setgain(&v->onepole_coeffs, dB2gain(ctl)); + else + cbox_onepolef_set_highshelf_setgain(&v->onepole_coeffs, 1.0); + } + + // Audio processing starts here + float leftright[2 * CBOX_BLOCK_SIZE]; + + uint32_t samples = sampler_gen_sample_playback_with_pipe(&v->gen, leftright, v->current_pipe); + if (l->computed.eff_use_channel_mixer) + do_channel_mixing(leftright, samples, l->position, l->width); + for (int i = 2 * samples; i < 2 * CBOX_BLOCK_SIZE; i++) + leftright[i] = 0.f; + + if (l->cutoff != -1) + sampler_filter_process_audio(&v->filter, l->computed.eff_num_stages, leftright); + if (l->cutoff2 != -1) + sampler_filter_process_audio(&v->filter2, l->computed.eff_num_stages2, leftright); + + if (__builtin_expect(l->tonectl_freq != 0, 0)) + cbox_onepolef_process_stereo(&v->onepole_left, &v->onepole_right, &v->onepole_coeffs, leftright); + + if (__builtin_expect(l->computed.eq_bitmask, 0)) + { + for (int eq = 0; eq < 3; eq++) + { + if (l->computed.eq_bitmask & (1 << eq)) + { + cbox_biquadf_process_stereo(&v->eq_left[eq], &v->eq_right[eq], &v->eq_coeffs[eq], leftright); + } + } + } + + mix_block_into(outputs, v->output_pair_no * 2, leftright); + if (__builtin_expect((v->send1bus > 0 && v->send1gain != 0) || (v->send2bus > 0 && v->send2gain != 0), 0)) + { + if (v->send1bus > 0 && v->send1gain != 0) + { + int oofs = m->module.aux_offset + (v->send1bus - 1) * 2; + mix_block_into_with_gain(outputs, oofs, leftright, v->send1gain); + } + if (v->send2bus > 0 && v->send2gain != 0) + { + int oofs = m->module.aux_offset + (v->send2bus - 1) * 2; + mix_block_into_with_gain(outputs, oofs, leftright, v->send2gain); + } + } + if (v->gen.mode == spt_inactive) + sampler_voice_inactivate(v, FALSE); +} + diff --git a/template/calfbox/scene.c b/template/calfbox/scene.c new file mode 100644 index 0000000..f29ed0e --- /dev/null +++ b/template/calfbox/scene.c @@ -0,0 +1,1238 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "auxbus.h" +#include "config-api.h" +#include "engine.h" +#include "errors.h" +#include "instr.h" +#include "io.h" +#include "layer.h" +#include "master.h" +#include "midi.h" +#include "module.h" +#include "pattern.h" +#include "rt.h" +#include "scene.h" +#include "seq.h" +#include +#include + +CBOX_CLASS_DEFINITION_ROOT(cbox_scene) + +static gboolean cbox_scene_addlayercmd(struct cbox_scene *s, struct cbox_command_target *fb, struct cbox_osc_command *cmd, int cmd_type, GError **error) +{ + int pos = CBOX_ARG_I(cmd, 0); + if (pos < 0 || pos > (int)(1 + s->layer_count)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d or 0 for append)", pos, 1 + s->layer_count); + return FALSE; + } + if (pos == 0) + pos = s->layer_count; + else + pos--; + struct cbox_layer *layer = NULL; + + switch(cmd_type) + { + case 1: + layer = cbox_layer_new_from_config(s, CBOX_ARG_S(cmd, 1), error); + break; + case 2: + layer = cbox_layer_new_with_instrument(s, CBOX_ARG_S(cmd, 1), error); + break; + case 3: + { + struct cbox_instrument *instr = cbox_scene_create_instrument(s, CBOX_ARG_S(cmd, 1), CBOX_ARG_S(cmd, 2), error); + if (!instr) + return FALSE; + layer = cbox_layer_new_with_instrument(s, CBOX_ARG_S(cmd, 1), error); + break; + } + case 4: + layer = cbox_layer_new(s); + if (cbox_uuid_fromstring(&layer->external_output, CBOX_ARG_S(cmd, 1), error)) + { + layer->external_output_set = TRUE; + } + else + { + CBOX_DELETE(layer); + return FALSE; + } + break; + default: + assert(0); + break; + } + if (!layer) + return FALSE; + if (!cbox_scene_insert_layer(s, layer, pos, error)) + { + CBOX_DELETE(layer); + return FALSE; + } + if (fb) + { + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, layer)) + return FALSE; + } + return TRUE; +} + +static gboolean cbox_scene_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_scene *s = ct->user_data; + const char *subcommand = NULL; + char *subobj = NULL; + int index = 0; + + if (!strcmp(cmd->command, "/transpose") && !strcmp(cmd->arg_types, "i")) + { + s->transpose = CBOX_ARG_I(cmd, 0); + return TRUE; + } + else if (!strcmp(cmd->command, "/load") && !strcmp(cmd->arg_types, "s")) + { + if (!cbox_scene_load(s, CBOX_ARG_S(cmd, 0), error)) + return FALSE; + return TRUE; + } + else if (!strcmp(cmd->command, "/clear") && !strcmp(cmd->arg_types, "")) + { + cbox_scene_clear(s); + return TRUE; + } + else if (!strcmp(cmd->command, "/add_layer") && !strcmp(cmd->arg_types, "is")) + { + return cbox_scene_addlayercmd(s, fb, cmd, 1, error); + } + else if (!strcmp(cmd->command, "/add_instrument_layer") && !strcmp(cmd->arg_types, "is")) + { + return cbox_scene_addlayercmd(s, fb, cmd, 2, error); + } + else if (!strcmp(cmd->command, "/add_new_instrument_layer") && !strcmp(cmd->arg_types, "iss")) + { + return cbox_scene_addlayercmd(s, fb, cmd, 3, error); + } + else if (!strcmp(cmd->command, "/add_midi_layer") && !strcmp(cmd->arg_types, "is")) + { + return cbox_scene_addlayercmd(s, fb, cmd, 4, error); + } + else if (!strcmp(cmd->command, "/delete_layer") && !strcmp(cmd->arg_types, "i")) + { + int pos = CBOX_ARG_I(cmd, 0); + if (pos < 0 || pos > (int)s->layer_count) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d or 0 for last)", pos, s->layer_count); + return FALSE; + } + if (pos == 0) + pos = s->layer_count - 1; + else + pos--; + struct cbox_layer *layer = cbox_scene_remove_layer(s, pos); + CBOX_DELETE(layer); + return TRUE; + } + else if (!strcmp(cmd->command, "/move_layer") && !strcmp(cmd->arg_types, "ii")) + { + int oldpos = CBOX_ARG_I(cmd, 0); + if (oldpos < 1 || oldpos > (int)s->layer_count) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d)", oldpos, s->layer_count); + return FALSE; + } + int newpos = CBOX_ARG_I(cmd, 1); + if (newpos < 1 || newpos > (int)s->layer_count) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid position %d (valid are 1..%d)", newpos, s->layer_count); + return FALSE; + } + cbox_scene_move_layer(s, oldpos - 1, newpos - 1); + return TRUE; + } + else if (cbox_parse_path_part_int(cmd, "/layer/", &subcommand, &index, 1, s->layer_count, error)) + { + if (!subcommand) + return FALSE; + return cbox_execute_sub(&s->layers[index - 1]->cmd_target, fb, cmd, subcommand, error); + } + else if (cbox_parse_path_part_str(cmd, "/aux/", &subcommand, &subobj, error)) + { + if (!subcommand) + return FALSE; + struct cbox_aux_bus *aux = cbox_scene_get_aux_bus(s, subobj, FALSE, error); + g_free(subobj); + if (!aux) + return FALSE; + return cbox_execute_sub(&aux->cmd_target, fb, cmd, subcommand, error); + } + else if (!strncmp(cmd->command, "/instr/", 7)) + { + const char *obj = &cmd->command[1]; + const char *pos = strchr(obj, '/'); + obj = &pos[1]; + pos = strchr(obj, '/'); + if (!pos) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid instrument path '%s'", cmd->command); + return FALSE; + } + int len = pos - obj; + + gchar *name = g_strndup(obj, len); + struct cbox_instrument *instr = cbox_scene_get_instrument_by_name(s, name, FALSE, error); + if (instr) + { + g_free(name); + + return cbox_execute_sub(&instr->cmd_target, fb, cmd, pos, error); + } + else + { + cbox_force_error(error); + g_prefix_error(error, "Cannot access instrument '%s': ", name); + g_free(name); + return FALSE; + } + return TRUE; + } + else if (!strcmp(cmd->command, "/load_aux") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_aux_bus *bus = cbox_scene_get_aux_bus(s, CBOX_ARG_S(cmd, 0), TRUE, error); + if (!bus) + return FALSE; + if (fb) + { + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, bus)) + return FALSE; + } + return TRUE; + } + else if (!strcmp(cmd->command, "/delete_aux") && !strcmp(cmd->arg_types, "s")) + { + const char *name = CBOX_ARG_S(cmd, 0); + struct cbox_aux_bus *aux = cbox_scene_get_aux_bus(s, name, FALSE, error); + if (!aux) + return FALSE; + CBOX_DELETE(aux); + return TRUE; + } + else if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (!cbox_execute_on(fb, NULL, "/name", "s", error, s->name) || + !cbox_execute_on(fb, NULL, "/title", "s", error, s->title) || + !cbox_execute_on(fb, NULL, "/transpose", "i", error, s->transpose) || + !cbox_execute_on(fb, NULL, "/enable_default_song_input", "i", error, s->enable_default_song_input) || + !cbox_execute_on(fb, NULL, "/enable_default_external_input", "i", error, s->enable_default_external_input) || + !CBOX_OBJECT_DEFAULT_STATUS(s, fb, error)) + return FALSE; + + for (uint32_t i = 0; i < s->layer_count; i++) + { + if (!cbox_execute_on(fb, NULL, "/layer", "o", error, s->layers[i])) + return FALSE; + } + for (uint32_t i = 0; i < s->instrument_count; i++) + { + if (!cbox_execute_on(fb, NULL, "/instrument", "sso", error, s->instruments[i]->module->instance_name, s->instruments[i]->module->engine_name, s->instruments[i])) + return FALSE; + } + for (uint32_t i = 0; i < s->aux_bus_count; i++) + { + if (!cbox_execute_on(fb, NULL, "/aux", "so", error, s->aux_buses[i]->name, s->aux_buses[i])) + return FALSE; + } + return TRUE; + } + else + if (!strcmp(cmd->command, "/send_event") && (!strcmp(cmd->arg_types, "iii") || !strcmp(cmd->arg_types, "ii") || !strcmp(cmd->arg_types, "i"))) + { + int mcmd = CBOX_ARG_I(cmd, 0); + int arg1 = 0, arg2 = 0; + if (cmd->arg_types[1] == 'i') + { + arg1 = CBOX_ARG_I(cmd, 1); + if (cmd->arg_types[2] == 'i') + arg2 = CBOX_ARG_I(cmd, 2); + } + struct cbox_midi_buffer buf; + cbox_midi_buffer_init(&buf); + cbox_midi_buffer_write_inline(&buf, 0, mcmd, arg1, arg2); + cbox_midi_merger_push(&s->scene_input_merger, &buf, s->rt); + return TRUE; + } + else + if (!strcmp(cmd->command, "/play_note") && !strcmp(cmd->arg_types, "iii")) + { + int channel = CBOX_ARG_I(cmd, 0); + int note = CBOX_ARG_I(cmd, 1); + int velocity = CBOX_ARG_I(cmd, 2); + struct cbox_midi_buffer buf; + cbox_midi_buffer_init(&buf); + cbox_midi_buffer_write_inline(&buf, 0, 0x90 + ((channel - 1) & 15), note & 127, velocity & 127); + cbox_midi_buffer_write_inline(&buf, 1, 0x80 + ((channel - 1) & 15), note & 127, velocity & 127); + cbox_midi_merger_push(&s->scene_input_merger, &buf, s->rt); + return TRUE; + } + else + if (!strcmp(cmd->command, "/play_pattern") && !strcmp(cmd->arg_types, "sfi")) + { + struct cbox_midi_pattern *pattern = (struct cbox_midi_pattern *)CBOX_ARG_O(cmd, 0, s, cbox_midi_pattern, error); + if (!pattern) + return FALSE; + + struct cbox_adhoc_pattern *ap = cbox_adhoc_pattern_new(s->engine, CBOX_ARG_I(cmd, 2), pattern); + ap->master->tempo = ap->master->new_tempo = CBOX_ARG_F(cmd, 1); + cbox_scene_play_adhoc_pattern(s, ap); + return TRUE; + } + else + if (!strcmp(cmd->command, "/enable_default_song_input") && !strcmp(cmd->arg_types, "i")) + { + s->enable_default_song_input = CBOX_ARG_I(cmd, 0); + cbox_scene_update_connected_inputs(s); + return TRUE; + } + else + if (!strcmp(cmd->command, "/enable_default_external_input") && !strcmp(cmd->arg_types, "i")) + { + s->enable_default_external_input = CBOX_ARG_I(cmd, 0); + cbox_scene_update_connected_inputs(s); + return TRUE; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +gboolean cbox_scene_load(struct cbox_scene *s, const char *name, GError **error) +{ + const char *cv = NULL; + int i; + gchar *section = g_strdup_printf("scene:%s", name); + + if (!cbox_config_has_section(section)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No config section for scene '%s'", name); + goto error; + } + + cbox_scene_clear(s); + + assert(s->layers == NULL); + assert(s->instruments == NULL); + assert(s->aux_buses == NULL); + assert(s->layer_count == 0); + assert(s->instrument_count == 0); + assert(s->aux_bus_count == 0); + + for (i = 1; ; i++) + { + struct cbox_layer *l = NULL; + + gchar *sn = g_strdup_printf("layer%d", i); + cv = cbox_config_get_string(section, sn); + g_free(sn); + + if (!cv) + break; + + l = cbox_layer_new_from_config(s, cv, error); + if (!l) + goto error; + + if (!cbox_scene_add_layer(s, l, error)) + goto error; + } + + s->transpose = cbox_config_get_int(section, "transpose", 0); + s->title = g_strdup(cbox_config_get_string_with_default(section, "title", "")); + g_free(section); + cbox_command_target_init(&s->cmd_target, cbox_scene_process_cmd, s); + s->name = g_strdup(name); + return TRUE; + +error: + g_free(section); + return FALSE; +} + +gboolean cbox_scene_insert_layer(struct cbox_scene *scene, struct cbox_layer *layer, int pos, GError **error) +{ + uint32_t i; + + struct cbox_instrument *instrument = layer->instrument; + + if (instrument) { + for (i = 0; i < instrument->aux_output_count; i++) + { + assert(!instrument->aux_outputs[i]); + if (instrument->aux_output_names[i]) + { + instrument->aux_outputs[i] = cbox_scene_get_aux_bus(scene, instrument->aux_output_names[i], TRUE, error); + if (!instrument->aux_outputs[i]) + return FALSE; + cbox_aux_bus_ref(instrument->aux_outputs[i]); + } + } + for (i = 0; i < scene->layer_count; i++) + { + if (scene->layers[i]->instrument == layer->instrument) + break; + } + if (i == scene->layer_count) + { + layer->instrument->scene = scene; + cbox_rt_array_insert(scene->rt, (void ***)&scene->instruments, &scene->instrument_count, -1, layer->instrument); + } + } + cbox_rt_array_insert(scene->rt, (void ***)&scene->layers, &scene->layer_count, pos, layer); + if (layer->external_output_set && scene->rt) + cbox_scene_update_connected_outputs(scene); + + return TRUE; +} + +gboolean cbox_scene_add_layer(struct cbox_scene *scene, struct cbox_layer *layer, GError **error) +{ + return cbox_scene_insert_layer(scene, layer, scene->layer_count, error); +} + +struct cbox_layer *cbox_scene_remove_layer(struct cbox_scene *scene, int pos) +{ + struct cbox_layer *removed = scene->layers[pos]; + cbox_rt_array_remove(scene->rt, (void ***)&scene->layers, &scene->layer_count, pos); + if (removed->instrument) + cbox_instrument_unref_aux_buses(removed->instrument); + if (removed->external_output_set) + cbox_scene_update_connected_outputs(scene); + + return removed; +} + +void cbox_scene_move_layer(struct cbox_scene *scene, unsigned int oldpos, unsigned int newpos) +{ + if (oldpos == newpos) + return; + struct cbox_layer **layers = malloc(sizeof(struct cbox_layer *) * scene->layer_count); + for (uint32_t i = 0; i < scene->layer_count; i++) + { + int s; + if (i == newpos) + s = oldpos; + else + { + if (oldpos < newpos) + s = (i < oldpos || i > newpos) ? i : i + 1; + else + s = (i < newpos || i > oldpos) ? i : i - 1; + } + layers[i] = scene->layers[s]; + } + free(cbox_rt_swap_pointers(scene->rt, (void **)&scene->layers, layers)); +} + +gboolean cbox_scene_remove_instrument(struct cbox_scene *scene, struct cbox_instrument *instrument) +{ + assert(instrument->scene == scene); + for (uint32_t pos = 0; pos < scene->instrument_count; pos++) + { + if (scene->instruments[pos] == instrument) + { + cbox_rt_array_remove(scene->rt, (void ***)&scene->instruments, &scene->instrument_count, pos); + g_hash_table_remove(scene->instrument_hash, instrument->module->instance_name); + instrument->scene = NULL; + return TRUE; + } + } + return FALSE; +} + +gboolean cbox_scene_insert_aux_bus(struct cbox_scene *scene, struct cbox_aux_bus *aux_bus) +{ + struct cbox_aux_bus **aux_buses = malloc(sizeof(struct cbox_aux_bus *) * (scene->aux_bus_count + 1)); + memcpy(aux_buses, scene->aux_buses, sizeof(struct cbox_aux_bus *) * (scene->aux_bus_count)); + aux_buses[scene->aux_bus_count] = aux_bus; + free(cbox_rt_swap_pointers_and_update_count(scene->rt, (void **)&scene->aux_buses, aux_buses, &scene->aux_bus_count, scene->aux_bus_count + 1)); + return TRUE; +} + +void cbox_scene_remove_aux_bus(struct cbox_scene *scene, struct cbox_aux_bus *removed) +{ + int pos = -1; + for (uint32_t i = 0; i < scene->aux_bus_count; i++) + { + if (scene->aux_buses[i] == removed) + { + pos = i; + break; + } + } + assert(pos != -1); + for (uint32_t i = 0; i < scene->instrument_count; i++) + cbox_instrument_disconnect_aux_bus(scene->instruments[i], removed); + + struct cbox_aux_bus **aux_buses = malloc(sizeof(struct cbox_aux_bus *) * (scene->aux_bus_count - 1)); + memcpy(aux_buses, scene->aux_buses, sizeof(struct cbox_aux_bus *) * pos); + memcpy(aux_buses + pos, scene->aux_buses + pos + 1, sizeof(struct cbox_aux_bus *) * (scene->aux_bus_count - pos - 1)); + free(cbox_rt_swap_pointers_and_update_count(scene->rt, (void **)&scene->aux_buses, aux_buses, &scene->aux_bus_count, scene->aux_bus_count - 1)); +} + +struct cbox_aux_bus *cbox_scene_get_aux_bus(struct cbox_scene *scene, const char *name, int allow_load, GError **error) +{ + for (uint32_t i = 0; i < scene->aux_bus_count; i++) + { + if (!strcmp(scene->aux_buses[i]->name, name)) + { + return scene->aux_buses[i]; + } + } + if (!allow_load) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Aux bus not found: %s", name); + return FALSE; + } + struct cbox_aux_bus *bus = cbox_aux_bus_load(scene, name, scene->rt, error); + if (!bus) + return NULL; + return bus; +} + +static int write_events_to_instrument_ports(struct cbox_scene *scene, struct cbox_midi_buffer *source) +{ + uint32_t i; + + for (i = 0; i < scene->instrument_count; i++) + cbox_midi_buffer_clear(&scene->instruments[i]->module->midi_input); + for (uint32_t l = 0; l < scene->layer_count; l++) + { + struct cbox_layer *lp = scene->layers[l]; + if (lp->external_output_set) + cbox_midi_buffer_clear(&lp->output_buffer); + } + + if (!source) + return 0; + + uint32_t event_count = cbox_midi_buffer_get_count(source); + for (i = 0; i < event_count; i++) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(source, i); + + // XXXKF ignore sysex for now + if (event->size >= 4) + continue; + + for (uint32_t l = 0; l < scene->layer_count; l++) + { + struct cbox_layer *lp = scene->layers[l]; + if (!lp->enabled) + continue; + if (!lp->external_output_set && !lp->instrument) + continue; + uint8_t data[4] = {0, 0, 0, 0}; + memcpy(data, event->data_inline, event->size); + if (data[0] < 0xF0) // per-channel messages + { + int cmd = data[0] >> 4; + // filter on MIDI channel + if (lp->in_channel >= 0 && lp->in_channel != (data[0] & 0x0F)) + continue; + // force output channel + if (lp->out_channel >= 0) + data[0] = (data[0] & 0xF0) + (lp->out_channel & 0x0F); + if (cmd >= 8 && cmd <= 10) + { + if (cmd == 10 && lp->disable_aftertouch) + continue; + // note filter + if (data[1] < lp->low_note || data[1] > lp->high_note) + continue; + // transpose + int transpose = lp->transpose + (lp->ignore_scene_transpose ? 0 : scene->transpose); + if (transpose) + { + int note = data[1] + transpose; + if (note < 0 || note > 127) + continue; + data[1] = (uint8_t)note; + } + // fixed note + if (lp->fixed_note != -1) + { + data[1] = (uint8_t)lp->fixed_note; + } + } + else if (cmd == 11 && data[1] == 64 && lp->invert_sustain) + { + data[2] = 127 - data[2]; + } + else if (lp->ignore_program_changes && cmd == 11 && (data[1] == 0 || data[1] == 32)) + continue; + else if (cmd == 13 && lp->disable_aftertouch) + continue; + else if (cmd == 12 && lp->ignore_program_changes) + continue; + } + struct cbox_midi_buffer *output = lp->instrument ? &lp->instrument->module->midi_input : &lp->output_buffer; + if (!cbox_midi_buffer_write_event(output, event->time, data, event->size)) + return -i; + if (lp->consume) + break; + } + } + + return event_count; +} + +void cbox_scene_update_connected_outputs(struct cbox_scene *scene) +{ + for (uint32_t l = 0; l < scene->layer_count; l++) + { + struct cbox_layer *lp = scene->layers[l]; + struct cbox_midi_merger *merger = NULL; + if (lp->external_output_set) + merger = cbox_rt_get_midi_output(scene->engine->rt, &lp->external_output); + if (merger != lp->external_merger) + { + if (lp->external_merger) + cbox_midi_merger_disconnect(lp->external_merger, &lp->output_buffer, scene->rt); + if (merger) + cbox_midi_merger_connect(merger, &lp->output_buffer, scene->rt, &lp->external_merger); + } + } +} + +void cbox_scene_render(struct cbox_scene *scene, uint32_t nframes, float *output_buffers[], uint32_t output_channels) +{ + uint32_t i, n; + + if (scene->rt && scene->rt->io) + { + struct cbox_io *io = scene->rt->io; + for (i = 0; i < io->io_env.input_count; i++) + { + if (IS_RECORDING_SOURCE_CONNECTED(scene->rec_mono_inputs[i])) + cbox_recording_source_push(&scene->rec_mono_inputs[i], (const float **)&io->input_buffers[i], 0, nframes); + } + for (i = 0; i < io->io_env.input_count / 2; i++) + { + if (IS_RECORDING_SOURCE_CONNECTED(scene->rec_stereo_inputs[i])) + { + const float *buf[2] = { io->input_buffers[i * 2], io->input_buffers[i * 2 + 1] }; + cbox_recording_source_push(&scene->rec_stereo_inputs[i], buf, 0, nframes); + } + } + } + + for(struct cbox_adhoc_pattern **ppat = &scene->adhoc_patterns; *ppat; ) + { + cbox_midi_buffer_clear(&(*ppat)->output_buffer); + if ((*ppat)->completed) + { + struct cbox_adhoc_pattern *retired = *ppat; + *ppat = retired->next; + retired->next = scene->retired_adhoc_patterns; + scene->retired_adhoc_patterns = retired; + } + else + { + cbox_adhoc_pattern_render((*ppat), 0, nframes); + ppat = &((*ppat)->next); + } + } + + cbox_midi_buffer_clear(&scene->midibuf_total); + cbox_midi_merger_render(&scene->scene_input_merger); + + write_events_to_instrument_ports(scene, &scene->midibuf_total); + + for (n = 0; n < scene->aux_bus_count; n++) + { + for (i = 0; i < nframes; i ++) + { + scene->aux_buses[n]->input_bufs[0][i] = 0.f; + scene->aux_buses[n]->input_bufs[1][i] = 0.f; + } + } + + for (n = 0; n < scene->instrument_count; n++) + { + struct cbox_instrument *instr = scene->instruments[n]; + struct cbox_module *module = instr->module; + int event_count = instr->module->midi_input.count; + int cur_event = 0; + uint32_t highwatermark = 0; + cbox_sample_t channels[CBOX_MAX_AUDIO_PORTS][CBOX_BLOCK_SIZE]; + cbox_sample_t *outputs[CBOX_MAX_AUDIO_PORTS]; + for (i= 0; i < module->outputs; i++) + outputs[i] = channels[i]; + + for (i = 0; i < nframes; i += CBOX_BLOCK_SIZE) + { + scene->engine->song_pos_offset = i; + if (i >= highwatermark) + { + while(cur_event < event_count) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(&module->midi_input, cur_event); + if (event) + { + if (event->time <= i) + (*module->process_event)(module, cbox_midi_event_get_data(event), event->size); + else + { + highwatermark = event->time; + break; + } + } + else + break; + + cur_event++; + } + } + (*module->process_block)(module, NULL, outputs); + for (uint32_t o = 0; o < module->outputs / 2; o++) + { + struct cbox_instrument_output *oobj = &instr->outputs[o]; + struct cbox_module *insert = oobj->insert; + struct cbox_gain *gain_obj = &oobj->gain_obj; + if (IS_RECORDING_SOURCE_CONNECTED(oobj->rec_dry)) + cbox_recording_source_push(&oobj->rec_dry, (const float **)(outputs + 2 * o), i, CBOX_BLOCK_SIZE); + if (insert && !insert->bypass) + (*insert->process_block)(insert, outputs + 2 * o, outputs + 2 * o); + if (IS_RECORDING_SOURCE_CONNECTED(oobj->rec_wet)) + cbox_recording_source_push(&oobj->rec_wet, (const float **)(outputs + 2 * o), i, CBOX_BLOCK_SIZE); + float *leftbuf, *rightbuf; + if (o < module->aux_offset / 2) + { + if (oobj->output_bus < 0) + continue; + uint32_t leftch = oobj->output_bus * 2; + if (leftch >= output_channels) + continue; + leftbuf = output_buffers[leftch]; + uint32_t rightch = leftch + 1; + rightbuf = rightch >= output_channels ? NULL : output_buffers[rightch]; + } + else + { + int bus = o - module->aux_offset / 2; + struct cbox_aux_bus *busobj = instr->aux_outputs[bus]; + if (busobj == NULL) + continue; + leftbuf = busobj->input_bufs[0]; + rightbuf = busobj->input_bufs[1]; + } + if (leftbuf && rightbuf) + { + cbox_gain_add_stereo(gain_obj, &leftbuf[i], channels[2 * o], &rightbuf[i], channels[2 * o + 1], CBOX_BLOCK_SIZE); + } + else + { + if (leftbuf) + cbox_gain_add_mono(gain_obj, &leftbuf[i], channels[2 * o], CBOX_BLOCK_SIZE); + if (rightbuf) + cbox_gain_add_mono(gain_obj, &rightbuf[i], channels[2 * o + 1], CBOX_BLOCK_SIZE); + } + } + } + while(cur_event < event_count) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(&module->midi_input, cur_event); + if (event) + { + (*module->process_event)(module, cbox_midi_event_get_data(event), event->size); + } + else + break; + + cur_event++; + } + } + + for (n = 0; n < scene->aux_bus_count; n++) + { + struct cbox_aux_bus *bus = scene->aux_buses[n]; + float left[CBOX_BLOCK_SIZE], right[CBOX_BLOCK_SIZE]; + float *outputs[2] = {left, right}; + for (i = 0; i < nframes; i += CBOX_BLOCK_SIZE) + { + float *inputs[2]; + inputs[0] = &bus->input_bufs[0][i]; + inputs[1] = &bus->input_bufs[1][i]; + bus->module->process_block(bus->module, inputs, outputs); + for (int j = 0; j < CBOX_BLOCK_SIZE; j++) + { + output_buffers[0][i + j] += left[j]; + output_buffers[1][i + j] += right[j]; + } + } + } + + uint32_t output_count = scene->engine->io_env.output_count; + // XXXKF this assumes that the buffers are zeroed on start - which isn't true if there are multiple scenes + for (i = 0; i < output_count; i++) + { + if (IS_RECORDING_SOURCE_CONNECTED(scene->rec_mono_outputs[i])) + cbox_recording_source_push(&scene->rec_mono_outputs[i], (const float **)&output_buffers[i], 0, nframes); + } + for (i = 0; i < output_count / 2; i++) + { + if (IS_RECORDING_SOURCE_CONNECTED(scene->rec_stereo_outputs[i])) + { + const float *buf[2] = { output_buffers[i * 2], output_buffers[i * 2 + 1] }; + cbox_recording_source_push(&scene->rec_stereo_outputs[i], buf, 0, nframes); + } + } +} + +void cbox_scene_clear(struct cbox_scene *scene) +{ + g_free(scene->name); + g_free(scene->title); + scene->name = g_strdup(""); + scene->title = g_strdup(""); + while(scene->layer_count > 0) + { + struct cbox_layer *layer = cbox_scene_remove_layer(scene, 0); + CBOX_DELETE(layer); + } + + while(scene->aux_bus_count > 0) + CBOX_DELETE(scene->aux_buses[scene->aux_bus_count - 1]); +} + +static struct cbox_instrument *create_instrument(struct cbox_scene *scene, struct cbox_module *module) +{ + int auxes = (module->outputs - module->aux_offset) / 2; + + struct cbox_instrument *instr = malloc(sizeof(struct cbox_instrument)); + CBOX_OBJECT_HEADER_INIT(instr, cbox_instrument, CBOX_GET_DOCUMENT(scene)); + instr->scene = scene; + instr->module = module; + instr->outputs = calloc(module->outputs / 2, sizeof(struct cbox_instrument_output)); + instr->refcount = 0; + instr->aux_outputs = calloc(auxes, sizeof(struct cbox_aux_bus *)); + instr->aux_output_names = calloc(auxes, sizeof(char *)); + instr->aux_output_count = auxes; + + for (uint32_t i = 0; i < module->outputs / 2; i ++) + cbox_instrument_output_init(&instr->outputs[i], scene, module->engine->io_env.buffer_size); + + return instr; +} + +struct cbox_instrument *cbox_scene_create_instrument(struct cbox_scene *scene, const char *instrument_name, const char *engine_name, GError **error) +{ + gpointer value = g_hash_table_lookup(scene->instrument_hash, instrument_name); + if (value) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Instrument already exists: '%s'", instrument_name); + return NULL; + } + + struct cbox_document *doc = CBOX_GET_DOCUMENT(scene); + struct cbox_module_manifest *mptr = NULL; + struct cbox_instrument *instr = NULL; + struct cbox_module *module = NULL; + + mptr = cbox_module_manifest_get_by_name(engine_name); + if (!mptr) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No engine called '%s'", engine_name); + return NULL; + } + + module = cbox_module_manifest_create_module(mptr, NULL, doc, scene->rt, scene->engine, instrument_name, error); + if (!module) + { + cbox_force_error(error); + g_prefix_error(error, "Cannot create engine '%s' for instrument '%s': ", engine_name, instrument_name); + return NULL; + } + + instr = create_instrument(scene, module); + + cbox_command_target_init(&instr->cmd_target, cbox_instrument_process_cmd, instr); + g_hash_table_insert(scene->instrument_hash, g_strdup(instrument_name), instr); + CBOX_OBJECT_REGISTER(instr); + + return instr; +} + +struct cbox_instrument *cbox_scene_get_instrument_by_name(struct cbox_scene *scene, const char *name, gboolean load, GError **error) +{ + struct cbox_module_manifest *mptr = NULL; + struct cbox_instrument *instr = NULL; + struct cbox_module *module = NULL; + gchar *instr_section = NULL; + gpointer value = g_hash_table_lookup(scene->instrument_hash, name); + const char *cv, *instr_engine; + struct cbox_document *doc = CBOX_GET_DOCUMENT(scene); + assert(scene); + + if (value) + return value; + if (!load) + return NULL; + + instr_section = g_strdup_printf("instrument:%s", name); + + if (!cbox_config_has_section(instr_section)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No config section for instrument '%s'", name); + goto error; + } + + instr_engine = cbox_config_get_string(instr_section, "engine"); + if (!instr_engine) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Engine not specified in instrument '%s'", name); + goto error; + } + + mptr = cbox_module_manifest_get_by_name(instr_engine); + if (!mptr) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No engine called '%s'", instr_engine); + goto error; + } + + // cbox_module_manifest_dump(mptr); + + module = cbox_module_manifest_create_module(mptr, instr_section, doc, scene->rt, scene->engine, name, error); + if (!module) + { + cbox_force_error(error); + g_prefix_error(error, "Cannot create engine '%s' for instrument '%s': ", instr_engine, name); + goto error; + } + + instr = create_instrument(scene, module); + + for (uint32_t i = 0; i < module->outputs / 2; i ++) + { + struct cbox_instrument_output *oobj = instr->outputs + i; + + gchar *key = i == 0 ? g_strdup("output_bus") : g_strdup_printf("output%d_bus", 1 + i); + oobj->output_bus = cbox_config_get_int(instr_section, key, 1) - 1; + g_free(key); + key = i == 0 ? g_strdup("gain") : g_strdup_printf("gain%d", 1 + i); + cbox_gain_set_db(&oobj->gain_obj, cbox_config_get_float(instr_section, key, 0)); + g_free(key); + + key = i == 0 ? g_strdup("insert") : g_strdup_printf("insert%d", 1 + i); + cv = cbox_config_get_string(instr_section, key); + g_free(key); + + if (cv) + { + oobj->insert = cbox_module_new_from_fx_preset(cv, CBOX_GET_DOCUMENT(scene), module->rt, scene->engine, error); + if (!oobj->insert) + { + cbox_force_error(error); + g_prefix_error(error, "Cannot instantiate effect preset '%s' for instrument '%s': ", cv, name); + } + } + } + + for (uint32_t i = 0; i < instr->aux_output_count; i++) + { + instr->aux_outputs[i] = NULL; + + gchar *key = g_strdup_printf("aux%d", 1 + i); + gchar *value = cbox_config_get_string(instr_section, key); + instr->aux_output_names[i] = value ? g_strdup(value) : NULL; + g_free(key); + + } + cbox_command_target_init(&instr->cmd_target, cbox_instrument_process_cmd, instr); + + free(instr_section); + + g_hash_table_insert(scene->instrument_hash, g_strdup(name), instr); + CBOX_OBJECT_REGISTER(instr); + + // cbox_recording_source_attach(&instr->outputs[0].rec_dry, cbox_recorder_new_stream("output.wav")); + + return instr; + +error: + free(instr_section); + return NULL; +} + +static struct cbox_recording_source *create_rec_sources(struct cbox_scene *scene, int buffer_size, int count, int channels) +{ + struct cbox_recording_source *s = malloc(sizeof(struct cbox_recording_source) * count); + for (int i = 0; i < count; i++) + cbox_recording_source_init(&s[i], scene, buffer_size, channels); + return s; +} + +static void destroy_rec_sources(struct cbox_recording_source *s, int count) +{ + for (int i = 0; i < count; i++) + cbox_recording_source_uninit(&s[i]); + free(s); +} + +struct cbox_scene *cbox_scene_new(struct cbox_document *document, struct cbox_engine *engine) +{ + if (!engine->io_env.buffer_size) + return NULL; + + struct cbox_scene *s = malloc(sizeof(struct cbox_scene)); + if (!s) + return NULL; + + CBOX_OBJECT_HEADER_INIT(s, cbox_scene, document); + s->engine = engine; + s->rt = engine ? engine->rt : NULL; + s->instrument_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + s->name = g_strdup(""); + s->title = g_strdup(""); + s->layers = NULL; + s->aux_buses = NULL; + s->instruments = NULL; + s->layer_count = 0; + s->instrument_count = 0; + s->aux_bus_count = 0; + cbox_command_target_init(&s->cmd_target, cbox_scene_process_cmd, s); + s->transpose = 0; + s->connected_inputs = NULL; + s->connected_input_count = 0; + s->enable_default_song_input = TRUE; + s->enable_default_external_input = TRUE; + + cbox_midi_buffer_init(&s->midibuf_total); + cbox_midi_merger_init(&s->scene_input_merger, &s->midibuf_total); + + int buffer_size = engine->io_env.buffer_size; + s->rec_mono_inputs = create_rec_sources(s, buffer_size, engine->io_env.input_count, 1); + s->rec_stereo_inputs = create_rec_sources(s, buffer_size, engine->io_env.input_count / 2, 2); + s->rec_mono_outputs = create_rec_sources(s, buffer_size, engine->io_env.output_count, 1); + s->rec_stereo_outputs = create_rec_sources(s, buffer_size, engine->io_env.output_count / 2, 2); + s->adhoc_patterns = NULL; + s->retired_adhoc_patterns = NULL; + + CBOX_OBJECT_REGISTER(s); + + cbox_engine_add_scene(s->engine, s); + cbox_scene_update_connected_inputs(s); + return s; +} + +void cbox_scene_update_connected_inputs(struct cbox_scene *scene) +{ + if (!scene->rt || !scene->rt->io) + return; + + // This is called when a MIDI Input port has been created, connected/disconnected + // or is about to be removed (and then the removing flag will be set) + for (uint32_t i = 0; i < scene->connected_input_count; ) + { + struct cbox_midi_input *input = scene->connected_inputs[i]; + if (input->removing || !cbox_uuid_equal(&input->output, &scene->_obj_hdr.instance_uuid)) + { + cbox_midi_merger_disconnect(&scene->scene_input_merger, &input->buffer, scene->rt); + cbox_rt_array_remove(scene->rt, (void ***)&scene->connected_inputs, &scene->connected_input_count, i); + } + else + i++; + } + for (GSList *p = scene->rt->io->midi_inputs; p; p = p->next) + { + struct cbox_midi_input *input = p->data; + if (cbox_uuid_equal(&input->output, &scene->_obj_hdr.instance_uuid)) + { + gboolean found = FALSE; + for (uint32_t i = 0; i < scene->connected_input_count; i++) + { + if (scene->connected_inputs[i] == input) + { + found = TRUE; + break; + } + } + if (!found) + { + cbox_midi_merger_connect(&scene->scene_input_merger, &input->buffer, scene->rt, NULL); + cbox_rt_array_insert(scene->rt, (void ***)&scene->connected_inputs, &scene->connected_input_count, -1, input); + } + } + } + if (scene->enable_default_song_input) + { + cbox_midi_merger_connect(&scene->scene_input_merger, &scene->engine->midibuf_aux, scene->rt, NULL); + cbox_midi_merger_connect(&scene->scene_input_merger, &scene->engine->midibuf_song, scene->rt, NULL); + } + else + { + cbox_midi_merger_disconnect(&scene->scene_input_merger, &scene->engine->midibuf_aux, scene->rt); + cbox_midi_merger_disconnect(&scene->scene_input_merger, &scene->engine->midibuf_song, scene->rt); + } + + if (scene->enable_default_external_input) + cbox_midi_merger_connect(&scene->scene_input_merger, &scene->engine->midibuf_jack, scene->rt, NULL); + else + cbox_midi_merger_disconnect(&scene->scene_input_merger, &scene->engine->midibuf_jack, scene->rt); + +} + +static void free_adhoc_pattern_list(struct cbox_scene *scene, struct cbox_adhoc_pattern *ap) +{ + while(ap) + { + struct cbox_adhoc_pattern *next = ap->next; + ap->next = NULL; + cbox_midi_merger_disconnect(&scene->scene_input_merger, &ap->output_buffer, scene->rt); + cbox_adhoc_pattern_destroy(ap); + ap = next; + } +} + +struct play_adhoc_pattern_arg +{ + struct cbox_scene *scene; + struct cbox_adhoc_pattern *ap; + struct cbox_adhoc_pattern *retired; +}; + +static int play_adhoc_pattern_execute(void *arg_) +{ + struct play_adhoc_pattern_arg *arg = arg_; + + if (arg->ap) + { + struct cbox_adhoc_pattern *ap = arg->scene->adhoc_patterns; + + // If there is already an adhoc pattern with a given non-zero id, stop it + // and release all the pending notes. Retry until all the notes are + // released. + if (arg->ap->id) + { + while(ap && ap->id != arg->ap->id) + ap = ap->next; + if (ap) + { + ap->completed = TRUE; + if (ap->active_notes.channels_active) + return 0; + } + } + + arg->ap->next = arg->scene->adhoc_patterns; + arg->scene->adhoc_patterns = arg->ap; + } + arg->retired = arg->scene->retired_adhoc_patterns; + arg->scene->retired_adhoc_patterns = NULL; + // XXXKF should convert pattern length into sample position instead of assuming 0x7FFFFFFF (though it likely doesn't matter) + cbox_midi_clip_playback_set_pattern(&arg->ap->playback, arg->ap->pattern_playback, 0, 0x7FFFFFFF, 0, 0); + return 1; +} + +void cbox_scene_play_adhoc_pattern(struct cbox_scene *scene, struct cbox_adhoc_pattern *ap) +{ + static struct cbox_rt_cmd_definition cmd = { NULL, play_adhoc_pattern_execute, NULL }; + struct play_adhoc_pattern_arg arg = { scene, ap, NULL }; + cbox_midi_merger_connect(&scene->scene_input_merger, &ap->output_buffer, scene->rt, NULL); + cbox_rt_execute_cmd_sync(scene->rt, &cmd, &arg); + if (arg.retired) + free_adhoc_pattern_list(scene, arg.retired); +} + +gboolean cbox_scene_move_instrument_to(struct cbox_scene *scene, struct cbox_instrument *instrument, struct cbox_scene *new_scene, int dstpos, GError **error) +{ + int lcount = 0; + if (dstpos == -1) + dstpos = new_scene->layer_count; + for (uint32_t i = 0; i < scene->layer_count; i++) + { + if (scene->layers[i]->instrument == instrument) + lcount++; + } + if (!lcount) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Instrument '%s' not found in source scene", instrument->module->instance_name); + return FALSE; + } + if (cbox_scene_get_instrument_by_name(new_scene, instrument->module->instance_name, FALSE, NULL)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Instrument '%s' already exists in target scene", instrument->module->instance_name); + return FALSE; + } + + struct cbox_layer **new_src_layers = malloc(sizeof(struct cbox_layer *) * (scene->layer_count - lcount)); + struct cbox_layer **new_dst_layers = malloc(sizeof(struct cbox_layer *) * (new_scene->layer_count + lcount)); + int srcidx = 0, dstidx = 0; + memcpy(&new_dst_layers[dstidx], new_scene->layers, dstpos * sizeof(struct cbox_layer **)); + dstidx = dstpos; + for (uint32_t i = 0; i < scene->layer_count; i++) + { + if (scene->layers[i]->instrument != instrument) + new_src_layers[srcidx++] = scene->layers[i]; + else + new_dst_layers[dstidx++] = scene->layers[i]; + } + memcpy(&new_dst_layers[dstidx], new_scene->layers, (new_scene->layer_count - dstpos) * sizeof(struct cbox_layer **)); + dstidx += new_scene->layer_count; + + free(cbox_rt_swap_pointers_and_update_count(scene->rt, (void **)&scene->layers, new_src_layers, &scene->layer_count, srcidx)); + cbox_rt_array_remove_by_value(scene->rt, (void ***)&scene->instruments, &scene->instrument_count, instrument); + + cbox_rt_array_insert(scene->rt, (void ***)&new_scene->instruments, &new_scene->instrument_count, -1, instrument); + free(cbox_rt_swap_pointers_and_update_count(new_scene->rt, (void **)&new_scene->layers, new_dst_layers, &new_scene->layer_count, dstidx)); + + return TRUE; +} + +static void cbox_scene_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_scene *scene = CBOX_H2O(objhdr); + cbox_midi_merger_disconnect(&scene->scene_input_merger, &scene->engine->midibuf_aux, scene->rt); + cbox_midi_merger_disconnect(&scene->scene_input_merger, &scene->engine->midibuf_jack, scene->rt); + cbox_midi_merger_disconnect(&scene->scene_input_merger, &scene->engine->midibuf_song, scene->rt); + cbox_engine_remove_scene(scene->engine, scene); + cbox_scene_clear(scene); + g_free(scene->name); + g_free(scene->title); + assert(scene->instrument_count == 0); + free(scene->layers); + free(scene->aux_buses); + free(scene->instruments); + g_hash_table_destroy(scene->instrument_hash); + free(scene->connected_inputs); + + destroy_rec_sources(scene->rec_mono_inputs, scene->engine->io_env.input_count); + destroy_rec_sources(scene->rec_stereo_inputs, scene->engine->io_env.input_count / 2); + destroy_rec_sources(scene->rec_mono_outputs, scene->engine->io_env.output_count); + destroy_rec_sources(scene->rec_stereo_outputs, scene->engine->io_env.output_count / 2); + + free_adhoc_pattern_list(scene, scene->retired_adhoc_patterns); + free_adhoc_pattern_list(scene, scene->adhoc_patterns); + cbox_midi_merger_close(&scene->scene_input_merger, scene->engine->rt); + free(scene); +} diff --git a/template/calfbox/scene.h b/template/calfbox/scene.h new file mode 100644 index 0000000..5b9174c --- /dev/null +++ b/template/calfbox/scene.h @@ -0,0 +1,82 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SCENE_H +#define CBOX_SCENE_H + +#include "cmd.h" +#include "dom.h" +#include "mididest.h" + +CBOX_EXTERN_CLASS(cbox_scene) + +struct cbox_aux_bus; +struct cbox_instrument; +struct cbox_midi_buffer; +struct cbox_recording_source; +struct cbox_song_playback; + +struct cbox_scene +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + gchar *name; + gchar *title; + + GHashTable *instrument_hash; + struct cbox_rt *rt; + struct cbox_layer **layers; + uint32_t layer_count; + struct cbox_instrument **instruments; + uint32_t instrument_count; + struct cbox_aux_bus **aux_buses; + uint32_t aux_bus_count; + int transpose; + struct cbox_engine *engine; + struct cbox_midi_merger scene_input_merger; + struct cbox_midi_buffer midibuf_total; + + struct cbox_midi_input **connected_inputs; + uint32_t connected_input_count; + + gboolean enable_default_song_input, enable_default_external_input; + + struct cbox_recording_source *rec_mono_inputs, *rec_mono_outputs; + struct cbox_recording_source *rec_stereo_inputs, *rec_stereo_outputs; + + struct cbox_adhoc_pattern *adhoc_patterns, *retired_adhoc_patterns; +}; + +extern struct cbox_scene *cbox_scene_new(struct cbox_document *document, struct cbox_engine *engine); +extern gboolean cbox_scene_add_layer(struct cbox_scene *scene, struct cbox_layer *layer, GError **error); +extern gboolean cbox_scene_insert_layer(struct cbox_scene *scene, struct cbox_layer *layer, int pos, GError **error); +extern struct cbox_layer *cbox_scene_remove_layer(struct cbox_scene *scene, int pos); +extern void cbox_scene_move_layer(struct cbox_scene *scene, unsigned int oldpos, unsigned int newpos); +extern gboolean cbox_scene_load(struct cbox_scene *scene, const char *section, GError **error); +extern gboolean cbox_scene_remove_instrument(struct cbox_scene *scene, struct cbox_instrument *instrument); +extern struct cbox_aux_bus *cbox_scene_get_aux_bus(struct cbox_scene *scene, const char *name, int allow_load, GError **error); +extern void cbox_scene_render(struct cbox_scene *scene, uint32_t nframes, float *output_buffers[], uint32_t output_channels); +extern void cbox_scene_clear(struct cbox_scene *scene); +extern void cbox_scene_update_connected_inputs(struct cbox_scene *scene); +extern void cbox_scene_update_connected_outputs(struct cbox_scene *scene); +extern gboolean cbox_scene_move_instrument_to(struct cbox_scene *scene, struct cbox_instrument *instrument, struct cbox_scene *new_scene, int dstpos, GError **error); +extern struct cbox_instrument *cbox_scene_get_instrument_by_name(struct cbox_scene *scene, const char *name, gboolean load, GError **error); +extern struct cbox_instrument *cbox_scene_create_instrument(struct cbox_scene *scene, const char *instrument_name, const char *engine_name, GError **error); +extern void cbox_scene_play_adhoc_pattern(struct cbox_scene *scene, struct cbox_adhoc_pattern *ap); + +#endif diff --git a/template/calfbox/scripting.c b/template/calfbox/scripting.c new file mode 100644 index 0000000..d4f273d --- /dev/null +++ b/template/calfbox/scripting.c @@ -0,0 +1,579 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" + +#include "app.h" +#include "blob.h" +#include "engine.h" +#include "errors.h" +#include "module.h" +#include "scripting.h" +#include +#include + +#include "config-api.h" +#include "tarfile.h" +#include "wavebank.h" +#include "scene.h" + +static gboolean audio_running = FALSE; +static gboolean engine_initialised = FALSE; + +gboolean cbox_embed_init_engine(const char *config_file, GError **error) +{ + if (engine_initialised) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Engine already initialized"); + return FALSE; + } + + cbox_dom_init(); + app.tarpool = cbox_tarpool_new(); + app.document = cbox_document_new(); + app.rt = cbox_rt_new(app.document); + app.engine = cbox_engine_new(app.document, app.rt); + app.rt->engine = app.engine; + cbox_config_init(config_file); + cbox_wavebank_init(); + engine_initialised = 1; + + return TRUE; +} + +gboolean cbox_embed_shutdown_engine(GError **error) +{ + if (!engine_initialised) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Engine not initialized"); + return FALSE; + } + + CBOX_DELETE(app.engine); + CBOX_DELETE(app.rt); + cbox_tarpool_destroy(app.tarpool); + cbox_document_destroy(app.document); + cbox_wavebank_close(); + cbox_config_close(); + cbox_dom_close(); + engine_initialised = FALSE; + + return TRUE; +} + +gboolean cbox_embed_start_audio(struct cbox_command_target *target, GError **error) +{ + if (!engine_initialised) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Engine not initialized"); + return FALSE; + } + if (audio_running) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Audio already started"); + return FALSE; + } + + struct cbox_open_params params; + + GError *error2 = NULL; + if (!cbox_io_init(&app.io, ¶ms, target, &error2)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot initialise sound I/O: %s", (error2 && error2->message ? error2->message : "Unknown error")); + g_error_free(error2); + return FALSE; + } + + const char *effect_preset_name = cbox_config_get_string("master", "effect"); + + cbox_rt_set_io(app.rt, &app.io); + cbox_scene_new(app.document, app.engine); + cbox_rt_start(app.rt, target); + if (effect_preset_name && *effect_preset_name) + { + app.engine->effect = cbox_module_new_from_fx_preset(effect_preset_name, app.document, app.rt, app.engine, error); + if (!app.engine->effect) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot load master effect preset %s: %s", effect_preset_name, (error2 && error2->message ? error2->message : "Unknown error")); + g_error_free(error2); + return FALSE; + } + } + audio_running = TRUE; + return TRUE; +} + +gboolean cbox_embed_stop_audio(GError **error) +{ + if (!engine_initialised) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Engine not initialised"); + return FALSE; + } + if (!audio_running) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Audio not running"); + return FALSE; + } + + while(app.engine->scene_count > 0) + CBOX_DELETE(app.engine->scenes[0]); + cbox_rt_stop(app.rt); + cbox_io_close(&app.io); + audio_running = FALSE; + return TRUE; +} + +struct cbox_command_target *cbox_embed_get_cmd_root() +{ + return &app.cmd_target; +} + +#if USE_PYTHON + +// This is a workaround for what I consider a defect in pyconfig.h +#undef _XOPEN_SOURCE +#undef _POSIX_C_SOURCE + +#include + +struct PyCboxCallback +{ + PyObject_HEAD + struct cbox_command_target *target; +}; + +static PyObject * +PyCboxCallback_New(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + struct PyCboxCallback *self; + + self = (struct PyCboxCallback *)type->tp_alloc(type, 0); + if (self != NULL) { + self->target = NULL; + } + + return (PyObject *)self; +} + +static int +PyCboxCallback_Init(struct PyCboxCallback *self, PyObject *args, PyObject *kwds) +{ + PyObject *cobj = NULL; + if (!PyArg_ParseTuple(args, "O!:init", &PyCapsule_Type, &cobj)) + return -1; + + self->target = PyCapsule_GetPointer(cobj, NULL); + return 0; +} + +static PyObject *cbox_python_do_cmd_on(struct cbox_command_target *ct, PyObject *self, PyObject *args); + +static PyObject * +PyCboxCallback_Call(PyObject *_self, PyObject *args, PyObject *kwds) +{ + struct PyCboxCallback *self = (struct PyCboxCallback *)_self; + + return cbox_python_do_cmd_on(self->target, _self, args); +} + +PyTypeObject CboxCallbackType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_cbox.Callback", + .tp_basicsize = sizeof(struct PyCboxCallback), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "Callback for feedback channel to Cbox C code", + .tp_init = (initproc)PyCboxCallback_Init, + .tp_new = PyCboxCallback_New, + .tp_call = PyCboxCallback_Call +}; + +static gboolean set_error_from_python(GError **error) +{ + PyObject *ptype = NULL, *pvalue = NULL, *ptraceback = NULL; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + PyObject *ptypestr = PyObject_Str(ptype); + PyObject *pvaluestr = PyObject_Str(pvalue); + PyObject *ptypestr_unicode = PyUnicode_AsUTF8String(ptypestr); + PyObject *pvaluestr_unicode = PyUnicode_AsUTF8String(pvaluestr); + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "%s: %s", PyBytes_AsString(ptypestr_unicode), PyBytes_AsString(pvaluestr_unicode)); + Py_DECREF(pvaluestr_unicode); + Py_DECREF(ptypestr_unicode); + //g_error("%s:%s", PyString_AsString(ptypestr), PyString_AsString(pvaluestr)); + Py_DECREF(ptypestr); + Py_DECREF(pvaluestr); + Py_DECREF(ptype); + Py_XDECREF(pvalue); + Py_XDECREF(ptraceback); + return FALSE; +} + +static gboolean bridge_to_python_callback(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + PyObject *callback = ct->user_data; + + int argc = strlen(cmd->arg_types); + PyObject *arg_values = PyList_New(argc); + for (int i = 0; i < argc; i++) + { + if (cmd->arg_types[i] == 's') + { + PyList_SetItem(arg_values, i, PyUnicode_FromString(cmd->arg_values[i])); + } + else + if (cmd->arg_types[i] == 'o') + { + struct cbox_objhdr *oh = cmd->arg_values[i]; + char buf[40]; + cbox_uuid_tostring(&oh->instance_uuid, buf); + PyList_SetItem(arg_values, i, PyUnicode_FromString(buf)); + } + else + if (cmd->arg_types[i] == 'u') + { + struct cbox_uuid *uuid = cmd->arg_values[i]; + char buf[40]; + cbox_uuid_tostring(uuid, buf); + PyList_SetItem(arg_values, i, PyUnicode_FromString(buf)); + } + else + if (cmd->arg_types[i] == 'i') + { + PyList_SetItem(arg_values, i, PyLong_FromLong(*(int *)cmd->arg_values[i])); + } + else + if (cmd->arg_types[i] == 'f') + { + PyList_SetItem(arg_values, i, PyFloat_FromDouble(*(double *)cmd->arg_values[i])); + } + else + if (cmd->arg_types[i] == 'b') + { + struct cbox_blob *blob = cmd->arg_values[i]; + PyList_SetItem(arg_values, i, PyByteArray_FromStringAndSize(blob->data, blob->size)); + } + else + { + PyList_SetItem(arg_values, i, Py_None); + Py_INCREF(Py_None); + } + } + struct PyCboxCallback *fbcb = NULL; + + PyObject *args = PyTuple_New(3); + PyTuple_SetItem(args, 0, PyUnicode_FromString(cmd->command)); + PyObject *pyfb = NULL; + if (fb) + { + struct PyCboxCallback *fbcb = PyObject_New(struct PyCboxCallback, &CboxCallbackType); + fbcb->target = fb; + pyfb = (PyObject *)fbcb; + } + else + { + pyfb = Py_None; + Py_INCREF(Py_None); + } + PyTuple_SetItem(args, 1, pyfb); + PyTuple_SetItem(args, 2, arg_values); + + PyObject *result = PyObject_Call(callback, args, NULL); + Py_DECREF(args); + + if (fbcb) + fbcb->target = NULL; + + if (result) + { + Py_DECREF(result); + return TRUE; + } + + return set_error_from_python(error); +} + +static PyObject *cbox_python_do_cmd_on(struct cbox_command_target *ct, PyObject *self, PyObject *args) +{ + const char *command = NULL; + PyObject *callback = NULL; + PyObject *list = NULL; + if (!PyArg_ParseTuple(args, "sOO!:do_cmd", &command, &callback, &PyList_Type, &list)) + return NULL; + + int len = PyList_Size(list); + void *extra = malloc(len * sizeof(double)); + struct cbox_osc_command cmd; + GError *error = NULL; + char *arg_types = malloc(len + 1); + void **arg_values = malloc(2 * len * sizeof(void *)); + void **arg_extra = &arg_values[len]; + cmd.command = command; + cmd.arg_types = arg_types; + cmd.arg_values = arg_values; + double *arg_space = extra; + gboolean free_blobs = FALSE; + for (int i = 0; i < len; i++) + { + cmd.arg_values[i] = &arg_space[i]; + PyObject *value = PyList_GetItem(list, i); + + if (PyLong_Check(value)) + { + arg_types[i] = 'i'; + *(int *)arg_values[i] = PyLong_AsLong(value); + } + else + if (PyFloat_Check(value)) + { + arg_types[i] = 'f'; + *(double *)arg_values[i] = PyFloat_AsDouble(value); + } + else + if (PyUnicode_Check(value)) + { + PyObject *utf8str = PyUnicode_AsUTF8String(value); + arg_types[i] = 's'; + arg_extra[i] = utf8str; + arg_values[i] = PyBytes_AsString(utf8str); + free_blobs = TRUE; + } + else + if (PyByteArray_Check(value)) + { + const void *buf = PyByteArray_AsString(value); + ssize_t len = PyByteArray_Size(value); + + if (buf) + { + // note: this is not really acquired, the blob is freed using free and not cbox_blob_destroy + struct cbox_blob *blob = cbox_blob_new_acquire_data((void *)buf, len); + arg_types[i] = 'b'; + arg_values[i] = blob; + free_blobs = TRUE; + } + else + arg_types[i] = 'N'; + } + else + { + PyObject *ob_type = (PyObject *)value->ob_type; + PyObject *typename_unicode = PyObject_Str(ob_type); + PyObject *typename_bytes = PyUnicode_AsUTF8String(typename_unicode); + PyObject *exc = PyErr_Format(PyExc_ValueError, "Cannot decode Python type '%s' to execute '%s'", PyBytes_AsString(typename_bytes), command); + Py_DECREF(typename_bytes); + Py_DECREF(typename_unicode); + + return exc; + } + } + arg_types[len] = '\0'; + + struct cbox_command_target target; + cbox_command_target_init(&target, bridge_to_python_callback, callback); + + // cbox_osc_command_dump(&cmd); + Py_INCREF(callback); + gboolean result = ct->process_cmd(ct, callback != Py_None ? &target : NULL, &cmd, &error); + Py_DECREF(callback); + + if (free_blobs) + { + for (int i = 0; i < len; i++) + { + if (arg_types[i] == 'b') + free(arg_values[i]); + if (arg_types[i] == 's') + Py_DECREF((PyObject *)arg_extra[i]); + } + } + free(arg_space); + free(arg_values); + free(arg_types); + + if (!result) + return PyErr_Format(PyExc_Exception, "%s", error ? error->message : "Unknown error"); + + Py_RETURN_NONE; +} + +static PyObject *cbox_python_do_cmd(PyObject *self, PyObject *args) +{ + if (!engine_initialised) + return PyErr_Format(PyExc_Exception, "Engine not initialised"); + return cbox_python_do_cmd_on(&app.cmd_target, self, args); +} + +#if CALFBOX_AS_MODULE + +#include "config-api.h" +#include "tarfile.h" +#include "wavebank.h" +#include "scene.h" + +static PyObject *pyerror_from_gerror(PyObject *exception, GError *error) +{ + PyObject *pyerr = PyErr_Format(exception, "%s", error ? error->message : "Unknown error"); + g_error_free(error); + return pyerr; +} + +static PyObject *cbox_python_init_engine(PyObject *self, PyObject *args) +{ + const char *config_file = NULL; + if (!PyArg_ParseTuple(args, "|z:init_engine", &config_file)) + return NULL; + + GError *error = NULL; + if (!cbox_embed_init_engine(config_file, &error)) + return pyerror_from_gerror(PyExc_Exception, error); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *cbox_python_shutdown_engine(PyObject *self, PyObject *args) +{ + if (!PyArg_ParseTuple(args, ":shutdown_engine")) + return NULL; + + GError *error = NULL; + if (!cbox_embed_shutdown_engine(&error)) + return pyerror_from_gerror(PyExc_Exception, error); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *cbox_python_start_audio(PyObject *self, PyObject *args) +{ + PyObject *callback = NULL; + if (!PyArg_ParseTuple(args, "|O:start_audio", &callback)) + return NULL; + + struct cbox_command_target target; + gboolean has_target = callback && callback != Py_None; + if (has_target) + cbox_command_target_init(&target, bridge_to_python_callback, callback); + + GError *error = NULL; + if (!cbox_embed_start_audio(has_target ? &target : NULL, &error)) + return pyerror_from_gerror(PyExc_Exception, error); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *cbox_python_start_noaudio(PyObject *self, PyObject *args) +{ + PyObject *callback = NULL; + int sample_rate = 0; + if (!PyArg_ParseTuple(args, "i|O:start_noaudio", &sample_rate, &callback)) + return NULL; + if (!engine_initialised) + return PyErr_Format(PyExc_Exception, "Engine not initialised"); + if (audio_running) + return PyErr_Format(PyExc_Exception, "Audio already started"); + + struct cbox_command_target target; + if (callback && callback != Py_None) + cbox_command_target_init(&target, bridge_to_python_callback, callback); + + cbox_rt_set_offline(app.rt, sample_rate, 1024); + cbox_scene_new(app.document, app.engine); + cbox_rt_start(app.rt, (callback && callback != Py_None) ? &target : NULL); + audio_running = TRUE; + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *cbox_python_stop_audio(PyObject *self, PyObject *args) +{ + if (!PyArg_ParseTuple(args, ":stop_audio")) + return NULL; + + GError *error = NULL; + if (!cbox_embed_stop_audio(&error)) + return pyerror_from_gerror(PyExc_Exception, error); + Py_INCREF(Py_None); + return Py_None; +} + +#endif + +static PyMethodDef CboxMethods[] = { + {"do_cmd", cbox_python_do_cmd, METH_VARARGS, "Execute a CalfBox command using a global path."}, +#if CALFBOX_AS_MODULE + {"init_engine", cbox_python_init_engine, METH_VARARGS, "Initialise the CalfBox engine using optional config file."}, + {"shutdown_engine", cbox_python_shutdown_engine, METH_VARARGS, "Shutdown the CalfBox engine."}, + {"start_audio", cbox_python_start_audio, METH_VARARGS, "Start real-time audio processing using I/O settings from the current config."}, + {"start_noaudio", cbox_python_start_noaudio, METH_VARARGS, "Start dummy audio processing using sample rate specified as argument."}, + {"stop_audio", cbox_python_stop_audio, METH_VARARGS, "Stop real-time audio processing."}, +#endif + {NULL, NULL, 0, NULL} +}; + +static PyModuleDef CboxModule = { + PyModuleDef_HEAD_INIT, "_cbox", NULL, -1, CboxMethods, + NULL, NULL, NULL, NULL +}; + +#if CALFBOX_AS_MODULE + +static void cbox_python_atexit() +{ + if (audio_running) { + cbox_rt_stop(app.rt); + cbox_io_close(&app.io); + audio_running = FALSE; + } + if (engine_initialised) { + cbox_tarpool_destroy(app.tarpool); + cbox_document_destroy(app.document); + cbox_wavebank_close(); + cbox_config_close(); + cbox_dom_close(); + engine_initialised = FALSE; + } +} + +PyMODINIT_FUNC +PyInit__cbox(void) +{ + PyObject *m = PyModule_Create(&CboxModule); + if (!m) + return NULL; + Py_INCREF(&CboxCallbackType); + if (PyType_Ready(&CboxCallbackType) < 0) + return NULL; + PyModule_AddObject(m, "Callback", (PyObject *)&CboxCallbackType); + atexit(cbox_python_atexit); + + return m; +} + +#else + +PyObject* +PyInit_cbox(void) +{ + PyObject *m = PyModule_Create(&CboxModule); + PyModule_AddObject(m, "Callback", (PyObject *)&CboxCallbackType); + return m; +} + +#endif +#endif diff --git a/template/calfbox/scripting.h b/template/calfbox/scripting.h new file mode 100644 index 0000000..ff93271 --- /dev/null +++ b/template/calfbox/scripting.h @@ -0,0 +1,19 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +extern void cbox_script_run(const char *name); diff --git a/template/calfbox/seq-adhoc.c b/template/calfbox/seq-adhoc.c new file mode 100644 index 0000000..73aab57 --- /dev/null +++ b/template/calfbox/seq-adhoc.c @@ -0,0 +1,56 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "master.h" +#include "seq.h" + +struct cbox_adhoc_pattern *cbox_adhoc_pattern_new(struct cbox_engine *engine, int id, struct cbox_midi_pattern *pattern) +{ + struct cbox_adhoc_pattern *ap = calloc(1, sizeof(struct cbox_adhoc_pattern)); + ap->next = NULL; + ap->pattern = pattern; + ap->pattern_playback = cbox_midi_pattern_playback_new(pattern); + ap->master = cbox_master_new(engine); + cbox_midi_playback_active_notes_init(&ap->active_notes); + cbox_midi_clip_playback_init(&ap->playback, &ap->active_notes, ap->master); + cbox_midi_buffer_init(&ap->output_buffer); + ap->id = id; + ap->completed = FALSE; + + return ap; +} + +void cbox_adhoc_pattern_render(struct cbox_adhoc_pattern *ap, uint32_t offset, uint32_t nsamples) +{ + if (ap->completed) + { + cbox_midi_playback_active_notes_release(&ap->active_notes, &ap->output_buffer, NULL); + return; + } + if (ap->playback.pos >= ap->playback.pattern->event_count) + ap->completed = TRUE; + cbox_midi_clip_playback_render(&ap->playback, &ap->output_buffer, offset, nsamples, FALSE); +} + +void cbox_adhoc_pattern_destroy(struct cbox_adhoc_pattern *ap) +{ + // XXXKF decide on pattern ownership and general object lifetime issues + cbox_midi_pattern_playback_destroy(ap->playback.pattern); + cbox_master_destroy(ap->master); + free(ap); +} diff --git a/template/calfbox/seq.c b/template/calfbox/seq.c new file mode 100644 index 0000000..98e1d4b --- /dev/null +++ b/template/calfbox/seq.c @@ -0,0 +1,930 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "engine.h" +#include "pattern.h" +#include "rt.h" +#include "seq.h" +#include "song.h" +#include "track.h" +#include + +static inline void accumulate_event(struct cbox_midi_playback_active_notes *notes, const struct cbox_midi_event *event) +{ + if (event->size != 3) + return; + // this ignores poly aftertouch - which, I supposed, is OK for now + if (event->data_inline[0] < 0x90 || event->data_inline[0] > 0x9F) + return; + if (event->data_inline[2] > 0) + { + int ch = event->data_inline[0] & 0x0F; + int note = event->data_inline[1] & 0x7F; + if (!(notes->channels_active & (1 << ch))) + { + for (int i = 0; i < 4; i++) + notes->notes[ch][i] = 0; + notes->channels_active |= 1 << ch; + } + notes->notes[ch][note >> 5] |= 1 << (note & 0x1F); + } +} + +// this releases a note on note off (accumulate_event is 'sticky') +static inline void accumulate_event2(struct cbox_midi_playback_active_notes *notes, const struct cbox_midi_event *event) +{ + if (event->size != 3) + return; + // this ignores poly aftertouch - which, I supposed, is OK for now + if (event->data_inline[0] < 0x80 || event->data_inline[0] > 0x9F) + return; + int ch = event->data_inline[0] & 0x0F; + int note = event->data_inline[1] & 0x7F; + uint32_t mask = 1 << (note & 0x1F); + if (event->data_inline[0] >= 0x90 && event->data_inline[2] > 0) + { + if (!(notes->channels_active & (1 << ch))) + { + for (int i = 0; i < 4; i++) + notes->notes[ch][i] = 0; + notes->channels_active |= 1 << ch; + } + notes->notes[ch][note >> 5] |= mask; + } else { + if (notes->notes[ch][note >> 5] & mask) { + notes->notes[ch][note >> 5] &= ~mask; + if (!notes->notes[ch][0] && !notes->notes[ch][1] && !notes->notes[ch][2] && !notes->notes[ch][3]) { + notes->channels_active &= ~(1 << ch); + } + } + } +} + +struct cbox_track_playback *cbox_track_playback_new_from_track(struct cbox_track *track, struct cbox_master *master, struct cbox_song_playback *spb, struct cbox_track_playback *old_state) +{ + struct cbox_track_playback *pb = malloc(sizeof(struct cbox_track_playback)); + cbox_uuid_copy(&pb->track_uuid, &CBOX_O2H(track)->instance_uuid); + pb->old_state = old_state; + pb->generation = track->generation; + pb->ref_count = 1; + pb->master = master; + int len = g_list_length(track->items); + pb->items = calloc(len, sizeof(struct cbox_track_playback_item)); + pb->external_merger = NULL; + pb->spb = spb; + pb->state_copied = FALSE; + pb->mute = track->mute; + + GList *it = track->items; + struct cbox_track_playback_item *p = pb->items; + uint32_t safe = 0; + while(it != NULL) + { + struct cbox_track_item *item = it->data; + struct cbox_midi_pattern_playback *mppb = cbox_song_playback_get_pattern(spb, item->pattern); + + // if items overlap, the first one takes precedence + if (item->time < safe) + { + // fully contained in previous item? skip all of it + // not fully contained - insert the fragment + if (item->time + item->length >= safe) + { + int cut = safe - item->time; + p->time = safe; + p->pattern = mppb; + p->offset = item->offset + cut; + p->length = item->length - cut; + p++; + } + } + else + { + p->time = item->time; + p->pattern = mppb; + p->offset = item->offset; + p->length = item->length; + safe = item->time + item->length; + p++; + } + + it = g_list_next(it); + } + // in case of full overlap, some items might have been skipped + pb->items_count = p - pb->items; + pb->pos = 0; + cbox_midi_clip_playback_init(&pb->playback, &pb->active_notes, master); + cbox_midi_playback_active_notes_init(&pb->active_notes); + cbox_midi_buffer_init(&pb->output_buffer); + cbox_track_playback_start_item(pb, 0, FALSE, 0); + + if (track->external_output_set) + { + struct cbox_midi_merger *merger = cbox_rt_get_midi_output(spb->engine->rt, &track->external_output); + if (merger) + cbox_midi_merger_connect(merger, &pb->output_buffer, spb->engine->rt, &pb->external_merger); + } + + return pb; +} + +void cbox_track_confirm_stuck_notes(struct cbox_track_playback *pb, struct cbox_midi_playback_active_notes *stuck_notes, uint32_t new_pos_ppqn) +{ + // Check if no notes are stuck + if (!stuck_notes->channels_active) + return; + uint32_t pos = 0; + while(pos < pb->items_count && pb->items[pos].time + pb->items[pos].length < new_pos_ppqn) + pos++; + if (pos >= pb->items_count) // past the end of the track - all notes are stuck + return; + const struct cbox_track_playback_item *tpi = &pb->items[pos]; + uint32_t rel_time_ppqn = new_pos_ppqn - tpi->time; + if (rel_time_ppqn < tpi->length) + { + // inside the clip + rel_time_ppqn += tpi->offset; + + for (unsigned c = 0; c < 16; c++) + { + if (!(stuck_notes->channels_active & (1 << c))) + continue; + + gboolean any_left = FALSE; + for (unsigned g = 0; g < 4; g++) + { + uint32_t group = stuck_notes->notes[c][g]; + if (!group) + continue; + for (unsigned i = 0; i < 32; i++) + { + if (!(group & (1 << i))) + continue; + uint8_t n = i + g * 32; + if (cbox_midi_pattern_playback_is_note_active_at(tpi->pattern, rel_time_ppqn, c, n)) + { + // That note is not stuck + group &= ~(1 << i); + } else { + // It is stuck, so keep the channel as containing stuck notes + any_left = TRUE; + } + } + stuck_notes->notes[c][g] = group; + } + if (!any_left) { + stuck_notes->channels_active &= ~(1 << c); + } + } + return; + } +} + +void cbox_track_playback_seek_ppqn(struct cbox_track_playback *pb, uint32_t time_ppqn, uint32_t min_time_ppqn) +{ + pb->pos = 0; + while(pb->pos < pb->items_count && pb->items[pb->pos].time + pb->items[pb->pos].length < time_ppqn) + pb->pos++; + cbox_track_playback_start_item(pb, time_ppqn, TRUE, min_time_ppqn); +} + +void cbox_track_playback_seek_samples(struct cbox_track_playback *pb, uint32_t time_samples) +{ + pb->pos = 0; + while(pb->pos < pb->items_count && cbox_master_ppqn_to_samples(pb->master, pb->items[pb->pos].time + pb->items[pb->pos].length) < time_samples) + pb->pos++; + if (pb->pos < pb->items_count) + { + int min_time_ppqn = cbox_master_samples_to_ppqn(pb->master, time_samples); + cbox_track_playback_start_item(pb, time_samples, FALSE, min_time_ppqn); + } +} + +void cbox_track_playback_start_item(struct cbox_track_playback *pb, int time, int is_ppqn, int min_time_ppqn) +{ + if (pb->pos >= pb->items_count) + { + return; + } + struct cbox_track_playback_item *cur = &pb->items[pb->pos]; + int time_samples, time_ppqn; + + if (is_ppqn) + { + time_ppqn = time; + time_samples = cbox_master_ppqn_to_samples(pb->master, time_ppqn); + } + else + { + time_samples = time; + time_ppqn = cbox_master_samples_to_ppqn(pb->master, time_samples); + } + int start_time_ppqn = cur->time, end_time_ppqn = cur->time + cur->length; + int start_time_samples = cbox_master_ppqn_to_samples(pb->master, start_time_ppqn); + int end_time_samples = cbox_master_ppqn_to_samples(pb->master, end_time_ppqn); + cbox_midi_clip_playback_set_pattern(&pb->playback, cur->pattern, start_time_samples, end_time_samples, cur->time, cur->offset); + + if (is_ppqn) + { + if (time_ppqn < start_time_ppqn) + cbox_midi_clip_playback_seek_ppqn(&pb->playback, 0, min_time_ppqn); + else + cbox_midi_clip_playback_seek_ppqn(&pb->playback, time_ppqn - start_time_ppqn, min_time_ppqn); + } + else + { + if (time_ppqn < start_time_ppqn) + cbox_midi_clip_playback_seek_samples(&pb->playback, 0, min_time_ppqn); + else + cbox_midi_clip_playback_seek_samples(&pb->playback, time_samples - start_time_samples, min_time_ppqn); + } +} + +void cbox_track_playback_render(struct cbox_track_playback *pb, uint32_t offset, uint32_t nsamples) +{ + struct cbox_song_playback *spb = pb->master->spb; + if (pb->mute) { + cbox_midi_playback_active_notes_release(&pb->active_notes, &pb->output_buffer, NULL); + } + uint32_t rpos = 0; + while(rpos < nsamples && pb->pos < pb->items_count) + { + uint32_t rend = nsamples; + struct cbox_track_playback_item *cur = &pb->items[pb->pos]; + // a gap before the current item + if (spb->song_pos_samples + rpos < pb->playback.start_time_samples) + { + uint32_t space_samples = pb->playback.start_time_samples - (spb->song_pos_samples + rpos); + if (space_samples >= rend - rpos) + return; + rpos += space_samples; + offset += space_samples; + } + // check if item finished + int cur_segment_end_samples = cbox_master_ppqn_to_samples(pb->master, cur->time + cur->length); + int render_end_samples = spb->song_pos_samples + rend; + if (render_end_samples > cur_segment_end_samples) + { + rend = cur_segment_end_samples - spb->song_pos_samples; + cbox_midi_clip_playback_render(&pb->playback, &pb->output_buffer, offset, rend - rpos, pb->mute); + pb->pos++; + cbox_track_playback_start_item(pb, cur_segment_end_samples, FALSE, FALSE); + } + else + cbox_midi_clip_playback_render(&pb->playback, &pb->output_buffer, offset, rend - rpos, pb->mute); + offset += rend - rpos; + rpos = rend; + } +} + +void cbox_track_playback_ref(struct cbox_track_playback *pb) +{ + ++pb->ref_count; +} + +void cbox_track_playback_destroy(struct cbox_track_playback *pb) +{ + if (pb->external_merger) + cbox_midi_merger_disconnect(pb->external_merger, &pb->output_buffer, pb->spb->engine->rt); + + for (uint32_t i = 0; i < pb->items_count; ++i) + cbox_midi_pattern_playback_unref(pb->items[i].pattern); + free(pb->items); + free(pb); +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static gint note_compare_fn(const void *p1, const void *p2, void *user_data) +{ + const struct cbox_midi_event *e1 = p1, *e2 = p2; + int cn1 = ((e1->data_inline[0] & 0x0F) << 8) | e1->data_inline[1]; + int cn2 = ((e2->data_inline[0] & 0x0F) << 8) | e2->data_inline[1]; + if (cn1 < cn2) + return -1; + if (cn2 < cn1) + return +1; + if (e1->time < e2->time) + return -1; + if (e1->time > e2->time) + return +1; + if (p1 < p2) + return -1; + if (p1 > p2) + return +1; + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_pattern_playback *cbox_midi_pattern_playback_new(struct cbox_midi_pattern *pattern) +{ + struct cbox_midi_pattern_playback *mppb = calloc(1, sizeof(struct cbox_midi_pattern_playback)); + mppb->events = malloc(sizeof(struct cbox_midi_event) * pattern->event_count); + memcpy(mppb->events, pattern->events, sizeof(struct cbox_midi_event) * pattern->event_count); + mppb->event_count = pattern->event_count; + mppb->ref_count = 1; + cbox_midi_playback_active_notes_init(&mppb->note_bitmask); + mppb->note_lookup = g_sequence_new(NULL); + for (uint32_t i = 0; i < mppb->event_count; ++i) { + struct cbox_midi_event *event = &mppb->events[i]; + if (event->size == 3 && (event->data_inline[0] & 0xE0) == 0x80) { + g_sequence_insert_sorted(mppb->note_lookup, event, note_compare_fn, NULL); + if (event->data_inline[0] >= 0x90) + accumulate_event(&mppb->note_bitmask, event); + } + } + + return mppb; +} + +void cbox_midi_pattern_playback_unref(struct cbox_midi_pattern_playback *mppb) +{ + if (!(--mppb->ref_count)) + cbox_midi_pattern_playback_destroy(mppb); +} + +void cbox_midi_pattern_playback_ref(struct cbox_midi_pattern_playback *mppb) +{ + ++mppb->ref_count; +} + +void cbox_midi_pattern_playback_destroy(struct cbox_midi_pattern_playback *mppb) +{ + g_sequence_free(mppb->note_lookup); + free(mppb->events); + free(mppb); +} + +gboolean cbox_midi_pattern_playback_is_note_active_at(struct cbox_midi_pattern_playback *mppb, uint32_t time_ppqn, uint32_t channel, uint32_t note) +{ + struct cbox_midi_event event; + event.time = time_ppqn; + event.size = 3; + event.data_inline[0] = 0x90 | channel; + event.data_inline[1] = note; + event.data_inline[2] = 127; + // printf("checking stuck note ch %d note %d at %d\n", channel, note, time_ppqn); + GSequenceIter *i = g_sequence_search(mppb->note_lookup, &event, note_compare_fn, NULL); + if (g_sequence_iter_is_begin(i)) // before first note + { + // printf("before first note\n"); + return FALSE; + } + i = g_sequence_iter_prev(i); + // A preceding note with the same channel and note number + struct cbox_midi_event *pevent = g_sequence_get(i); + // If it's an event for a different note, channel or not a note on event, then the note hasn't been active at the time + // XXXKF what about notes that start before clip offset? + if (pevent->size != 3 || pevent->data_inline[0] != event.data_inline[0] || pevent->data_inline[1] != event.data_inline[1] || !pevent->data_inline[2]) { + // printf("pevent wrong %d %d %d %d\n", pevent->time, pevent->data_inline[0], pevent->data_inline[1], pevent->data_inline[2]); + return FALSE; + } + + // printf("confirmed note ch %d note %d\n", channel, note); + return TRUE; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +void cbox_midi_clip_playback_init(struct cbox_midi_clip_playback *pb, struct cbox_midi_playback_active_notes *active_notes, struct cbox_master *master) +{ + pb->pattern = NULL; + pb->master = master; + pb->pos = 0; + pb->rel_time_samples = 0; + pb->start_time_samples = 0; + pb->end_time_samples = 0; + pb->active_notes = active_notes; + pb->min_time_ppqn = 0; + // cbox_midi_playback_active_notes_init(active_notes); +} + +void cbox_midi_clip_playback_set_pattern(struct cbox_midi_clip_playback *pb, struct cbox_midi_pattern_playback *pattern, int start_time_samples, int end_time_samples, int item_start_ppqn, int offset_ppqn) +{ + pb->pattern = pattern; + pb->pos = 0; + pb->rel_time_samples = 0; + pb->start_time_samples = start_time_samples; + pb->end_time_samples = end_time_samples; + pb->item_start_ppqn = item_start_ppqn; + pb->offset_ppqn = offset_ppqn; + pb->min_time_ppqn = offset_ppqn; +} + +void cbox_midi_clip_playback_render(struct cbox_midi_clip_playback *pb, struct cbox_midi_buffer *buf, uint32_t offset, uint32_t nsamples, gboolean mute) +{ + uint32_t end_time_samples = pb->end_time_samples; + uint32_t cur_time_samples = pb->start_time_samples + pb->rel_time_samples; + + if (end_time_samples > cur_time_samples + nsamples) + end_time_samples = cur_time_samples + nsamples; + + while(pb->pos < pb->pattern->event_count) + { + const struct cbox_midi_event *src = &pb->pattern->events[pb->pos]; + + if (src->time - pb->offset_ppqn + pb->item_start_ppqn >= pb->min_time_ppqn) + { + uint32_t event_time_samples = cbox_master_ppqn_to_samples(pb->master, src->time - pb->offset_ppqn + pb->item_start_ppqn); + + if (event_time_samples >= end_time_samples) + break; + int32_t time = 0; + if (event_time_samples >= cur_time_samples) // convert negative relative time to 0 time + time = event_time_samples - cur_time_samples; + + if (!mute) { + cbox_midi_buffer_copy_event(buf, src, offset + time); + if (pb->active_notes) + accumulate_event2(pb->active_notes, src); + } + } + pb->pos++; + } + pb->rel_time_samples += nsamples; +} + +void cbox_midi_clip_playback_seek_ppqn(struct cbox_midi_clip_playback *pb, uint32_t time_ppqn, uint32_t min_time_ppqn) +{ + uint32_t patrel_time_ppqn = time_ppqn + pb->offset_ppqn; + uint32_t L = 0, U = pb->pattern->event_count; + + if (patrel_time_ppqn > 0) { + while (U > L + 2) { + uint32_t M = (L >> 1) + (U >> 1) + (L & U & 1); + uint32_t time = pb->pattern->events[M].time; + if (time < patrel_time_ppqn) + L = M + 1; + else if (time >= patrel_time_ppqn) + U = M + 1; // this might still be the event we're looking for + } + } + + uint32_t pos = L; + while (pos < U && pb->pattern->events[pos].time < patrel_time_ppqn) + pos++; + pb->rel_time_samples = cbox_master_ppqn_to_samples(pb->master, pb->item_start_ppqn + time_ppqn) - pb->start_time_samples; + pb->min_time_ppqn = min_time_ppqn; + pb->pos = pos; +} + +void cbox_midi_clip_playback_seek_samples(struct cbox_midi_clip_playback *pb, uint32_t time_samples, uint32_t min_time_ppqn) +{ + uint32_t pos = 0; + while (pos < pb->pattern->event_count && time_samples > cbox_master_ppqn_to_samples(pb->master, pb->item_start_ppqn + pb->pattern->events[pos].time - pb->offset_ppqn)) + pos++; + pb->rel_time_samples = time_samples; + pb->min_time_ppqn = min_time_ppqn; + pb->pos = pos; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +void cbox_midi_playback_active_notes_init(struct cbox_midi_playback_active_notes *notes) +{ + notes->channels_active = 0; +} + +void cbox_midi_playback_active_notes_copy(struct cbox_midi_playback_active_notes *dest, const struct cbox_midi_playback_active_notes *src) +{ + dest->channels_active = src->channels_active; + memcpy(dest->notes, src->notes, sizeof(dest->notes)); +} + +int cbox_midi_playback_active_notes_release(struct cbox_midi_playback_active_notes *notes, struct cbox_midi_buffer *buf, struct cbox_midi_playback_active_notes *leftover_notes) +{ + if (!notes->channels_active) + return 0; + int note_offs = 0; + for (int c = 0; c < 16; c++) + { + if (!(notes->channels_active & (1 << c))) + continue; + + for (int g = 0; g < 4; g++) + { + uint32_t group = notes->notes[c][g]; + if (!group) + continue; + for (int i = 0; i < 32; i++) + { + int n = i + g * 32; + if (!(group & (1 << i))) + continue; + if (!cbox_midi_buffer_can_store_msg(buf, 3)) + return -1; + cbox_midi_buffer_write_inline(buf, cbox_midi_buffer_get_last_event_time(buf), 0x80 + c, n, 0); + group &= ~(1 << i); + notes->notes[c][g] = group; + if (leftover_notes) + leftover_notes->notes[c][g] &= ~(1 << i); + note_offs++; + } + } + // all Note Offs emitted without buffer overflow - channel is no longer active + notes->channels_active &= ~(1 << c); + } + return note_offs; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_song_playback *cbox_song_playback_new(struct cbox_song *song, struct cbox_master *master, struct cbox_engine *engine, struct cbox_song_playback *old_state) +{ + struct cbox_song_playback *spb = calloc(1, sizeof(struct cbox_song_playback)); + if (old_state && old_state->song != song) + old_state = NULL; + spb->song = song; + spb->engine = engine; + spb->pattern_map = g_hash_table_new(NULL, NULL); + spb->master = master; + spb->track_count = g_list_length(song->tracks); + spb->tracks = malloc(spb->track_count * sizeof(struct cbox_track_playback *)); + spb->song_pos_samples = 0; + spb->song_pos_ppqn = 0; + spb->min_time_ppqn = 0; + spb->loop_start_ppqn = song->loop_start_ppqn; + spb->loop_end_ppqn = song->loop_end_ppqn; + cbox_midi_merger_init(&spb->track_merger, NULL); + int pos = 0; + for (GList *p = song->tracks; p != NULL; p = g_list_next(p)) + { + struct cbox_track *trk = p->data; + struct cbox_track_playback *old_trk = NULL; + if (old_state && old_state->track_count) + { + for (uint32_t i = 0; i < old_state->track_count; i++) + { + if (cbox_uuid_equal(&old_state->tracks[i]->track_uuid, &CBOX_O2H(trk)->instance_uuid)) + { + old_trk = old_state->tracks[i]; + break; + } + } + } + if (old_trk && trk->generation == old_trk->generation) { + old_trk->state_copied = TRUE; + cbox_track_playback_ref(old_trk); + spb->tracks[pos++] = old_trk; + } + else { + if (old_trk) + old_trk->state_copied = FALSE; + spb->tracks[pos++] = cbox_track_playback_new_from_track(trk, spb->master, spb, old_trk); + } + if (!trk->external_output_set) + cbox_midi_merger_connect(&spb->track_merger, &spb->tracks[pos - 1]->output_buffer, NULL, NULL); + } + + spb->tempo_map_item_count = g_list_length(song->master_track_items); + spb->tempo_map_items = malloc(spb->tempo_map_item_count * sizeof(struct cbox_tempo_map_item)); + pos = 0; + int pos_ppqn = 0; + int pos_samples = 0; + double tempo = master->tempo; + int timesig_num = master->timesig_num; + int timesig_denom = master->timesig_denom; + struct cbox_bbt cur_bbt = {0, 0, 0, 0}; + for (GList *p = song->master_track_items; p != NULL; p = g_list_next(p)) + { + struct cbox_master_track_item *mti = p->data; + if (mti->tempo == 0 && mti->timesig_num == 0 && + mti->timesig_denom == 0 && p == song->master_track_items) { + spb->tempo_map_item_count--; + continue; + } + if (mti->tempo > 0) + tempo = mti->tempo; + if (mti->timesig_num > 0) + timesig_num = mti->timesig_num; + if (mti->timesig_denom > 0) + timesig_denom = mti->timesig_denom; + struct cbox_tempo_map_item *tmi = &spb->tempo_map_items[pos]; + tmi->time_ppqn = pos_ppqn; + tmi->time_samples = pos_samples; + tmi->tempo = tempo; + tmi->timesig_num = timesig_num; + tmi->timesig_denom = timesig_denom; + memcpy(&tmi->bbt, &cur_bbt, sizeof(cur_bbt)); + + cbox_bbt_add(&cur_bbt, mti->duration_ppqn, master->ppqn_factor, timesig_num, timesig_denom); + pos_ppqn += mti->duration_ppqn; + pos_samples += master->srate * 60.0 * mti->duration_ppqn / (tempo * master->ppqn_factor); + pos++; + } + return spb; +} + +void cbox_song_playback_apply_old_state(struct cbox_song_playback *spb) +{ + for (uint32_t i = 0; i < spb->track_count; i++) + { + struct cbox_track_playback *tpb = spb->tracks[i]; + tpb->spb = spb; + if (tpb->old_state) + { + cbox_midi_playback_active_notes_copy(&tpb->active_notes, &tpb->old_state->active_notes); + tpb->old_state->state_copied = TRUE; + tpb->old_state = NULL; + } + } +} + + +static void cbox_song_playback_set_tempo(struct cbox_song_playback *spb, double tempo) +{ + int ppos = spb->song_pos_ppqn; + int pos1 = cbox_master_ppqn_to_samples(spb->master, ppos); + int pos2 = cbox_master_ppqn_to_samples(spb->master, ppos + 1); + double relpos = 0.0; + if (pos1 != pos2) + relpos = (spb->song_pos_samples - pos1) * 1.0 / (pos2 - pos1); + spb->master->tempo = tempo; + + // This seek loses the fractional value of the PPQN song position. + // This needs to be compensated for by shifting the playback + // position by the fractional part. + cbox_song_playback_seek_ppqn(spb, ppos, spb->min_time_ppqn); + if (relpos > 0) + { + pos2 = cbox_master_ppqn_to_samples(spb->master, ppos + 1); + cbox_song_playback_seek_samples(spb, spb->song_pos_samples + (pos2 - spb->song_pos_samples) * relpos + 0.5); + } +} + +int cbox_song_playback_get_next_tempo_change(struct cbox_song_playback *spb) +{ + double new_tempo = 0; + // Skip items at or already past the playback pointer + while (spb->tempo_map_pos + 1 < spb->tempo_map_item_count && + spb->song_pos_samples >= spb->tempo_map_items[spb->tempo_map_pos + 1].time_samples) + { + new_tempo = spb->tempo_map_items[spb->tempo_map_pos + 1].tempo; + spb->tempo_map_pos++; + } + if (new_tempo != 0.0 && new_tempo != spb->master->tempo) { + cbox_song_playback_set_tempo(spb, new_tempo); + } + + // No more items? + if (spb->tempo_map_pos + 1 >= spb->tempo_map_item_count) + return -1; + + return spb->tempo_map_items[spb->tempo_map_pos + 1].time_samples; +} + +void cbox_song_playback_prepare_render(struct cbox_song_playback *spb) +{ + for(uint32_t i = 0; i < spb->track_count; i++) + { + cbox_midi_buffer_clear(&spb->tracks[i]->output_buffer); + } +} + +void cbox_song_playback_render(struct cbox_song_playback *spb, struct cbox_midi_buffer *output, uint32_t nsamples) +{ + cbox_midi_buffer_clear(output); + + if (spb->master->new_tempo != 0) + { + if (spb->master->new_tempo != spb->master->tempo) + cbox_song_playback_set_tempo(spb, spb->master->new_tempo); + spb->master->new_tempo = 0; + } + if (spb->master->state == CMTS_STOPPING) + { + if (cbox_song_playback_active_notes_release(spb, NULL, 0, output) > 0) + spb->master->state = CMTS_STOP; + } + else + if (spb->master->state == CMTS_ROLLING) + { + uint32_t end_samples = cbox_master_ppqn_to_samples(spb->master, spb->loop_end_ppqn); + + uint32_t rpos = 0; + while (rpos < nsamples) + { + uint32_t rend = nsamples; + + // 1. Shorten the period so that it doesn't go past a tempo change + int tmpos = cbox_song_playback_get_next_tempo_change(spb); + if (tmpos != -1) + { + // Number of samples until the next tempo change + uint32_t stntc = tmpos - spb->song_pos_samples; + if (rend - rpos > stntc) + rend = rpos + stntc; + } + + // 2. Shorten the period so that it doesn't go past the song length + uint32_t end_pos = spb->song_pos_samples + (rend - rpos); + if (end_pos >= end_samples) + { + rend = end_samples - spb->song_pos_samples; + end_pos = end_samples; + } + + if (rend > rpos) + { + for (uint32_t i = 0; i < spb->track_count; i++) + cbox_track_playback_render(spb->tracks[i], rpos, rend - rpos); + } + + if (end_pos < end_samples) + { + spb->song_pos_samples += rend - rpos; + // XXXKF optimize + spb->min_time_ppqn = cbox_master_samples_to_ppqn(spb->master, spb->song_pos_samples - 1) + 1; + spb->song_pos_ppqn = cbox_master_samples_to_ppqn(spb->master, spb->song_pos_samples); + } + else + { + if (spb->loop_start_ppqn >= spb->loop_end_ppqn) + { + spb->song_pos_samples = end_samples; + spb->song_pos_ppqn = spb->loop_end_ppqn; + spb->master->state = CMTS_STOPPING; + break; + } + + cbox_song_playback_seek_ppqn(spb, spb->loop_start_ppqn, spb->loop_start_ppqn); + } + rpos = rend; + } + cbox_midi_merger_render_to(&spb->track_merger, output); + } +} + +int cbox_song_playback_active_notes_release(struct cbox_song_playback *spb, struct cbox_song_playback *new_spb, uint32_t new_pos, struct cbox_midi_buffer *buf) +{ + // Release notes from deleted tracks + for(uint32_t i = 0; i < spb->track_count; i++) + { + struct cbox_track_playback *trk = spb->tracks[i]; + if (new_spb && trk->state_copied) + continue; + struct cbox_midi_buffer *output = trk->external_merger ? &trk->output_buffer : buf; + if (cbox_midi_playback_active_notes_release(&trk->active_notes, output, NULL) < 0) + return 0; + } + // Release notes from removed/modified clips + if (new_spb) { + for(uint32_t i = 0; i < new_spb->track_count; i++) + { + struct cbox_track_playback *new_trk = new_spb->tracks[i]; + if (!new_trk->active_notes.channels_active) + continue; + // struct cbox_track_playback *old_trk = new_trk->old_state; + // if (!old_trk) + // continue; + struct cbox_midi_buffer *output = new_trk->external_merger ? &new_trk->output_buffer : buf; + struct cbox_midi_playback_active_notes stuck_notes; + cbox_midi_playback_active_notes_copy(&stuck_notes, &new_trk->active_notes); + cbox_track_confirm_stuck_notes(new_trk, &stuck_notes, new_pos); + if (cbox_midi_playback_active_notes_release(&stuck_notes, output, &new_trk->active_notes) < 0) + return 0; + } + } + return 1; +} + +void cbox_song_playback_seek_ppqn(struct cbox_song_playback *spb, int time_ppqn, int min_time_ppqn) +{ + for(uint32_t i = 0; i < spb->track_count; i++) + { + struct cbox_track_playback *trk = spb->tracks[i]; + cbox_track_playback_seek_ppqn(trk, time_ppqn, min_time_ppqn); + } + spb->song_pos_samples = cbox_master_ppqn_to_samples(spb->master, time_ppqn); + spb->song_pos_ppqn = time_ppqn; + spb->min_time_ppqn = min_time_ppqn; + spb->tempo_map_pos = cbox_song_playback_tmi_from_ppqn(spb, time_ppqn); +} + +void cbox_song_playback_seek_samples(struct cbox_song_playback *spb, uint32_t time_samples) +{ + for(uint32_t i = 0; i < spb->track_count; i++) + { + struct cbox_track_playback *trk = spb->tracks[i]; + cbox_track_playback_seek_samples(trk, time_samples); + } + spb->song_pos_samples = time_samples; + spb->song_pos_ppqn = cbox_master_samples_to_ppqn(spb->master, time_samples); + spb->min_time_ppqn = spb->song_pos_ppqn; + spb->tempo_map_pos = cbox_song_playback_tmi_from_samples(spb, time_samples); +} + +int cbox_song_playback_tmi_from_ppqn(struct cbox_song_playback *spb, uint32_t time_ppqn) +{ + if (!spb->tempo_map_item_count) + return -1; + assert(spb->tempo_map_items[0].time_samples == 0); + assert(spb->tempo_map_items[0].time_ppqn == 0); + // XXXKF should use binary search here really + for (int i = 1; i < spb->tempo_map_item_count; i++) + { + if (time_ppqn < spb->tempo_map_items[i].time_ppqn) + return i - 1; + } + return spb->tempo_map_item_count - 1; +} + +int cbox_song_playback_tmi_from_samples(struct cbox_song_playback *spb, uint32_t time_samples) +{ + if (!spb->tempo_map_item_count) + return -1; + assert(spb->tempo_map_items[0].time_samples == 0); + assert(spb->tempo_map_items[0].time_ppqn == 0); + // XXXKF should use binary search here really + for (int i = 1; i < spb->tempo_map_item_count; i++) + { + if (time_samples < spb->tempo_map_items[i].time_samples) + return i - 1; + } + return spb->tempo_map_item_count - 1; +} + +struct cbox_midi_pattern_playback *cbox_song_playback_get_pattern(struct cbox_song_playback *spb, struct cbox_midi_pattern *pattern) +{ + struct cbox_midi_pattern_playback *mppb = g_hash_table_lookup(spb->pattern_map, pattern); + if (mppb) { + cbox_midi_pattern_playback_ref(mppb); + return mppb; + } + + mppb = cbox_midi_pattern_playback_new(pattern); + g_hash_table_insert(spb->pattern_map, pattern, mppb); + + return mppb; +} + +uint32_t cbox_song_playback_correct_for_looping(struct cbox_song_playback *spb, uint32_t abs_samples) +{ + struct cbox_engine *engine = spb->engine; + // This is rather expensive, the start/end of the loop expressed in samples should be cached. + uint32_t loop_end_samples = cbox_master_ppqn_to_samples(engine->master, spb->loop_end_ppqn); + if (abs_samples >= loop_end_samples) { + // Correct for looping + uint32_t loop_start_samples = cbox_master_ppqn_to_samples(engine->master, spb->loop_start_ppqn); + if (loop_start_samples < loop_end_samples) { + uint32_t loop_length = loop_end_samples - loop_start_samples; + abs_samples = loop_start_samples + (abs_samples - loop_start_samples) % loop_length; + } + } + return abs_samples; +} + +void cbox_song_playback_destroy(struct cbox_song_playback *spb) +{ + cbox_midi_merger_close(&spb->track_merger, spb->engine->rt); + for (uint32_t i = 0; i < spb->track_count; i++) + { + if (!(--spb->tracks[i]->ref_count)) + cbox_track_playback_destroy(spb->tracks[i]); + } + free(spb->tempo_map_items); + free(spb->tracks); + g_hash_table_destroy(spb->pattern_map); + free(spb); +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +uint32_t cbox_song_time_mapper_map_time(struct cbox_time_mapper *tmap, uint32_t free_running_counter) +{ + struct cbox_song_time_mapper *stmap = (struct cbox_song_time_mapper *)tmap; + struct cbox_engine *engine = stmap->engine; + + if (!engine->spb || engine->master->state != CMTS_ROLLING) + return free_running_counter & 0x7FFFFFFF; + + int32_t rel_samples = free_running_counter - engine->rt->io->free_running_frame_counter; + if (rel_samples < 0 || rel_samples >= 1048576) + return (uint32_t)-1; + uint32_t abs_samples = engine->spb->song_pos_samples + rel_samples; + abs_samples = cbox_song_playback_correct_for_looping(engine->spb, abs_samples); + uint32_t abs_ppqn = cbox_master_samples_to_ppqn(engine->master, abs_samples); + return abs_ppqn | 0x80000000; +} + +void cbox_song_time_mapper_init(struct cbox_song_time_mapper *tmap, struct cbox_engine *engine) +{ + tmap->tmap.map_time = cbox_song_time_mapper_map_time; + tmap->engine = engine; +} diff --git a/template/calfbox/seq.h b/template/calfbox/seq.h new file mode 100644 index 0000000..f8ca0de --- /dev/null +++ b/template/calfbox/seq.h @@ -0,0 +1,199 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SEQ_H +#define CBOX_SEQ_H + +#include + +#include "dom.h" +#include "midi.h" +#include "mididest.h" + +struct cbox_engine; +struct cbox_midi_pattern; +struct cbox_track; +struct cbox_song; + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_playback_active_notes +{ + uint16_t channels_active; + uint32_t notes[16][4]; // 0..127 +}; + +extern void cbox_midi_playback_active_notes_init(struct cbox_midi_playback_active_notes *notes); +extern void cbox_midi_playback_active_notes_copy(struct cbox_midi_playback_active_notes *dest, const struct cbox_midi_playback_active_notes *src); +extern void cbox_midi_playback_active_notes_clear(struct cbox_midi_playback_active_notes *notes); +extern int cbox_midi_playback_active_notes_release(struct cbox_midi_playback_active_notes *notes, struct cbox_midi_buffer *buf, struct cbox_midi_playback_active_notes *leftover_notes); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_pattern_playback +{ + struct cbox_midi_event *events; + uint32_t event_count; + int ref_count; + GSequence *note_lookup; + struct cbox_midi_playback_active_notes note_bitmask; +}; + +extern struct cbox_midi_pattern_playback *cbox_midi_pattern_playback_new(struct cbox_midi_pattern *pattern); +extern void cbox_midi_pattern_playback_ref(struct cbox_midi_pattern_playback *mppb); +extern void cbox_midi_pattern_playback_unref(struct cbox_midi_pattern_playback *mppb); +extern void cbox_midi_pattern_playback_destroy(struct cbox_midi_pattern_playback *mppb); +extern gboolean cbox_midi_pattern_playback_is_note_active_at(struct cbox_midi_pattern_playback *mppb, uint32_t time_ppqn, uint32_t channel, uint32_t note); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_midi_clip_playback +{ + struct cbox_midi_pattern_playback *pattern; + struct cbox_master *master; + + uint32_t pos; + int rel_time_samples; + // [start, end) of the pattern slice + uint32_t start_time_samples, end_time_samples; + uint32_t item_start_ppqn, min_time_ppqn; + int offset_ppqn; + struct cbox_midi_playback_active_notes *active_notes; +}; + +extern void cbox_midi_clip_playback_init(struct cbox_midi_clip_playback *pb, struct cbox_midi_playback_active_notes *active_notes, struct cbox_master *master); +extern void cbox_midi_clip_playback_render(struct cbox_midi_clip_playback *pb, struct cbox_midi_buffer *buf, uint32_t offset, uint32_t nsamples, gboolean mute); +extern void cbox_midi_clip_playback_seek_ppqn(struct cbox_midi_clip_playback *pb, uint32_t time_ppqn, uint32_t min_time_ppqn); +extern void cbox_midi_clip_playback_seek_samples(struct cbox_midi_clip_playback *pb, uint32_t time_samples, uint32_t min_time_ppqn); +extern void cbox_midi_clip_playback_set_pattern(struct cbox_midi_clip_playback *pb, struct cbox_midi_pattern_playback *pattern, int start_time_samples, int end_time_samples, int item_start_ppqn, int offset_ppqn); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +// all times in this structure are in PPQN +struct cbox_track_playback_item +{ + uint32_t time; + struct cbox_midi_pattern_playback *pattern; + uint32_t offset; + uint32_t length; + // in future, it should also contain a pre-calculated list of notes to release +}; + +struct cbox_track_playback +{ + struct cbox_uuid track_uuid; // used as identification only + struct cbox_track_playback_item *items; + struct cbox_master *master; + uint32_t items_count; + uint32_t pos; + uint32_t generation; // of the original track + int ref_count; + struct cbox_midi_buffer output_buffer; + struct cbox_midi_clip_playback playback; + struct cbox_midi_playback_active_notes active_notes; + struct cbox_midi_merger *external_merger; + struct cbox_song_playback *spb; + struct cbox_track_playback *old_state; + gboolean state_copied; + gboolean mute; +}; + +extern struct cbox_track_playback *cbox_track_playback_new_from_track(struct cbox_track *track, struct cbox_master *master, struct cbox_song_playback *spb, struct cbox_track_playback *old_state); +extern void cbox_track_playback_render(struct cbox_track_playback *pb, uint32_t offset, uint32_t nsamples); +extern void cbox_track_playback_seek_ppqn(struct cbox_track_playback *pb, uint32_t time_ppqn, uint32_t min_time_ppqn); +extern void cbox_track_playback_seek_samples(struct cbox_track_playback *pb, uint32_t time_samples); +extern void cbox_track_playback_start_item(struct cbox_track_playback *pb, int time, int is_ppqn, int skip_this_pos); +extern void cbox_track_confirm_stuck_notes(struct cbox_track_playback *pb, struct cbox_midi_playback_active_notes *stuck_notes, uint32_t new_pos); +extern void cbox_track_playback_destroy(struct cbox_track_playback *pb); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_tempo_map_item +{ + uint32_t time_ppqn; + uint32_t time_samples; + double tempo; + int timesig_num, timesig_denom; + + struct cbox_bbt bbt; +}; + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_adhoc_pattern +{ + struct cbox_adhoc_pattern *next; + + struct cbox_master *master; + struct cbox_midi_pattern *pattern; + struct cbox_midi_pattern_playback *pattern_playback; + + struct cbox_midi_playback_active_notes active_notes; + struct cbox_midi_clip_playback playback; + + struct cbox_midi_buffer output_buffer; + int id; + gboolean completed; +}; + +extern struct cbox_adhoc_pattern *cbox_adhoc_pattern_new(struct cbox_engine *engine, int id, struct cbox_midi_pattern *pattern); +extern void cbox_adhoc_pattern_render(struct cbox_adhoc_pattern *adp, uint32_t offset, uint32_t nsamples); +extern void cbox_adhoc_pattern_destroy(struct cbox_adhoc_pattern *ap); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_song_playback +{ + struct cbox_master *master; + struct cbox_song *song; // for identification only + struct cbox_track_playback **tracks; + uint32_t track_count; + struct cbox_tempo_map_item *tempo_map_items; + int tempo_map_item_count; + int tempo_map_pos; + uint32_t song_pos_samples, song_pos_ppqn, min_time_ppqn; + uint32_t loop_start_ppqn, loop_end_ppqn; + GHashTable *pattern_map; + struct cbox_midi_merger track_merger; + struct cbox_engine *engine; +}; + +extern struct cbox_song_playback *cbox_song_playback_new(struct cbox_song *song, struct cbox_master *master, struct cbox_engine *engine, struct cbox_song_playback *old_state); +extern void cbox_song_playback_prepare_render(struct cbox_song_playback *spb); +extern void cbox_song_playback_render(struct cbox_song_playback *spb, struct cbox_midi_buffer *output, uint32_t nsamples); +extern int cbox_song_playback_active_notes_release(struct cbox_song_playback *old_spb, struct cbox_song_playback *new_spb, uint32_t new_pos, struct cbox_midi_buffer *buf); +extern void cbox_song_playback_seek_ppqn(struct cbox_song_playback *spb, int time_ppqn, int skip_this_pos); +extern void cbox_song_playback_seek_samples(struct cbox_song_playback *spb, uint32_t time_samples); +extern int cbox_song_playback_tmi_from_ppqn(struct cbox_song_playback *spb, uint32_t time_ppqn); +extern int cbox_song_playback_tmi_from_samples(struct cbox_song_playback *spb, uint32_t time_samples); +struct cbox_midi_pattern_playback *cbox_song_playback_get_pattern(struct cbox_song_playback *spb, struct cbox_midi_pattern *pattern); +extern void cbox_song_playback_apply_old_state(struct cbox_song_playback *spb); +uint32_t cbox_song_playback_correct_for_looping(struct cbox_song_playback *spb, uint32_t abs_samples); +extern void cbox_song_playback_destroy(struct cbox_song_playback *spb); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_song_time_mapper +{ + struct cbox_time_mapper tmap; + struct cbox_engine *engine; +}; + +extern void cbox_song_time_mapper_init(struct cbox_song_time_mapper *tmap, struct cbox_engine *engine); + +#endif diff --git a/template/calfbox/setup.py b/template/calfbox/setup.py new file mode 100755 index 0000000..185aaa7 --- /dev/null +++ b/template/calfbox/setup.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +from distutils.core import setup, Extension +import glob +import os +import sys + +support_ext_module = False + +ext_modules = [] +if support_ext_module: + if sys.version_info[0] < 3: + raise Exception("Python 3 required.") + + packages = ['glib-2.0', 'sndfile'] + + if '#define USE_FLUIDSYNTH 1' in open('config.h').read(): + packages.append('fluidsynth') + if '#define USE_JACK 1' in open('config.h').read(): + packages.append('jack') + if '#define USE_LIBUSB 1' in open('config.h').read(): + packages.append('libusb-1.0') + if '#define USE_LIBSMF 1' in open('config.h').read(): + packages.append('smf') + + eargs = os.popen("pkg-config --cflags %s" % (" ".join(packages)), "r").read().split() + eargs.append("-std=c99") + # Workaround for Python3.4 headers + eargs.append("-Wno-error=declaration-after-statement") + + libs = os.popen("pkg-config --libs %s" % (" ".join(packages)), "r").read().split() + libs.append("-luuid") + + csources = [ + "app.c", + "auxbus.c", + "blob.c", + "@chorus.c", + "cmd.c", + "@compressor.c", + "config-api.c", + "@delay.c", + "@distortion.c", + "dom.c", + "eq.c", + "engine.c", + "errors.c", + "@fbr.c", + "fifo.c", + "@fluid.c", + "@fuzz.c", + "@fxchain.c", + "@gate.c", + "hwcfg.c", + "instr.c", + "io.c", + "@jackinput.c", + "@jackio.c", + "layer.c", + "@limiter.c", + "master.c", + "meter.c", + "midi.c", + "mididest.c", + "module.c", + "pattern.c", + "pattern-maker.c", + "@phaser.c", + "prefetch_pipe.c", + "recsrc.c", + "@reverb.c", + "rt.c", + "sampler.c", + "@sampler_channel.c", + "@sampler_gen.c", + "sampler_layer.c", + "@sampler_nif.c", + "@sampler_prevoice.c", + "sampler_prg.c", + "@sampler_rll.c", + "@sampler_voice.c", + "scene.c", + "scripting.c", + "seq.c", + "@seq-adhoc.c", + "sfzloader.c", + "sfzparser.c", + "song.c", + "@streamplay.c", + "@streamrec.c", + "tarfile.c", + "@tonectl.c", + "@tonewheel.c", + "track.c", + "@usbaudio.c", + "@usbio.c", + "@usbmidi.c", + "@usbprobe.c", + "wavebank.c", + ] + + headers = [ + "biquad-float.h", + "config.h", + "dspmath.h", + "envelope.h", + "ioenv.h", + "onepole-float.h", + "onepole-int.h", + "sampler_impl.h", + "stm.h", + "usbio_impl.h", + ] + + headers += [fn[:-2] + ".h" for fn in csources if fn.endswith(".c") and not fn.startswith("@")] + csources = [fn.lstrip("@") for fn in csources] + + if '#define USE_SSE 1' in open('config.h').read(): + eargs.append('-msse') + eargs.append('-ffast-math') + if '#define USE_NEON 1' in open('config.h').read(): + eargs.append('-mfloat-abi=hard') + eargs.append('-mfpu=neon') + eargs.append('-ffast-math') + + ext_modules.append([ + + Extension('_cbox', csources, + extra_compile_args = eargs, + include_dirs=['.'], + extra_link_args=libs, + define_macros=[("_GNU_SOURCE","1"),("_POSIX_C_SOURCE", "199309L"),("USE_PYTHON","1"),("CALFBOX_AS_MODULE", "1")], + undef_macros=['NDEBUG'], + depends = ['setup.py'] + headers + ) + ]) +setup(name="CalfBox", + version="0.0.0.2", description="Assorted music-related code", + author="Krzysztof Foltman", author_email="wdev@foltman.com", + url="https://github.com/kfoltman/calfbox", + packages=["calfbox"], + package_dir={'calfbox':'py'}, + ext_modules=ext_modules, +) diff --git a/template/calfbox/sfzloader.c b/template/calfbox/sfzloader.c new file mode 100644 index 0000000..8b64bbc --- /dev/null +++ b/template/calfbox/sfzloader.c @@ -0,0 +1,292 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "sampler.h" +#include "sfzparser.h" +#include "sampler_impl.h" + +#define DUMP_LAYER_ATTRIBS 0 + +enum sfz_load_section_type +{ + slst_normal, + slst_control, + slst_effect, + slst_curve, +}; + +struct sfz_load_state +{ + struct sampler_module *m; + const char *filename, *default_path; + struct sampler_program *program; + struct sampler_layer *global, *master, *group, *region, *target; + struct sampler_midi_curve *curve; + enum sfz_load_section_type section_type; + uint32_t curve_index; + GError **error; +}; + +static void load_sfz_end_region(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + // printf("-- copy current region to the list of layers\n"); + struct sampler_layer *l = ls->region; + sampler_layer_data_finalize(&l->data, l->parent ? &l->parent->data : NULL, ls->program); + sampler_layer_reset_switches(l, ls->m); + sampler_layer_update(l); + sampler_program_add_layer(ls->program, ls->region); + + ls->region = NULL; +} + +static void end_token(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; +#if DUMP_LAYER_ATTRIBS + if (ls->group) + { + fprintf(stdout, ""); + sampler_layer_dump(&ls->group, stdout); + } + if (ls->region) + { + fprintf(stdout, ""); + sampler_layer_dump(&ls->region, stdout); + } +#endif + if (ls->section_type == slst_curve) + { + uint32_t i = ls->curve_index; + if (i < MAX_MIDI_CURVES) + { + if (ls->program->curves[i]) + g_free(ls->program->curves[i]); + else + ls->program->interpolated_curves[i] = g_new(float, 128); + sampler_midi_curve_interpolate(ls->curve, ls->program->interpolated_curves[i], 0, 1, FALSE); + ls->program->curves[i] = ls->curve; + } + else + { + if (i == (uint32_t)-1) + g_warning("Curve index not specified"); + else + g_warning("Curve number %u is greater than the maximum of %u", (unsigned)i, (unsigned)MAX_MIDI_CURVES); + g_free(ls->curve); + } + ls->curve = NULL; + } + if (ls->region) + load_sfz_end_region(client); + ls->region = NULL; + ls->section_type = slst_normal; +} + +static gboolean load_sfz_global(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + // printf("-- start global\n"); + ls->target = ls->global = ls->program->global; + ls->master = ls->global->default_child; + ls->group = ls->master->default_child; + return TRUE; +} + +static gboolean load_sfz_master(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + // printf("-- start master\n"); + ls->target = ls->master = sampler_layer_new(ls->m, ls->program, ls->program->global); + ls->group = ls->master->default_child = sampler_layer_new(ls->m, ls->program, ls->master); + return TRUE; +} + +static gboolean load_sfz_group(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + // printf("-- start group\n"); + ls->target = ls->group = sampler_layer_new(ls->m, ls->program, ls->master); + return TRUE; +} + +static gboolean load_sfz_region(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + + ls->target = ls->region = sampler_layer_new(ls->m, ls->program, ls->group); + // g_warning("-- start region"); + return TRUE; +} + +static gboolean load_sfz_control(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + ls->section_type = slst_control; + return TRUE; +} + +static gboolean load_sfz_curve(struct sfz_parser_client *client) +{ + struct sfz_load_state *ls = client->user_data; + ls->section_type = slst_curve; + ls->curve = g_new0(struct sampler_midi_curve, 1); + ls->curve_index = -1; + sampler_midi_curve_init(ls->curve); + return TRUE; +} + +static gboolean load_sfz_key_value(struct sfz_parser_client *client, const char *key, const char *value) +{ + struct sfz_load_state *ls = client->user_data; + + if (ls->section_type == slst_curve) + { + if (key[0] == 'v' && isdigit(key[1])) + { + int pos = atoi(key + 1); + if (pos >= 0 && pos < 128) + { + double fvalue = -1; + if (!atof_C_verify(key, value, &fvalue, ls->error)) + return FALSE; + ls->curve->values[pos] = fvalue; + return TRUE; + } + else + g_warning("Out of range curve point: %s", key); + } + else if (!strcmp(key, "curve_index")) + { + ls->curve_index = atoi(value); + return TRUE; + } + else + g_warning("Unknown parameter in curve section: %s=%s", key, value); + return TRUE; + } + if (ls->section_type == slst_effect) + { + g_warning("Parameter found in unsupported effect section: %s=%s", key, value); + return TRUE; + } + if (ls->section_type == slst_control) + { + if (!strncmp(key, "label_cc", 8)) + { + int ctrl = atoi(key + 8); + sampler_program_add_controller_label(ls->program, ctrl, g_strdup(value)); + } + else if (!strncmp(key, "set_cc", 6)) + { + int ctrl = atoi(key + 6); + int val = atoi(value); + if (ctrl >= 0 && ctrl < CC_COUNT && val >=0 && val <= 127) + sampler_program_add_controller_init(ls->program, ctrl, val); + else + g_warning("Invalid CC initialisation: %s=%s", key, value); + } + else if (!strcmp(key, "default_path")) + { + g_free(ls->program->sample_dir); + gchar *dir = g_path_get_dirname(ls->filename); + char value2[strlen(value) + 1]; + int i; + for (i = 0; value[i]; ++i) + value2[i] = value[i] == '\\' ? '/' : value[i]; + value2[i] = '\0'; + gchar *combined = g_build_filename(dir, value2, NULL); + ls->program->sample_dir = combined; + g_free(dir); + } + else + g_warning("Unrecognized SFZ key in control section: %s", key); + return TRUE; + } + + struct sampler_layer *l = ls->target; + if (!ls->target) + { + g_warning("Parameter '%s' entered outside of global, master, region or group", key); + return TRUE; + } + + if (!sampler_layer_apply_param(l, key, value, ls->error)) + return FALSE; + + return TRUE; +} + +static gboolean handle_token(struct sfz_parser_client *client, const char *token, GError **error) +{ + struct sfz_load_state *ls = client->user_data; + end_token(client); + + if (!strcmp(token, "region")) + return load_sfz_region(client); + + if (!strcmp(token, "group")) + return load_sfz_group(client); + + if (!strcmp(token, "master")) + return load_sfz_master(client); + + if (!strcmp(token, "global")) + return load_sfz_global(client); + + if (!strcmp(token, "control")) + return load_sfz_control(client); + + if (!strcmp(token, "curve")) + return load_sfz_curve(client); + + if (!strcmp(token, "effect")) + { + ls->section_type = slst_effect; + return TRUE; + } + + g_set_error(error, CBOX_SFZPARSER_ERROR, CBOX_SFZ_PARSER_ERROR_INVALID_HEADER, "Unexpected header <%s>", token); + return FALSE; +} + +gboolean sampler_module_load_program_sfz(struct sampler_module *m, struct sampler_program *prg, const char *sfz, int is_from_string, GError **error) +{ + struct sfz_load_state ls = { .global = prg->global, .master = prg->global->default_child, .group = prg->global->default_child->default_child, .target = NULL, .m = m, .filename = sfz, .region = NULL, .error = error, .program = prg, .section_type = slst_normal, .default_path = NULL }; + struct sfz_parser_client c = { .user_data = &ls, .token = handle_token, .key_value = load_sfz_key_value }; + g_clear_error(error); + + gboolean status; + if (is_from_string) + status = load_sfz_from_string(sfz, strlen(sfz), &c, error); + else + status = load_sfz(sfz, prg->tarfile, &c, error); + if (!status) + { + if (ls.region) + CBOX_DELETE(ls.region); + return FALSE; + } + + end_token(&c); + + prg->all_layers = g_slist_reverse(prg->all_layers); + sampler_program_update_layers(prg); + return TRUE; +} + diff --git a/template/calfbox/sfzloader.h b/template/calfbox/sfzloader.h new file mode 100644 index 0000000..4dfa47e --- /dev/null +++ b/template/calfbox/sfzloader.h @@ -0,0 +1,26 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SFZLOADER_H +#define CBOX_SFZLOADER_H + +#include "sampler.h" + +gboolean sampler_module_load_program_sfz(struct sampler_module *m, struct sampler_program *prg, const char *sfz, int is_from_string, GError **error); + +#endif diff --git a/template/calfbox/sfzparser.c b/template/calfbox/sfzparser.c new file mode 100644 index 0000000..7515ee2 --- /dev/null +++ b/template/calfbox/sfzparser.c @@ -0,0 +1,523 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "sfzparser.h" +#include "tarfile.h" +#include +#include +#include +#include +#include + +int debug_variable_definitions = 0; +int debug_variable_substitutions = 0; +int debug_includes = 0; + +struct sfz_parser_state +{ + struct sfz_parser_client *client; + gboolean (*handler)(struct sfz_parser_state *state, int ch); + const char *filename; + const char *buf; + int pos, len, line; + int token_start; + int key_start, key_end; + int value_start, value_end; + GHashTable *variables; + struct cbox_tarfile *tarfile; + GError **error; +}; + +static gboolean load_sfz_into_state(struct sfz_parser_state *s, const char *name); +static gboolean handle_char(struct sfz_parser_state *state, int ch); + +static void unexpected_char(struct sfz_parser_state *state, int ch) +{ + g_set_error(state->error, CBOX_SFZPARSER_ERROR, CBOX_SFZ_PARSER_ERROR_INVALID_CHAR, "Unexpected character '%c' (%d)", ch, ch); +} + +static gboolean handle_header(struct sfz_parser_state *state, int ch) +{ + if (ch >= 'a' && ch <= 'z') + return TRUE; + if (ch == '>') + { + char *token = g_strndup(state->buf + state->token_start, state->pos - 1 - state->token_start); + gboolean result = state->client->token(state->client, token, state->error); + g_free(token); + state->handler = handle_char; + return result; + } + unexpected_char(state, ch); + return FALSE; +} + +static void scan_for_value(struct sfz_parser_state *state) +{ + state->value_start = state->pos; + while(state->pos < state->len) + { + if (state->pos < state->len + 2 && state->buf[state->pos] == '/' && state->buf[state->pos + 1] == '/') + { + state->value_end = state->pos; + while(state->value_end > state->value_start && isspace(state->buf[state->value_end - 1])) + state->value_end--; + state->pos += 2; + while (state->pos < state->len && state->buf[state->pos] != '\r' && state->buf[state->pos] != '\n') + state->pos++; + return; + } + int ch = state->buf[state->pos]; + if (ch == 0 || ch == '\r' || ch == '\n' || ch == '<') + { + state->value_end = state->pos; + // remove spaces before next key + while(state->value_end > state->value_start && isspace(state->buf[state->value_end - 1])) + state->value_end--; + return; + } + if (ch == '=') + { + state->value_end = state->pos; + // remove next key + while(state->value_end > state->value_start && !isspace(state->buf[state->value_end - 1])) + state->value_end--; + // remove spaces before next key + while(state->value_end > state->value_start && isspace(state->buf[state->value_end - 1])) + state->value_end--; + state->pos = state->value_end; + return; + } + state->pos++; + } + state->value_end = state->pos; + while(state->value_end > state->value_start && isspace(state->buf[state->value_end - 1])) + state->value_end--; +} + +static gchar *expand_variables(struct sfz_parser_state *state, gchar *text) +{ + gchar *pos = strchr(text, '$'); + // No variables, no changes. + if (!pos) + return text; + + GString *result = g_string_new_len(text, pos - text); + pos++; + // Start of the variable name + gchar *start = pos; + if (!*start) + return text; + pos = start + 1; + while(1) + { + gchar ch = *pos; + gpointer value; + if (ch) + { + *pos = '\0'; + value = g_hash_table_lookup(state->variables, start); + *pos = ch; + } + else + value = g_hash_table_lookup(state->variables, start); + if (value) + { + g_string_append(result, value); + // pos = first char that is not part of the variable name + + if (!ch) + break; + start = strchr(pos, '$'); + if (!start) + { + // Remainder has no variable references, add and stop + g_string_append(result, pos); + break; + } + // Add everything up to the next variable reference + if (start != pos) + g_string_append_len(result, pos, start - pos); + // Restart, variable name starts at the next character + start++; + if (!*start) + break; + pos = start + 1; + } + else + { + if (!ch) + { + // Might throw an error here, but just quote the var name verbatim instead for now + g_string_append(result, "$"); + g_string_append(result, start); + break; + } + pos++; + } + } + // Replace with a substituted version + gchar *substituted = g_string_free(result, FALSE); + if (debug_variable_substitutions) + printf("Substitute: '%s' -> '%s'\n", text, substituted); + g_free(text); + return substituted; +} + +static gboolean handle_key(struct sfz_parser_state *state, int ch) +{ + if (isalpha(ch) || isdigit(ch) || ch == '_') + return TRUE; + if(ch == '=') + { + state->key_end = state->pos - 1; + scan_for_value(state); + + gchar *key = g_strndup(state->buf + state->key_start, state->key_end - state->key_start); + gchar *value = g_strndup(state->buf + state->value_start, state->value_end - state->value_start); + key = expand_variables(state, key); + value = expand_variables(state, value); + gboolean result = state->client->key_value(state->client, key, value); + g_free(key); + g_free(value); + state->handler = handle_char; + return result; + } + unexpected_char(state, ch); + return FALSE; +} + +static gboolean do_include(struct sfz_parser_state *state, const char *name) +{ + if (debug_includes) + printf("Include file: %s\n", name); + gchar *dir = g_path_get_dirname(state->filename); + gchar *combined = g_build_filename(dir, name, NULL); + gboolean result = load_sfz_into_state(state, combined); + g_free(combined); + g_free(dir); + if (debug_includes) + printf("End include file: %s\n", name); + return result; +} + +static void do_define(struct sfz_parser_state *state, char *key, char *value) +{ + if (debug_variable_definitions) + printf("Define: '%s' -> '%s'\n", key, value); + g_hash_table_insert(state->variables, key, value); +} + +static gboolean handle_include_filename(struct sfz_parser_state *state, int ch) +{ + if (ch == '"') + { + char *token = g_strndup(state->buf + state->token_start, state->pos - 1 - state->token_start); + gboolean result = do_include(state, token); + g_free(token); + state->handler = handle_char; + return result; + } + if ((unsigned)(ch) >= ' ') + return TRUE; + unexpected_char(state, ch); + return FALSE; +} + +static gboolean handle_include_skipwhite(struct sfz_parser_state *state, int ch) +{ + if (isspace(ch)) + return TRUE; + if (ch == '"') + { + state->token_start = state->pos; + state->handler = handle_include_filename; + return TRUE; + } + unexpected_char(state, ch); + return FALSE; +} + +static gboolean handle_define_value(struct sfz_parser_state *state, int ch) +{ + if (ch == '\n' || ch == '\r' || ch == -1) + { + char *key = g_strndup(state->buf + state->key_start, state->key_end - state->key_start); + char *value = g_strndup(state->buf + state->value_start, state->pos - state->value_start - 1); + do_define(state, key, value); + state->handler = handle_char; + return TRUE; + } + return TRUE; +} + +static gboolean handle_define_skipwhite2(struct sfz_parser_state *state, int ch) +{ + if (ch == '\n' || ch == '\r' || ch == -1) + { + char *key = g_strndup(state->buf + state->key_start, state->key_end - state->key_start); + g_set_error(state->error, CBOX_SFZPARSER_ERROR, CBOX_SFZ_PARSER_ERROR_INVALID_CHAR, "Unspecified variable value for '%s'", key); + g_free(key); + return FALSE; + } + if (isspace(ch)) + return TRUE; + state->value_start = state->pos - 1; + state->handler = handle_define_value; + return TRUE; +} + +static gboolean handle_define_varname(struct sfz_parser_state *state, int ch) +{ + if (ch == '\n' || ch == '\r' || ch == -1) + { + char *key = g_strndup(state->buf + state->key_start, state->key_end - state->key_start); + g_set_error(state->error, CBOX_SFZPARSER_ERROR, CBOX_SFZ_PARSER_ERROR_INVALID_CHAR, "Unspecified variable name"); + g_free(key); + return FALSE; + } + if (isspace(ch)) + { + state->key_end = state->pos - 1; + state->handler = handle_define_skipwhite2; + return TRUE; + } + if (ch >= 33 && ch <= 127 && ch != '$') + return TRUE; + unexpected_char(state, ch); + return FALSE; +} + +static gboolean handle_define_skipwhite(struct sfz_parser_state *state, int ch) +{ + if (isspace(ch)) + return TRUE; + if (ch == '$') + { + state->key_start = state->pos; + state->handler = handle_define_varname; + return TRUE; + } + + unexpected_char(state, ch); + return FALSE; +} + +static gboolean handle_preprocessor(struct sfz_parser_state *state, int ch) +{ + if (isalpha(ch)) + return TRUE; + if (isspace(ch)) + { + if (!memcmp(state->buf + state->token_start, "include", state->pos - state->token_start - 1)) + { + state->handler = handle_include_skipwhite; + return TRUE; + } + if (!memcmp(state->buf + state->token_start, "define", state->pos - state->token_start - 1)) + { + state->handler = handle_define_skipwhite; + return TRUE; + } + char *preproc = g_strndup(state->buf + state->token_start, state->pos - state->token_start - 1); + g_set_error(state->error, CBOX_SFZPARSER_ERROR, CBOX_SFZ_PARSER_ERROR_INVALID_CHAR, "%s:%d Unsupported parser directive '%s'", state->filename, state->line, preproc); + g_free(preproc); + state->handler = handle_char; + return FALSE; + } + unexpected_char(state, ch); + return FALSE; +} + +static gboolean handle_char(struct sfz_parser_state *state, int ch) +{ + if (isalpha(ch) || isdigit(ch)) + { + state->key_start = state->pos - 1; + state->handler = handle_key; + return TRUE; + } + switch(ch) + { + case '_': + return TRUE; + + case '\r': + case '\n': + case ' ': + case '\t': + case -1: + return TRUE; + case '<': + state->token_start = state->pos; + state->handler = handle_header; + return TRUE; + case '#': + state->token_start = state->pos; + state->handler = handle_preprocessor; + return TRUE; + default: + unexpected_char(state, ch); + return FALSE; + } +} + +gboolean load_sfz_from_string_into_state(struct sfz_parser_state *s, const char *buf, int len) +{ + const char *oldbuf = s->buf; + int oldpos = s->pos, oldlen = s->len, oldline = s->line; + gboolean ok = FALSE; + s->buf = buf; + s->pos = 0; + s->len = len; + s->handler = handle_char; + s->token_start = 0; + if (len >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) + { + // UTF-8 BOM + s->pos += 3; + } + while(s->pos < len && s->handler != NULL) + { + if (s->pos < len + 2 && buf[s->pos] == '/' && buf[s->pos + 1] == '/') + { + s->pos += 2; + while (s->pos < len && buf[s->pos] != '\r' && buf[s->pos] != '\n') + s->pos++; + continue; + } + char ch = buf[s->pos++]; + gboolean newline = FALSE, eat_lf = FALSE; + // Convert CR or CR/LF to LF + if (ch == '\r') { + newline = TRUE; + eat_lf = (buf[s->pos] == '\n'); + ch = '\n'; + } else if (ch == '\n') + newline = TRUE; + + if (!(*s->handler)(s, ch)) + goto restore; + if (newline) + s->line++; + if (eat_lf) + s->pos++; + } + if (s->handler) + { + if (!(*s->handler)(s, -1)) + goto restore; + } + ok = TRUE; +restore: + s->buf = oldbuf; + s->pos = oldpos; + s->line = oldline; + s->len = oldlen; + s->handler = handle_char; + s->token_start = oldpos; + return ok; +} + +gboolean load_sfz_from_string(const char *buf, int len, struct sfz_parser_client *c, GError **error) +{ + struct sfz_parser_state s; + memset(&s, 0, sizeof(s)); + s.line = 1; + s.filename = ""; + s.tarfile = NULL; + s.client = c; + s.error = error; + s.variables = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + gboolean result = load_sfz_from_string_into_state(&s, buf, len); + g_hash_table_destroy(s.variables); + return result; +} + +gboolean load_sfz_into_state(struct sfz_parser_state *s, const char *name) +{ + g_clear_error(s->error); + FILE *f; + int len = -1; + if (s->tarfile) + { + struct cbox_taritem *item = cbox_tarfile_get_item_by_name(s->tarfile, name, TRUE); + if (!item) + { + g_set_error(s->error, G_FILE_ERROR, g_file_error_from_errno (2), "Cannot find '%s' in the tarfile", name); + return FALSE; + } + int fd = cbox_tarfile_openitem(s->tarfile, item); + if (fd < 0) + { + g_set_error(s->error, G_FILE_ERROR, g_file_error_from_errno (errno), "Cannot open '%s' in the tarfile", name); + return FALSE; + } + f = fdopen(fd, "rb"); + len = item->size; + } + else + f = fopen(name, "rb"); + + if (!f) + { + g_set_error(s->error, G_FILE_ERROR, g_file_error_from_errno (errno), "Cannot open '%s'", name); + return FALSE; + } + + if (len == -1) + { + fseek(f, 0, SEEK_END); + len = ftell(f); + fseek(f, 0, SEEK_SET); + } + + unsigned char *buf = malloc(len + 1); + buf[len] = '\0'; + if (fread(buf, 1, len, f) != (size_t)len) + { + g_set_error(s->error, G_FILE_ERROR, g_file_error_from_errno (errno), "Cannot read '%s'", name); + fclose(f); + return FALSE; + } + fclose(f); + + gboolean result = load_sfz_from_string_into_state(s, (char *)buf, len); + free(buf); + return result; +} + +gboolean load_sfz(const char *name, struct cbox_tarfile *tarfile, struct sfz_parser_client *c, GError **error) +{ + struct sfz_parser_state s; + memset(&s, 0, sizeof(s)); + s.line = 1; + s.filename = name; + s.tarfile = tarfile; + s.client = c; + s.error = error; + s.variables = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + gboolean result = load_sfz_into_state(&s, name); + g_hash_table_destroy(s.variables); + return result; +} + +GQuark cbox_sfz_parser_error_quark(void) +{ + return g_quark_from_string("cbox-sfz-parser-error-quark"); +} diff --git a/template/calfbox/sfzparser.h b/template/calfbox/sfzparser.h new file mode 100644 index 0000000..a76f829 --- /dev/null +++ b/template/calfbox/sfzparser.h @@ -0,0 +1,47 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SFZPARSER_H +#define CBOX_SFZPARSER_H + +#include + +#define CBOX_SFZPARSER_ERROR cbox_sfz_parser_error_quark() + +enum CboxSfzParserError +{ + CBOX_SFZ_PARSER_ERROR_FAILED, + CBOX_SFZ_PARSER_ERROR_INVALID_CHAR, + CBOX_SFZ_PARSER_ERROR_INVALID_HEADER, +}; + +struct cbox_tarfile; + +struct sfz_parser_client +{ + void *user_data; + gboolean (*token)(struct sfz_parser_client *client, const char *token, GError **error); + gboolean (*key_value)(struct sfz_parser_client *client, const char *key, const char *value); +}; + +extern gboolean load_sfz(const char *name, struct cbox_tarfile *tarfile, struct sfz_parser_client *c, GError **error); +extern gboolean load_sfz_from_string(const char *buf, int len, struct sfz_parser_client *c, GError **error); + +extern GQuark cbox_sfz_parser_error_quark(void); + +#endif diff --git a/template/calfbox/skel.c b/template/calfbox/skel.c new file mode 100644 index 0000000..572d7df --- /dev/null +++ b/template/calfbox/skel.c @@ -0,0 +1,105 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS {name}_params + +struct {name}_params +{ +}; + +struct {name}_module +{ + struct cbox_module module; + + struct {name}_params *params, *old_params; +}; + +gboolean {name}_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct {name}_module *m = (struct {name}_module *)ct->user_data; + + // EFFECT_PARAM("/wet_dry", "f", wet_dry, double, , 0, 1) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + // return cbox_execute_on(fb, NULL, "/wet_dry", "f", error, m->params->wet_dry); + return CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void {name}_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + struct {name}_module *m = module->user_data; +} + +void {name}_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct {name}_module *m = module->user_data; + + if (m->params != m->old_params) + { + // update calculated values + } +} + +MODULE_SIMPLE_DESTROY_FUNCTION({name}) + +MODULE_CREATE_FUNCTION({name}) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct {name}_module *m = malloc(sizeof(struct {name}_module)); + CALL_MODULE_INIT(m, 0, 2, {name}); + m->module.process_event = {name}_process_event; + m->module.process_block = {name}_process_block; + struct {name}_params *p = malloc(sizeof(struct {name}_params)); + m->params = p; + m->old_params = NULL; + + return &m->module; +} + + +struct cbox_module_keyrange_metadata {name}_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata {name}_controllers[] = { +}; + +DEFINE_MODULE({name}, 0, 2) + diff --git a/template/calfbox/song.c b/template/calfbox/song.c new file mode 100644 index 0000000..d8b0978 --- /dev/null +++ b/template/calfbox/song.c @@ -0,0 +1,357 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "app.h" +#include "engine.h" +#include "errors.h" +#include "song.h" +#include "track.h" +#include +#include + +CBOX_CLASS_DEFINITION_ROOT(cbox_song) + +///////////////////////////////////////////////////////////////////////////////////////////////// + +void cbox_master_track_item_destroy(struct cbox_master_track_item *item) +{ + free(item); +} + +///////////////////////////////////////////////////////////////////////////////////////////////// + +gboolean cbox_song_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_song *song = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + for(GList *p = song->tracks; p; p = g_list_next(p)) + { + struct cbox_track *trk = p->data; + if (!cbox_execute_on(fb, NULL, "/track", "sio", error, trk->name, g_list_length(trk->items), trk)) + return FALSE; + } + for(GList *p = song->patterns; p; p = g_list_next(p)) + { + struct cbox_midi_pattern *pat = p->data; + if (!cbox_execute_on(fb, NULL, "/pattern", "sio", error, pat->name, pat->loop_end, pat)) + return FALSE; + } + uint32_t pos = 0; + for(GList *p = song->master_track_items; p; p = g_list_next(p)) + { + struct cbox_master_track_item *mti = p->data; + // Omit dummy item at 0 position. + if (pos || (mti->timesig_num && mti->timesig_denom) || mti->tempo) + { + if (!cbox_execute_on(fb, NULL, "/mti", "ifii", error, pos, mti->tempo, mti->timesig_num, mti->timesig_denom)) + return FALSE; + } + pos += mti->duration_ppqn; + } + return cbox_execute_on(fb, NULL, "/loop_start", "i", error, (int)song->loop_start_ppqn) && + cbox_execute_on(fb, NULL, "/loop_end", "i", error, (int)song->loop_end_ppqn) && + CBOX_OBJECT_DEFAULT_STATUS(song, fb, error); + } + else + if (!strcmp(cmd->command, "/set_loop") && !strcmp(cmd->arg_types, "ii")) + { + song->loop_start_ppqn = CBOX_ARG_I(cmd, 0); + song->loop_end_ppqn = CBOX_ARG_I(cmd, 1); + return TRUE; + } + else + if (!strcmp(cmd->command, "/set_mti") && !strcmp(cmd->arg_types, "ifii")) + { + cbox_song_set_mti(song, CBOX_ARG_I(cmd, 0), CBOX_ARG_F(cmd, 1), CBOX_ARG_I(cmd, 2), CBOX_ARG_I(cmd, 3)); + return TRUE; + } + else + if (!strcmp(cmd->command, "/clear") && !strcmp(cmd->arg_types, "")) + { + cbox_song_clear(song); + return TRUE; + } + else + if (!strcmp(cmd->command, "/add_track") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_track *track = cbox_track_new(CBOX_GET_DOCUMENT(song)); + cbox_song_add_track(song, track); + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, track)) + { + CBOX_DELETE(track); + return FALSE; + } + + return TRUE; + } + else + if (!strcmp(cmd->command, "/load_pattern") && !strcmp(cmd->arg_types, "si")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_midi_pattern *pattern = cbox_midi_pattern_load(song, CBOX_ARG_S(cmd, 0), CBOX_ARG_I(cmd, 1), app.engine->master->ppqn_factor); + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern)) + { + CBOX_DELETE(pattern); + return FALSE; + } + + return TRUE; + } + else + if (!strcmp(cmd->command, "/load_track") && !strcmp(cmd->arg_types, "si")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_midi_pattern *pattern = cbox_midi_pattern_load_track(song, CBOX_ARG_S(cmd, 0), CBOX_ARG_I(cmd, 1), app.engine->master->ppqn_factor); + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern)) + { + CBOX_DELETE(pattern); + return FALSE; + } + + return TRUE; + } + else + if (!strcmp(cmd->command, "/load_metronome") && !strcmp(cmd->arg_types, "i")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_midi_pattern *pattern = cbox_midi_pattern_new_metronome(song, CBOX_ARG_I(cmd, 0), app.engine->master->ppqn_factor); + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern)) + { + CBOX_DELETE(pattern); + return FALSE; + } + + return TRUE; + } + else + if (!strcmp(cmd->command, "/load_blob") && !strcmp(cmd->arg_types, "bi")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct cbox_midi_pattern *pattern = cbox_midi_pattern_new_from_blob(song, CBOX_ARG_B(cmd, 0), CBOX_ARG_I(cmd, 1), app.engine->master->ppqn_factor); + if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern)) + { + CBOX_DELETE(pattern); + return FALSE; + } + + return TRUE; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + + +///////////////////////////////////////////////////////////////////////////////////////////////// + +struct cbox_song *cbox_song_new(struct cbox_document *document) +{ + struct cbox_song *p = calloc(1, sizeof(struct cbox_song)); + CBOX_OBJECT_HEADER_INIT(p, cbox_song, document); + + // Create the first, dummy tempo map item + struct cbox_master_track_item *mti = calloc(1, sizeof(struct cbox_master_track_item)); + mti->timesig_num = 0; + mti->timesig_denom = 0; + mti->tempo = 0; + mti->duration_ppqn = 0; + + p->master_track_items = g_list_append(NULL, mti); + p->tracks = NULL; + p->patterns = NULL; + p->lyrics_sheet = NULL; + p->chord_sheet = NULL; + p->loop_start_ppqn = 0; + p->loop_end_ppqn = 0; + cbox_command_target_init(&p->cmd_target, cbox_song_process_cmd, p); + CBOX_OBJECT_REGISTER(p); + + return p; +} + +void cbox_song_set_mti(struct cbox_song *song, uint32_t pos, double tempo, int timesig_num, int timesig_denom) +{ + uint32_t tstart = 0, tend = 0; + GList *prev = NULL; + // A full no-op + if (tempo < 0 && timesig_num < 0) + return; + gboolean is_noop = tempo == 0 && timesig_num == 0; + + struct cbox_master_track_item *mti = NULL; + for(GList *p = song->master_track_items; p; p = g_list_next(p)) + { + mti = p->data; + tend = tstart + mti->duration_ppqn; + // printf("range %d-%d %f %d\n", tstart, tend, mti->tempo, mti->timesig_num); + if (pos == tstart) + { + double new_tempo = tempo >= 0 ? tempo : mti->tempo; + int new_timesig_num = timesig_num >= 0 ? timesig_num : mti->timesig_num; + // Is this operation going to become a no-op after the change? + gboolean is_noop_here = new_tempo <= 0 && new_timesig_num <= 0; + // If the new item is a no-op and not the first item, delete it + // and extend the previous item by deleted item's duration + if (is_noop_here) + { + uint32_t deleted_duration = mti->duration_ppqn; + if (prev) { + song->master_track_items = g_list_remove(song->master_track_items, mti); + mti = prev->data; + mti->duration_ppqn += deleted_duration; + } else { + // Instead of deleting the first item, make it a dummy one. + mti->tempo = 0; + mti->timesig_num = 0; + mti->timesig_denom = 0; + } + return; + } + goto set_values; + } + if (pos >= tstart && pos < tend) + { + if (is_noop || (tempo <= 0 && timesig_num <= 0)) + return; + // Split old item's duration + mti->duration_ppqn = pos - tstart; + mti = calloc(1, sizeof(struct cbox_master_track_item)); + mti->duration_ppqn = tend - pos; + p = g_list_next(p); + song->master_track_items = g_list_insert_before(song->master_track_items, p, mti); + goto set_values; + } + prev = p; + tstart = tend; + } + // The new item is a no-op and it's not deleting any of the current MTIs. + // Ignore it then. + if (is_noop) + return; + // The add position is past the end of the current MTIs. + if (pos > tend) + { + // Either extend the previous item, if there's any + if (prev) + { + mti = prev->data; + mti->duration_ppqn += pos - tend; + } + else + { + // ... or add a dummy 'pad' item + mti = calloc(1, sizeof(struct cbox_master_track_item)); + mti->duration_ppqn = pos; + assert(!song->master_track_items); + song->master_track_items = g_list_append(song->master_track_items, mti); + prev = song->master_track_items; + } + } + // Add the new item at the end + mti = calloc(1, sizeof(struct cbox_master_track_item)); + song->master_track_items = g_list_append(song->master_track_items, mti); +set_values: + // No effect if -1 + if (tempo >= 0) + mti->tempo = tempo; + if ((timesig_num > 0 && timesig_denom > 0) || + (timesig_num == 0 && timesig_denom == 0)) + { + mti->timesig_num = timesig_num; + mti->timesig_denom = timesig_denom; + } +} + +void cbox_song_add_track(struct cbox_song *song, struct cbox_track *track) +{ + track->owner = song; + song->tracks = g_list_append(song->tracks, track); +} + +void cbox_song_remove_track(struct cbox_song *song, struct cbox_track *track) +{ + assert(track->owner == song); + song->tracks = g_list_remove(song->tracks, track); + track->owner = NULL; +} + +void cbox_song_add_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern) +{ + pattern->owner = song; + song->patterns = g_list_append(song->patterns, pattern); +} + +void cbox_song_remove_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern) +{ + assert(pattern->owner == song); + pattern->owner = NULL; + song->patterns = g_list_remove(song->patterns, pattern); +} + +void cbox_song_clear(struct cbox_song *song) +{ + while(song->tracks) + cbox_object_destroy(song->tracks->data); + while(song->patterns) + cbox_object_destroy(song->patterns->data); + while(song->master_track_items) + { + struct cbox_master_track_item *mti = song->master_track_items->data; + song->master_track_items = g_list_remove(song->master_track_items, mti); + cbox_master_track_item_destroy(mti); + } +} + +void cbox_song_use_looped_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern) +{ + assert(pattern->owner == song); + song->patterns = g_list_remove(song->patterns, pattern); + pattern->owner = NULL; + + cbox_song_clear(song); + struct cbox_track *trk = cbox_track_new(CBOX_GET_DOCUMENT(song)); + cbox_song_add_track(song, trk); + cbox_song_add_pattern(song, pattern); + song->loop_start_ppqn = 0; + song->loop_end_ppqn = pattern->loop_end; + cbox_track_add_item(trk, 0, pattern, 0, pattern->loop_end); + cbox_engine_update_song_playback(app.engine); +} + +void cbox_song_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_song *song = CBOX_H2O(objhdr); + cbox_song_clear(song); + free(song); +} + diff --git a/template/calfbox/song.h b/template/calfbox/song.h new file mode 100644 index 0000000..cf54bee --- /dev/null +++ b/template/calfbox/song.h @@ -0,0 +1,62 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_SONG_H +#define CBOX_SONG_H + +#include "dom.h" +#include "pattern.h" + +CBOX_EXTERN_CLASS(cbox_song) + +struct cbox_track; + +struct cbox_master_track_item +{ + uint32_t duration_ppqn; + // May be zero (= no change) + double tempo; + // Either both are zero (= no change) or both are non-zero + int timesig_num, timesig_denom; +}; + +struct cbox_master_track +{ + GList *items; +}; + +struct cbox_song +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + GList *master_track_items; + GList *tracks; + GList *patterns; + gchar *lyrics_sheet, *chord_sheet; + uint32_t loop_start_ppqn, loop_end_ppqn; +}; + +extern struct cbox_song *cbox_song_new(struct cbox_document *document); +extern void cbox_song_add_track(struct cbox_song *song, struct cbox_track *track); +extern void cbox_song_remove_track(struct cbox_song *song, struct cbox_track *track); +extern void cbox_song_clear(struct cbox_song *song); +extern void cbox_song_use_looped_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern); +extern void cbox_song_set_mti(struct cbox_song *song, uint32_t pos, double tempo, int timesig_num, int timesig_denom); +extern void cbox_song_destroy(struct cbox_song *song); + +#endif diff --git a/template/calfbox/song_api_example.py b/template/calfbox/song_api_example.py new file mode 100644 index 0000000..a44a07c --- /dev/null +++ b/template/calfbox/song_api_example.py @@ -0,0 +1,76 @@ +import os +import sys +import struct +import time +import unittest + +sys.path = ["./py"] + sys.path + +import cbox + +global Document +global Transport +Document = cbox.Document +Transport = cbox.Transport + +# Make sure playback doesn't start prematurely +Transport.stop() + +song = Document.get_song() + +# Delete all the tracks and patterns +song.clear() + +# Add the first track +trk = song.add_track() +trk.clear_clips() + +# Create a binary blob that contains the MIDI events +pblob = bytes() +for noteindex in range(20): + # note on + pblob += cbox.Pattern.serialize_event(noteindex * 24, 0x90, 36+noteindex*3, 127) + # note off + pblob += cbox.Pattern.serialize_event(noteindex * 24 + 23, 0x90, 36+noteindex*3, 0) + +# This will be the length of the pattern (in pulses). It should be large enough +# to fit all the events +pattern_len = 10 * 24 * 2 + +# Create a new pattern object using events from the blob +pattern = song.pattern_from_blob(pblob, pattern_len) + +# Add an instance (clip) of the pattern to the track at position 0 +# The clip will contain the whole pattern (it is also possible to insert +# a single slice of the pattern) +clip = trk.add_clip(0, 0, pattern_len, pattern) + +# Stop the song at the end +#song.set_loop(pattern_len, pattern_len) +song.set_loop(pattern_len, pattern_len) + +# Set tempo - the argument must be a float +Transport.set_tempo(160.0) + +# Send the updated song data to the realtime thread +song.update_playback() + +# Flush +Transport.stop() + +print ("Song length (seconds) is %f" % (cbox.Transport.ppqn_to_samples(pattern_len) * 1.0 / Transport.status().sample_rate)) + +# The /master object API doesn't have any nice Python wrapper yet, so accessing +# it is a bit ugly, still - it works + +# Start playback +Transport.play() +print ("Playing") + +while True: + # Get transport information - current position (samples and pulses), current tempo etc. + master = Transport.status() + print (master.pos_ppqn) + # Query JACK ports, new USB devices etc. + cbox.call_on_idle() + time.sleep(0.1) diff --git a/template/calfbox/song_api_example2.py b/template/calfbox/song_api_example2.py new file mode 100644 index 0000000..6bbfd0d --- /dev/null +++ b/template/calfbox/song_api_example2.py @@ -0,0 +1,91 @@ +import os +import sys +import struct +import time +import unittest + +sys.path = ["./py"] + sys.path + +import cbox + +global Document +global Transport +Document = cbox.Document +Transport = cbox.Transport + +# Make sure playback doesn't start prematurely +Transport.stop() + +song = Document.get_song() + +# Delete all the tracks and patterns +song.clear() + +# Add the first track +trk = song.add_track() + +# Create a binary blob that contains zero MIDI events +emptyblob = bytes() + +# Create a new empty pattern +empty_pattern = song.pattern_from_blob(emptyblob, 16) + +# This will be the length of the pattern (in pulses). It should be large enough +# to fit all the events +pattern_len = 10 * 24 * 2 + +# Add an instance (clip) of the empty pattern to the track +clip1 = trk.add_clip(pattern_len, 0, 16, empty_pattern) + +# Add another instance after it +clip2 = trk.add_clip(2 * pattern_len, 0, 16, empty_pattern) + +# Create a binary blob that contains the MIDI events +pblob = bytes() +for noteindex in range(20): + # note on + pblob += cbox.Pattern.serialize_event(noteindex * 24, 0x90, 36+noteindex*3, 127) + # note off + pblob += cbox.Pattern.serialize_event(noteindex * 24 + 23, 0x90, 36+noteindex*3, 0) + +# Create a new pattern object using events from the blob +pattern = song.pattern_from_blob(pblob, pattern_len) + +# Update all attributes of the second clip, rearranging the order +clip2.set_pattern(pattern) +clip2.set_pos(0) +clip2.set_offset(0) +clip2.set_length(pattern_len) + +# Verify that the clips have been reordered +clips = [o.clip for o in trk.status().clips] +assert clips == [clip2, clip1] + +# Stop the song at the end +song.set_loop(pattern_len, pattern_len) + +# Set tempo - the argument must be a float +Transport.set_tempo(160.0) + +# Send the updated song data to the realtime thread +song.update_playback() + +# Flush +Transport.stop() + +print ("Song length (seconds) is %f" % (cbox.Transport.ppqn_to_samples(pattern_len) * 1.0 / Transport.status().sample_rate)) + +# The /master object API doesn't have any nice Python wrapper yet, so accessing +# it is a bit ugly, still - it works + +# Start playback +Transport.play() +print ("Playing") + +while True: + # Get transport information - current position (samples and pulses), current tempo etc. + master = Transport.status() + print (master.pos_ppqn) + # Query JACK ports, new USB devices etc. + cbox.call_on_idle() + time.sleep(0.1) diff --git a/template/calfbox/stm.h b/template/calfbox/stm.h new file mode 100644 index 0000000..a6c81f6 --- /dev/null +++ b/template/calfbox/stm.h @@ -0,0 +1,43 @@ +#ifndef CBOX_STM_H +#define CBOX_STM_H + +#include +#include + +static inline void **stm_array_clone_insert(void **old_array, int old_count, int index, void *data) +{ + size_t ps = sizeof(void *); + if (index == -1) + index = old_count; + void **new_array = malloc(ps * (old_count + 1)); + memcpy(&new_array[0], &old_array[0], ps * index); + new_array[index] = data; + if (index != old_count) + memcpy(&new_array[index + 1], &old_array[index], ps * (old_count - index)); + return new_array; +} + +static inline void **stm_array_clone_remove(void **old_array, int old_count, int index) +{ + size_t ps = sizeof(void *); + if (old_count == 1) + return NULL; + void **new_array = malloc(ps * (old_count - 1)); + memcpy(&new_array[0], &old_array[0], ps * index); + memcpy(&new_array[index], &old_array[index + 1], ps * (old_count - index - 1)); + return new_array; +} + +#define STM_ARRAY_FREE(old_array, count, destructor) \ + for (uint32_t i = 0; i < (count); i++) \ + destructor((old_array)[i]); \ + free(old_array); + +#define STM_ARRAY_FREE_OBJS(old_array, count) \ + do { \ + for (uint32_t i = 0; i < (count); i++) \ + cbox_object_destroy(&(old_array)[i]->_obj_hdr); \ + free(old_array); \ + } while(0) + +#endif \ No newline at end of file diff --git a/template/calfbox/streamplay.c b/template/calfbox/streamplay.c new file mode 100644 index 0000000..abbd9ac --- /dev/null +++ b/template/calfbox/streamplay.c @@ -0,0 +1,734 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "assert.h" +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "fifo.h" +#include "module.h" +#include "rt.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CBOX_STREAM_PLAYER_ERROR cbox_stream_player_error_quark() + +enum CboxStreamPlayerError +{ + CBOX_STREAM_PLAYER_ERROR_FAILED, +}; + +GQuark cbox_stream_player_error_quark(void) +{ + return g_quark_from_string("cbox-stream-player-error-quark"); +} + +#define CUE_BUFFER_SIZE 16000 +#define PREFETCH_THRESHOLD ((uint32_t)(CUE_BUFFER_SIZE / 4)) +#define MAX_READAHEAD_BUFFERS 3 + +#define NO_SAMPLE_LOOP ((uint64_t)-1ULL) + +struct stream_player_cue_point +{ + volatile uint64_t position; + volatile uint32_t size, length; + float *data; + int queued; +}; + +enum stream_state_phase +{ + STOPPED, + PLAYING, + STOPPING, + STARTING +}; + +struct stream_state +{ + SNDFILE *sndfile; + SF_INFO info; + uint64_t readptr; + uint64_t restart; + uint64_t readptr_new; + + volatile int buffer_in_use; + + struct stream_player_cue_point cp_start, cp_loop, cp_readahead[MAX_READAHEAD_BUFFERS]; + int cp_readahead_ready[MAX_READAHEAD_BUFFERS]; + struct stream_player_cue_point *pcp_current, *pcp_next; + + struct cbox_fifo *rb_for_reading, *rb_just_read; + float gain, fade_gain, fade_increment; + enum stream_state_phase phase; + + pthread_t thr_preload; + int thread_started; + + gchar *filename; +}; + +struct stream_player_module +{ + struct cbox_module module; + + struct stream_state *stream; + float fade_increment; +}; + +static void init_cue(struct stream_state *ss, struct stream_player_cue_point *pt, uint32_t size, uint64_t pos) +{ + pt->data = malloc(size * sizeof(float) * ss->info.channels); + pt->size = size; + pt->length = 0; + pt->queued = 0; + pt->position = pos; +} + +static void destroy_cue(struct stream_player_cue_point *pt) +{ + free(pt->data); + pt->data = NULL; +} + +static void load_at_cue(struct stream_state *ss, struct stream_player_cue_point *pt) +{ + if (pt->position != NO_SAMPLE_LOOP) + { + sf_seek(ss->sndfile, pt->position, 0); + pt->length = sf_readf_float(ss->sndfile, pt->data, pt->size); + } + pt->queued = 0; +} + +static int is_contained(struct stream_player_cue_point *pt, uint64_t ofs) +{ + return pt->position != NO_SAMPLE_LOOP && ofs >= pt->position && ofs < pt->position + pt->length; +} + +static int is_queued(struct stream_player_cue_point *pt, uint64_t ofs) +{ + return pt->queued && pt->position != NO_SAMPLE_LOOP && ofs >= pt->position && ofs < pt->position + pt->size; +} + +struct stream_player_cue_point *get_cue(struct stream_state *ss, uint64_t pos) +{ + int i; + + if (is_contained(&ss->cp_loop, pos)) + return &ss->cp_loop; + if (is_contained(&ss->cp_start, pos)) + return &ss->cp_start; + + for (i = 0; i < MAX_READAHEAD_BUFFERS; i++) + { + if (ss->cp_readahead_ready[i] && is_contained(&ss->cp_readahead[i], pos)) + return &ss->cp_readahead[i]; + } + return NULL; +} + +struct stream_player_cue_point *get_queued_buffer(struct stream_state *ss, uint64_t pos) +{ + int i; + + for (i = 0; i < MAX_READAHEAD_BUFFERS; i++) + { + if (!ss->cp_readahead_ready[i] && is_queued(&ss->cp_readahead[i], pos)) + return &ss->cp_readahead[i]; + } + return NULL; +} + +void request_load(struct stream_state *ss, int buf_idx, uint64_t pos) +{ + unsigned char cidx = (unsigned char)buf_idx; + struct stream_player_cue_point *pt = &ss->cp_readahead[buf_idx]; + + ss->cp_readahead_ready[buf_idx] = 0; + pt->position = pos; + pt->length = 0; + pt->queued = 1; + +#ifdef NDEBUG + cbox_fifo_write_atomic(ss->rb_for_reading, &cidx, 1); +#else + gboolean result = cbox_fifo_write_atomic(ss->rb_for_reading, &cidx, 1); + assert(result); +#endif +} + +int get_unused_buffer(struct stream_state *ss) +{ + int i = 0; + int notbad = -1; + + // return first buffer that is not currently played or in queue; XXXKF this is a very primitive strategy, a good one would at least use the current play position + for (i = 0; i < MAX_READAHEAD_BUFFERS; i++) + { + int64_t rel; + if (&ss->cp_readahead[i] == ss->pcp_current) + continue; + if (ss->cp_readahead[i].queued) + continue; + // If there's any unused buffer, return it + if (ss->cp_readahead[i].position == NO_SAMPLE_LOOP) + return i; + // If this has already been played, return it + rel = ss->readptr - ss->cp_readahead[i].position; + if (rel >= ss->cp_readahead[i].length) + return i; + // Use as second chance + notbad = i; + } + return notbad; +} + +static void *sample_preload_thread(void *user_data) +{ + struct stream_state *ss = user_data; + + do { + unsigned char buf_idx; + if (!cbox_fifo_read_atomic(ss->rb_for_reading, &buf_idx, 1)) + { + usleep(5000); + continue; + } + if (buf_idx == 255) + break; + // fprintf(stderr, "Preload: %d, %lld\n", (int)buf_idx, (long long)m->cp_readahead[buf_idx].position); + load_at_cue(ss, &ss->cp_readahead[buf_idx]); + // fprintf(stderr, "Preloaded\n", (int)buf_idx, (long long)m->cp_readahead[buf_idx].position); + cbox_fifo_write_atomic(ss->rb_just_read, &buf_idx, 1); + } while(1); + return NULL; +} + +void stream_player_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct stream_player_module *m = (struct stream_player_module *)module; +} + +static void request_next(struct stream_state *ss, uint64_t pos) +{ + // Check if we've requested a next buffer, if not, request it + + // First verify if our idea of 'next' buffer is correct + // XXXKF This is technically incorrect, it won't tell whether the next "block" that's there + // isn't actually a single sample. I worked it around by ensuring end of blocks are always + // at CUE_BUFFER_SIZE boundary, and this works well, but causes buffers to be of uneven size. + if (ss->pcp_next && (is_contained(ss->pcp_next, pos) || is_queued(ss->pcp_next, pos))) + { + // We're still waiting for the requested buffer, but that's OK + return; + } + + // We don't know the next buffer, or the next buffer doesn't contain + // the sample we're looking for. + ss->pcp_next = get_queued_buffer(ss, pos); + if (!ss->pcp_next) + { + // It hasn't even been requested - request it + int buf_idx = get_unused_buffer(ss); + if(buf_idx == -1) + { + printf("Ran out of buffers\n"); + return; + } + request_load(ss, buf_idx, pos); + ss->pcp_next = &ss->cp_readahead[buf_idx]; + + // printf("@%lld: Requested load into buffer %d at %lld\n", (long long)m->readptr, buf_idx, (long long) pos); + } +} + +static void copy_samples(struct stream_state *ss, cbox_sample_t **outputs, float *data, int count, int ofs, int pos) +{ + int i; + float gain = ss->gain * ss->fade_gain; + if (ss->phase == STARTING) + { + ss->fade_gain += ss->fade_increment; + if (ss->fade_gain >= 1) + { + ss->fade_gain = 1; + ss->phase = PLAYING; + } + } + else + if (ss->phase == STOPPING) + { + ss->fade_gain -= ss->fade_increment; + if (ss->fade_gain < 0) + { + ss->fade_gain = 0; + ss->phase = STOPPED; + } + } + float new_gain = ss->gain * ss->fade_gain; + float gain_delta = (new_gain - gain) * (1.0 / CBOX_BLOCK_SIZE); + + if (ss->info.channels == 1) + { + for (i = 0; i < count; i++) + { + outputs[0][ofs + i] = outputs[1][ofs + i] = gain * data[pos + i]; + gain += gain_delta; + } + } + else + if (ss->info.channels == 2) + { + for (i = 0; i < count; i++) + { + outputs[0][ofs + i] = gain * data[pos << 1]; + outputs[1][ofs + i] = gain * data[(pos << 1) + 1]; + gain += gain_delta; + pos++; + } + } + else + { + uint32_t ch = ss->info.channels; + for (i = 0; i < count; i++) + { + outputs[0][ofs + i] = gain * data[pos * ch]; + outputs[1][ofs + i] = gain * data[pos * ch + 1]; + gain += gain_delta; + pos++; + } + } + ss->readptr += count; + if (ss->readptr >= (uint32_t)ss->info.frames) + { + ss->readptr = ss->restart; + } +} + +void stream_player_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct stream_player_module *m = (struct stream_player_module *)module; + struct stream_state *ss = m->stream; + int i, optr; + unsigned char buf_idx; + + if (!ss || ss->readptr == NO_SAMPLE_LOOP) + { + for (int i = 0; i < CBOX_BLOCK_SIZE; i++) + { + outputs[0][i] = outputs[1][i] = 0; + } + return; + } + + // receive buffer completion messages from the queue + while(cbox_fifo_read_atomic(ss->rb_just_read, &buf_idx, 1)) + { + ss->cp_readahead_ready[buf_idx] = 1; + } + + optr = 0; + do { + if (ss->phase == STOPPED) + break; + if (ss->readptr == NO_SAMPLE_LOOP) + { + ss->phase = STOPPED; + break; + } + + if (ss->pcp_current && !is_contained(ss->pcp_current, ss->readptr)) + ss->pcp_current = NULL; + + if (!ss->pcp_current) + { + if (ss->pcp_next && is_contained(ss->pcp_next, ss->readptr)) + { + ss->pcp_current = ss->pcp_next; + ss->pcp_next = NULL; + } + else + ss->pcp_current = get_cue(ss, ss->readptr); + } + + if (!ss->pcp_current) + { + printf("Underrun at %d\n", (int)ss->readptr); + // Underrun; request/wait for next block and output zeros + request_next(ss, ss->readptr); + break; + } + assert(!ss->pcp_current->queued); + + uint64_t data_end = ss->pcp_current->position + ss->pcp_current->length; + uint32_t data_left = data_end - ss->readptr; + + // If we're close to running out of space, prefetch the next bit + if (data_left < PREFETCH_THRESHOLD && data_end < (uint64_t)ss->info.frames) + request_next(ss, data_end); + + float *data = ss->pcp_current->data; + uint32_t pos = ss->readptr - ss->pcp_current->position; + uint32_t count = data_end - ss->readptr; + if (count > (uint32_t)(CBOX_BLOCK_SIZE - optr)) + count = (uint32_t)(CBOX_BLOCK_SIZE - optr); + + // printf("Copy samples: copying %d, optr %d, %lld = %d @ [%lld - %lld], left %d\n", count, optr, (long long)m->readptr, pos, (long long)m->pcp_current->position, (long long)data_end, (int)data_left); + copy_samples(ss, outputs, data, count, optr, pos); + optr += count; + } while(optr < CBOX_BLOCK_SIZE); + + for (i = optr; i < CBOX_BLOCK_SIZE; i++) + { + outputs[0][i] = outputs[1][i] = 0; + } +} + +static void stream_state_destroy(struct stream_state *ss) +{ + unsigned char cmd = 255; + + if (ss->rb_for_reading && ss->thread_started) + { + cbox_fifo_write_atomic(ss->rb_for_reading, &cmd, 1); + pthread_join(ss->thr_preload, NULL); + } + destroy_cue(&ss->cp_start); + destroy_cue(&ss->cp_loop); + for (int i = 0; i < MAX_READAHEAD_BUFFERS; i++) + destroy_cue(&ss->cp_readahead[i]); + if (ss->rb_for_reading) + cbox_fifo_destroy(ss->rb_for_reading); + if (ss->rb_just_read) + cbox_fifo_destroy(ss->rb_just_read); + if (ss->sndfile) + sf_close(ss->sndfile); + if (ss->filename) + g_free(ss->filename); + free(ss); +} + +void stream_player_destroyfunc(struct cbox_module *module) +{ + struct stream_player_module *m = (struct stream_player_module *)module; + if (m->stream) + stream_state_destroy(m->stream); +} + +static struct stream_state *stream_state_new(const char *context, const gchar *filename, uint64_t loop, float fade_increment, GError **error) +{ + struct stream_state *stream = malloc(sizeof(struct stream_state)); + memset(&stream->info, 0, sizeof(stream->info)); + stream->sndfile = sf_open(filename, SFM_READ, &stream->info); + + if (!stream->sndfile) + { + g_set_error(error, CBOX_STREAM_PLAYER_ERROR, CBOX_STREAM_PLAYER_ERROR_FAILED, "instrument '%s': cannot open file '%s': %s", context, filename, sf_strerror(NULL)); + free(stream); + return NULL; + } + // g_message("Frames %d channels %d", (int)stream->info.frames, (int)stream->info.channels); + + stream->rb_for_reading = cbox_fifo_new(MAX_READAHEAD_BUFFERS + 1); + stream->rb_just_read = cbox_fifo_new(MAX_READAHEAD_BUFFERS + 1); + + stream->phase = STOPPED; + stream->readptr = 0; + stream->restart = loop; + stream->pcp_current = &stream->cp_start; + stream->pcp_next = NULL; + stream->gain = 1.0; + stream->fade_gain = 0.0; + stream->fade_increment = fade_increment; + stream->thread_started = 0; + stream->filename = g_strdup(filename); + + init_cue(stream, &stream->cp_start, CUE_BUFFER_SIZE, 0); + load_at_cue(stream, &stream->cp_start); + if (stream->restart > 0 && (stream->restart % CUE_BUFFER_SIZE) > 0) + init_cue(stream, &stream->cp_loop, CUE_BUFFER_SIZE + (CUE_BUFFER_SIZE - (stream->restart % CUE_BUFFER_SIZE)), stream->restart); + else + init_cue(stream, &stream->cp_loop, CUE_BUFFER_SIZE, stream->restart); + load_at_cue(stream, &stream->cp_loop); + for (int i = 0; i < MAX_READAHEAD_BUFFERS; i++) + { + init_cue(stream, &stream->cp_readahead[i], CUE_BUFFER_SIZE, NO_SAMPLE_LOOP); + stream->cp_readahead_ready[i] = 0; + } + if (pthread_create(&stream->thr_preload, NULL, sample_preload_thread, stream)) + { + stream_state_destroy(stream); + g_set_error(error, CBOX_STREAM_PLAYER_ERROR, CBOX_STREAM_PLAYER_ERROR_FAILED, "cannot create streaming thread: %s", strerror(errno)); + return NULL; + } + stream->thread_started = 1; + + return stream; +} + +/////////////////////////////////////////////////////////////////////////////////// + +static int stream_player_seek_execute(void *p) +{ + struct stream_player_module *m = p; + + m->stream->readptr = m->stream->readptr_new; + + return 1; +} + +static struct cbox_rt_cmd_definition stream_seek_command = { + .prepare = NULL, + .execute = stream_player_seek_execute, + .cleanup = NULL +}; + +/////////////////////////////////////////////////////////////////////////////////// + +static int stream_player_play_execute(void *p) +{ + struct stream_player_module *m = p; + + if (m->stream->readptr == NO_SAMPLE_LOOP) + m->stream->readptr = 0; + if (m->stream->phase != PLAYING) + { + if (m->stream->readptr == 0) + { + m->stream->fade_gain = 1.0; + m->stream->phase = PLAYING; + } + else + m->stream->phase = STARTING; + } + return 1; +} + +static struct cbox_rt_cmd_definition stream_play_command = { + .prepare = NULL, + .execute = stream_player_play_execute, + .cleanup = NULL +}; + +/////////////////////////////////////////////////////////////////////////////////// + +static int stream_player_stop_execute(void *p) +{ + struct stream_player_module *m = p; + + if (m->stream->phase != STOPPED) + m->stream->phase = STOPPING; + return 1; +} + +static struct cbox_rt_cmd_definition stream_stop_command = { + .prepare = NULL, + .execute = stream_player_stop_execute, + .cleanup = NULL +}; + +/////////////////////////////////////////////////////////////////////////////////// + +struct load_command_data +{ + struct stream_player_module *module; + gchar *context; + gchar *filename; + int loop_start; + struct stream_state *stream, *old_stream; + GError **error; +}; + +static int stream_player_load_prepare(void *p) +{ + struct load_command_data *c = p; + + if (!c->filename) + return 0; + c->stream = stream_state_new(c->context, c->filename, c->loop_start, c->module->fade_increment, c->error); + c->old_stream = NULL; + if (!c->stream) + { + g_free(c->filename); + return -1; + } + return 0; +} + +static int stream_player_load_execute(void *p) +{ + struct load_command_data *c = p; + + c->old_stream = c->module->stream; + c->module->stream = c->stream; + return 1; +} + +static void stream_player_load_cleanup(void *p) +{ + struct load_command_data *c = p; + + if (c->filename) + g_free(c->filename); + if (c->old_stream && c->old_stream != c->stream) + stream_state_destroy(c->old_stream); +} + +static struct cbox_rt_cmd_definition stream_load_command = { + .prepare = stream_player_load_prepare, + .execute = stream_player_load_execute, + .cleanup = stream_player_load_cleanup +}; + +/////////////////////////////////////////////////////////////////////////////////// + +static gboolean require_stream(struct stream_player_module *m, GError **error) +{ + if (!m->stream) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No stream loaded"); + return FALSE; + } + return TRUE; +} + +gboolean stream_player_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct stream_player_module *m = (struct stream_player_module *)ct->user_data; + if (!strcmp(cmd->command, "/seek") && !strcmp(cmd->arg_types, "i")) + { + if (!require_stream(m, error)) + return FALSE; + m->stream->readptr_new = CBOX_ARG_I(cmd, 0); + cbox_rt_execute_cmd_async(m->module.rt, &stream_seek_command, m); + } + else if (!strcmp(cmd->command, "/play") && !strcmp(cmd->arg_types, "")) + { + if (!require_stream(m, error)) + return FALSE; + cbox_rt_execute_cmd_async(m->module.rt, &stream_play_command, m); + } + else if (!strcmp(cmd->command, "/stop") && !strcmp(cmd->arg_types, "")) + { + if (!require_stream(m, error)) + return FALSE; + cbox_rt_execute_cmd_async(m->module.rt, &stream_stop_command, m); + } + else if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + if (m->stream) + { + return cbox_execute_on(fb, NULL, "/filename", "s", error, m->stream->filename) && + cbox_execute_on(fb, NULL, "/pos", "i", error, m->stream->readptr) && + cbox_execute_on(fb, NULL, "/length", "i", error, m->stream->info.frames) && + cbox_execute_on(fb, NULL, "/channels", "i", error, m->stream->info.channels) && + cbox_execute_on(fb, NULL, "/sample_rate", "i", error, m->stream->info.samplerate) && + cbox_execute_on(fb, NULL, "/playing", "i", error, m->stream->phase != STOPPED); + } + else + return + cbox_execute_on(fb, NULL, "/filename", "s", error, ""); + } + else if (!strcmp(cmd->command, "/load") && !strcmp(cmd->arg_types, "si")) + { + struct load_command_data *c = malloc(sizeof(struct load_command_data)); + c->context = m->module.instance_name; + c->module = m; + c->stream = NULL; + c->old_stream = NULL; + c->filename = g_strdup(CBOX_ARG_S(cmd, 0)); + c->loop_start = CBOX_ARG_I(cmd, 1); + c->error = error; + cbox_rt_execute_cmd_sync(m->module.rt, &stream_load_command, c); + gboolean success = c->stream != NULL; + free(c); + return success; + } + else if (!strcmp(cmd->command, "/unload") && !strcmp(cmd->arg_types, "")) + { + struct load_command_data *c = malloc(sizeof(struct load_command_data)); + c->context = m->module.instance_name; + c->module = m; + c->stream = NULL; + c->old_stream = NULL; + c->filename = NULL; + c->loop_start = 0; + c->error = error; + cbox_rt_execute_cmd_sync(m->module.rt, &stream_load_command, c); + gboolean success = c->stream == NULL; + free(c); + return success; + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown command '%s'", cmd->command); + return FALSE; + } + return TRUE; +} + +MODULE_CREATE_FUNCTION(stream_player) +{ + static int inited = 0; + + if (!inited) + { + inited = 1; + } + + struct stream_player_module *m = malloc(sizeof(struct stream_player_module)); + gchar *filename = cbox_config_get_string(cfg_section, "file"); + CALL_MODULE_INIT(m, 0, 2, stream_player); + m->module.process_event = stream_player_process_event; + m->module.process_block = stream_player_process_block; + m->fade_increment = 1.0 / (cbox_config_get_float(cfg_section, "fade_time", 0.01) * (m->module.srate / CBOX_BLOCK_SIZE)); + if (filename) + { + m->stream = stream_state_new(cfg_section, filename, (uint64_t)(int64_t)cbox_config_get_int(cfg_section, "loop", -1), m->fade_increment, error); + if (!m->stream) + { + CBOX_DELETE(&m->module); + return NULL; + } + } + else + m->stream = NULL; + + return &m->module; +} + +struct cbox_module_keyrange_metadata stream_player_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata stream_player_controllers[] = { +}; + +DEFINE_MODULE(stream_player, 0, 2) + diff --git a/template/calfbox/streamrec.c b/template/calfbox/streamrec.c new file mode 100644 index 0000000..9b2ed71 --- /dev/null +++ b/template/calfbox/streamrec.c @@ -0,0 +1,244 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "engine.h" +#include "errors.h" +#include "recsrc.h" +#include "rt.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// XXXKF the syncing model here is flawed in several ways: +// - it's not possible to do block-accurate syncing +// - it's not possible to flush the output buffer and stop recording +// - rb_for_writing is being written from two threads (audio and UI), +// which is not guaranteed to work +// - + +// 1/8s for 44.1kHz stereo float +#define STREAM_BUFFER_SIZE 16384 +#define STREAM_BUFFER_COUNT 8 + +#define STREAM_CMD_QUIT (-1) +#define STREAM_CMD_SYNC (-2) + +struct recording_buffer +{ + float data[STREAM_BUFFER_SIZE]; + uint32_t write_ptr; +}; + +struct stream_recorder +{ + struct cbox_recorder iface; + struct recording_buffer buffers[STREAM_BUFFER_COUNT]; + + struct cbox_rt *rt; + struct cbox_engine *engine; + gchar *filename; + SNDFILE *volatile sndfile; + SF_INFO info; + pthread_t thr_writeout; + sem_t sem_sync_completed; + + struct recording_buffer *cur_buffer; + uint32_t write_ptr; + + struct cbox_fifo *rb_for_writing, *rb_just_written; +}; + +static void *stream_recorder_thread(void *user_data) +{ + struct stream_recorder *self = user_data; + + do { + int8_t buf_idx; + if (!cbox_fifo_read_atomic(self->rb_for_writing, &buf_idx, 1)) + { + usleep(10000); + continue; + } + if (buf_idx == STREAM_CMD_QUIT) + break; + if (buf_idx == STREAM_CMD_SYNC) + { + // this assumes that the recorder is already detached from any source + if (self->cur_buffer && self->cur_buffer->write_ptr) + sf_write_float(self->sndfile, self->cur_buffer->data, self->cur_buffer->write_ptr); + + sf_command(self->sndfile, SFC_UPDATE_HEADER_NOW, NULL, 0); + sf_write_sync(self->sndfile); + sem_post(&self->sem_sync_completed); + continue; + } + else + { + sf_write_float(self->sndfile, self->buffers[buf_idx].data, self->buffers[buf_idx].write_ptr); + self->buffers[buf_idx].write_ptr = 0; + cbox_fifo_write_atomic(self->rb_just_written, &buf_idx, 1); + sf_command(self->sndfile, SFC_UPDATE_HEADER_NOW, NULL, 0); + } + } while(1); + return NULL; +} + +static gboolean stream_recorder_attach(struct cbox_recorder *handler, struct cbox_recording_source *src, GError **error) +{ + struct stream_recorder *self = handler->user_data; + + if (self->sndfile) + { + if (error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Recorder already attached to a different source"); + return FALSE; + } + + memset(&self->info, 0, sizeof(self->info)); + self->info.frames = 0; + self->info.samplerate = self->engine->io_env.srate; + self->info.channels = src->channels; + self->info.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT; // XXXKF make format configurable on instantiation + self->info.sections = 0; + self->info.seekable = 0; + + self->sndfile = sf_open(self->filename, SFM_WRITE, &self->info); + if (!self->sndfile) + { + if (error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot open sound file '%s': %s", self->filename, sf_strerror(NULL)); + return FALSE; + } + + pthread_create(&self->thr_writeout, NULL, stream_recorder_thread, self); + return TRUE; +} + +void stream_recorder_record_block(struct cbox_recorder *handler, const float **buffers, uint32_t offset, uint32_t numsamples) +{ + struct stream_recorder *self = handler->user_data; + + if (!self->sndfile) + return; + + if (self->cur_buffer && (self->cur_buffer->write_ptr + numsamples * self->info.channels) * sizeof(float) >= STREAM_BUFFER_SIZE) + { + int8_t idx = self->cur_buffer - self->buffers; + cbox_fifo_write_atomic(self->rb_for_writing, &idx, 1); + self->cur_buffer = NULL; + } + if (!self->cur_buffer) + { + int8_t buf_idx = -1; + if (!cbox_fifo_read_atomic(self->rb_just_written, &buf_idx, 1)) // underrun + return; + self->cur_buffer = &self->buffers[buf_idx]; + } + + unsigned int nc = self->info.channels; + + float *wbuf = self->cur_buffer->data + self->cur_buffer->write_ptr; + for (unsigned int c = 0; c < nc; c++) + for (uint32_t i = 0; i < numsamples; i++) + wbuf[c + i * nc] = buffers[c][i]; + self->cur_buffer->write_ptr += nc * numsamples; +} + +gboolean stream_recorder_detach(struct cbox_recorder *handler, GError **error) +{ + struct stream_recorder *self = handler->user_data; + + if (!self->sndfile) + { + if (error) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "No sound file associated with stream recorder"); + return FALSE; + } + + int8_t cmd = STREAM_CMD_SYNC; + cbox_fifo_write_atomic(self->rb_for_writing, (char *)&cmd, 1); + sem_wait(&self->sem_sync_completed); + return TRUE; +} + +void stream_recorder_destroy(struct cbox_recorder *handler) +{ + struct stream_recorder *self = handler->user_data; + + if (self->sndfile) + { + int8_t cmd = STREAM_CMD_QUIT; + cbox_fifo_write_atomic(self->rb_for_writing, (char *)&cmd, 1); + pthread_join(self->thr_writeout, NULL); + } + + cbox_fifo_destroy(self->rb_for_writing); + cbox_fifo_destroy(self->rb_just_written); + free(self); +} + + +static gboolean stream_recorder_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct stream_recorder *rec = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + if (!cbox_execute_on(fb, NULL, "/filename", "s", error, rec->filename)) + return FALSE; + return CBOX_OBJECT_DEFAULT_STATUS(&rec->iface, fb, error); + } + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +struct cbox_recorder *cbox_recorder_new_stream(struct cbox_engine *engine, struct cbox_rt *rt, const char *filename) +{ + struct stream_recorder *self = malloc(sizeof(struct stream_recorder)); + self->rt = rt; + self->engine = engine; + CBOX_OBJECT_HEADER_INIT(&self->iface, cbox_recorder, CBOX_GET_DOCUMENT(engine)); + cbox_command_target_init(&self->iface.cmd_target, stream_recorder_process_cmd, self); + + self->iface.user_data = self; + self->iface.attach = stream_recorder_attach; + self->iface.record_block = stream_recorder_record_block; + self->iface.detach = stream_recorder_detach; + self->iface.destroy = stream_recorder_destroy; + + self->sndfile = NULL; + self->filename = g_strdup(filename); + self->cur_buffer = NULL; + + self->rb_for_writing = cbox_fifo_new(STREAM_BUFFER_COUNT + 1); + self->rb_just_written = cbox_fifo_new(STREAM_BUFFER_COUNT + 1); + sem_init(&self->sem_sync_completed, 0, 0); + + CBOX_OBJECT_REGISTER(&self->iface); + + for (uint8_t i = 0; i < STREAM_BUFFER_COUNT; i++) + cbox_fifo_write_atomic(self->rb_just_written, (char *)&i, 1); + + return &self->iface; +} diff --git a/template/calfbox/synthbass.sfz b/template/calfbox/synthbass.sfz new file mode 100644 index 0000000..1290475 --- /dev/null +++ b/template/calfbox/synthbass.sfz @@ -0,0 +1,12 @@ + +sample=*sine loop_mode=loop_continuous tune=0 transpose=0 volume=-12 amp_velcurve_1=1 amp_velcurve_127=1 ampeg_release=0.1 + + +sample=*saw loop_mode=loop_continuous cutoff=220 cutoff_cc74=3600 resonance=14 fileg_sustain=10 fileg_decay=0.5 fileg_depth=3600 volume=-18 amp_velcurve_1=1 amp_velcurve_127=1 ampeg_release=0.1 fil_keytrack=100 +tonectl_freq=3000 tonectl_cc84=24 + + +tune=-5 fil_veltrack=1200 + + +tune=+5 transpose=+12 diff --git a/template/calfbox/tarfile.c b/template/calfbox/tarfile.c new file mode 100644 index 0000000..e253009 --- /dev/null +++ b/template/calfbox/tarfile.c @@ -0,0 +1,339 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config-api.h" +#include "errors.h" +#include "tarfile.h" +#include +#include +#include +#include +#include +#include +#include +#include + +struct tar_record +{ + char name[100]; + char mode[8]; + char uid[8]; + char gid[8]; + char size[12]; + char mtime[12]; + char checksum[8]; + char typeflag; + char linkname[100]; + char ustar[6]; + char ustarver[2]; + char uname[32]; + char gname[32]; + char devmajor[8]; + char devminor[8]; + char prefix[155]; + char padding[12]; +}; + +static void remove_item_if(gpointer p); + +struct cbox_tarfile *cbox_tarfile_open(const char *pathname, GError **error) +{ + gboolean debug = cbox_config_get_int("debug", "tarfile", 0); + gchar *canonical = realpath(pathname, NULL); + if (!canonical) + { + if (error) + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "cannot determine canonical name of '%s'", pathname); + return NULL; + } + int fd = open(canonical, O_RDONLY | O_LARGEFILE); + if (fd < 0) + { + free(canonical); + if (error) + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "cannot open '%s'", pathname); + return NULL; + } + GHashTable *byname = NULL, *byname_nc = NULL; + + byname = g_hash_table_new(g_str_hash, g_str_equal); + byname_nc = g_hash_table_new(g_str_hash, g_str_equal); + if (!byname || !byname_nc) + goto error; + + struct cbox_tarfile *tf = calloc(1, sizeof(struct cbox_tarfile)); + if (!tf) + goto error; + tf->fd = fd; + tf->items_byname = byname; + tf->items_byname_nc = byname_nc; + tf->refs = 1; + tf->file_pathname = canonical; + while(1) + { + struct tar_record rec; + int nbytes = read(fd, &rec, sizeof(rec)); + if (nbytes != sizeof(rec)) + break; + + int len = sizeof(rec.name); + while(len > 0 && (rec.name[len - 1] == ' ' || rec.name[len - 1] == '\0')) + len--; + + char sizetext[13]; + memcpy(sizetext, rec.size, 12); + sizetext[12] = '\0'; + unsigned long long size = strtoll(sizetext, NULL, 8); + + // skip block if name is empty + if (!len) + goto skipitem; + struct cbox_taritem *ti = calloc(1, sizeof(struct cbox_taritem)); + if (ti) + { + int offset = 0; + if (len >= 2 && rec.name[0] == '.' && rec.name[1] == '/') + offset = 2; + ti->filename = g_strndup(rec.name + offset, len - offset); + ti->filename_nc = g_utf8_casefold(rec.name + offset, len - offset); + if (!ti->filename || !ti->filename_nc) + goto itemerror; + ti->offset = lseek64(fd, 0, SEEK_CUR); + ti->size = size; + ti->refs = 2; + + // Overwrite old items by the same name and/or same case-folded name + remove_item_if(g_hash_table_lookup(tf->items_byname, ti->filename)); + remove_item_if(g_hash_table_lookup(tf->items_byname_nc, ti->filename_nc)); + + g_hash_table_insert(tf->items_byname, ti->filename, ti); + g_hash_table_insert(tf->items_byname_nc, ti->filename_nc, ti); + if (debug) + printf("name = %s len = %d offset = %d readsize = %d\n", ti->filename, len, (int)ti->offset, (int)size); + + goto skipitem; + } + itemerror: + rec.name[99] = '\0'; + g_warning("Could not allocate memory for tar item %s", rec.name); + if (ti) + { + if (ti->filename_nc) + g_free(ti->filename_nc); + if (ti->filename) + g_free(ti->filename); + free(ti); + } + skipitem: + lseek64(fd, (size + 511) &~ 511, SEEK_CUR); + } + return tf; + +error: + if (byname) + g_hash_table_destroy(byname); + if (byname_nc) + g_hash_table_destroy(byname_nc); + free(canonical); + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot allocate memory for tarfile data"); + return NULL; +} + +void remove_item_if(gpointer p) +{ + if (!p) + return; + + struct cbox_taritem *ti = p; + // If all references (by name and by case-folded name) gone, remove the item + if (!--ti->refs) + { + g_free(ti->filename); + g_free(ti->filename_nc); + free(ti); + } +} + +struct cbox_taritem *cbox_tarfile_get_item_by_name(struct cbox_tarfile *tarfile, const char *item_filename, gboolean ignore_case) +{ + if (item_filename[0] == '.' && item_filename[1] == '/') + item_filename += 2; + if (ignore_case) + { + gchar *folded = g_utf8_casefold(item_filename, -1); + struct cbox_taritem *item = g_hash_table_lookup(tarfile->items_byname_nc, folded); + g_free(folded); + return item; + } + else + return g_hash_table_lookup(tarfile->items_byname, item_filename); +} + +int cbox_tarfile_openitem(struct cbox_tarfile *tarfile, struct cbox_taritem *item) +{ + int fd = open(tarfile->file_pathname, O_RDONLY | O_LARGEFILE); + if (fd >= 0) + lseek64(fd, item->offset, SEEK_SET); + return fd; +} + +void cbox_tarfile_closeitem(struct cbox_tarfile *tarfile, struct cbox_taritem *item, int fd) +{ + if (fd >= 0) + close(fd); +} + +static void delete_foreach_func(gpointer key, gpointer value, gpointer user_data) +{ + struct cbox_taritem *ti = value; + if (!--ti->refs) + { + g_free(ti->filename); + g_free(ti->filename_nc); + free(ti); + } +} + +void cbox_tarfile_destroy(struct cbox_tarfile *tf) +{ + g_hash_table_foreach(tf->items_byname, delete_foreach_func, NULL); + g_hash_table_foreach(tf->items_byname_nc, delete_foreach_func, NULL); + close(tf->fd); + g_hash_table_destroy(tf->items_byname); + g_hash_table_destroy(tf->items_byname_nc); + free(tf->file_pathname); + free(tf); +} + +//////////////////////////////////////////////////////////////////////////////// + +sf_count_t tarfile_get_filelen(void *user_data) +{ + struct cbox_tarfile_sndstream *ss = user_data; + + return ss->item->size; +} + +sf_count_t tarfile_seek(sf_count_t offset, int whence, void *user_data) +{ + struct cbox_tarfile_sndstream *ss = user_data; + switch(whence) + { + case SEEK_SET: + ss->filepos = offset; + break; + case SEEK_CUR: + ss->filepos += offset; + break; + case SEEK_END: + ss->filepos = ss->item->size; + break; + } + if (((int64_t)ss->filepos) < 0) + ss->filepos = 0; + if (((int64_t)ss->filepos) >= (int64_t)ss->item->size) + ss->filepos = ss->item->size; + return ss->filepos; +} + +sf_count_t tarfile_read(void *ptr, sf_count_t count, void *user_data) +{ + struct cbox_tarfile_sndstream *ss = user_data; + ssize_t len = pread64(ss->file->fd, ptr, count, ss->item->offset + ss->filepos); + if (len > 0) + ss->filepos += len; + return len; +} + +sf_count_t tarfile_tell(void *user_data) +{ + struct cbox_tarfile_sndstream *ss = user_data; + return ss->filepos; +} + +struct SF_VIRTUAL_IO cbox_taritem_virtual_io = { + .get_filelen = tarfile_get_filelen, + .seek = tarfile_seek, + .read = tarfile_read, + .write = NULL, + .tell = tarfile_tell, +}; + +SNDFILE *cbox_tarfile_opensndfile(struct cbox_tarfile *tarfile, struct cbox_taritem *item, struct cbox_tarfile_sndstream *stream, SF_INFO *sfinfo) +{ + stream->file = tarfile; + stream->item = item; + stream->filepos = 0; + return sf_open_virtual(&cbox_taritem_virtual_io, SFM_READ, sfinfo, stream); +} + +//////////////////////////////////////////////////////////////////////////////// + +struct cbox_tarpool *cbox_tarpool_new(void) +{ + struct cbox_tarpool *pool = calloc(1, sizeof(struct cbox_tarpool)); + pool->files = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + return pool; +} + +struct cbox_tarfile *cbox_tarpool_get_tarfile(struct cbox_tarpool *pool, const char *name, GError **error) +{ + gchar *c = realpath(name, NULL); + if (!c) + { + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "cannot find a real path for '%s': %s", name, strerror(errno)); + return NULL; + } + struct cbox_tarfile *tf = g_hash_table_lookup(pool->files, c); + if (tf) + tf->refs++; + else + { + tf = cbox_tarfile_open(c, error); + if (!tf) + { + free(c); + return NULL; + } + g_hash_table_insert(pool->files, c, tf); + } + return tf; +} + +void cbox_tarpool_release_tarfile(struct cbox_tarpool *pool, struct cbox_tarfile *file) +{ + if (!--file->refs) + { + // XXXKF the insertion key is realpath(name) but the removal key is realpath(realpath(name)) + // usually it shouldn't cause problems, but it should be improved. + if (!g_hash_table_lookup(pool->files, file->file_pathname)) + g_warning("Removing tarfile %s not in the pool hash", file->file_pathname); + g_hash_table_remove(pool->files, file->file_pathname); + cbox_tarfile_destroy(file); + } +} + +void cbox_tarpool_destroy(struct cbox_tarpool *pool) +{ + int nelems = g_hash_table_size(pool->files); + if (nelems) + g_warning("%d unfreed elements in tar pool %p.", nelems, pool); + g_hash_table_destroy(pool->files); + free(pool); +} + diff --git a/template/calfbox/tarfile.h b/template/calfbox/tarfile.h new file mode 100644 index 0000000..c940cc7 --- /dev/null +++ b/template/calfbox/tarfile.h @@ -0,0 +1,74 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_TARFILE_H +#define CBOX_TARFILE_H + +#include +#include +#include + +struct cbox_taritem +{ + gchar *filename; + gchar *filename_nc; + uint64_t offset; + uint64_t size; + int refs; +}; + +struct cbox_tarfile +{ + int fd; + int refs; + GHashTable *items_byname; + GHashTable *items_byname_nc; + char *file_pathname; +}; + +struct cbox_tarpool +{ + GHashTable *files; +}; + +struct cbox_tarfile_sndstream +{ + struct cbox_tarfile *file; + struct cbox_taritem *item; + uint64_t filepos; +}; + +extern struct SF_VIRTUAL_IO cbox_taritem_virtual_io; + +extern struct cbox_tarfile *cbox_tarfile_open(const char *pathname, GError **error); + +extern struct cbox_taritem *cbox_tarfile_get_item_by_name(struct cbox_tarfile *tarfile, const char *item_filename, gboolean ignore_case); +extern int cbox_tarfile_openitem(struct cbox_tarfile *tarfile, struct cbox_taritem *item); +extern void cbox_tarfile_closeitem(struct cbox_tarfile *tarfile, struct cbox_taritem *item, int fd); + +extern SNDFILE *cbox_tarfile_opensndfile(struct cbox_tarfile *tarfile, struct cbox_taritem *item, struct cbox_tarfile_sndstream *stream, SF_INFO *sfinfo); +// No need to close - it reuses the cbox_tarfile file descriptor + +extern void cbox_tarfile_destroy(struct cbox_tarfile *tf); + +extern struct cbox_tarpool *cbox_tarpool_new(void); +extern struct cbox_tarfile *cbox_tarpool_get_tarfile(struct cbox_tarpool *pool, const char *name, GError **error); +extern void cbox_tarpool_release_tarfile(struct cbox_tarpool *pool, struct cbox_tarfile *file); +extern void cbox_tarpool_destroy(struct cbox_tarpool *pool); + +#endif diff --git a/template/calfbox/test.py b/template/calfbox/test.py new file mode 100644 index 0000000..13a7ae3 --- /dev/null +++ b/template/calfbox/test.py @@ -0,0 +1,364 @@ +import os +import sys +import struct +import time +import unittest + +# This is for locale testing +from gi.repository import GObject, Gdk, Gtk + +from calfbox import cbox +cbox.init_engine("") +cbox.start_noaudio(44100) + +cbox.Config.add_section("drumpattern:pat1", """ +title=Straight - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9... .... 9.6. .... +sd_trigger=.... 9..5 .2.. 9... +hh_trigger=9353 7353 7353 73.3 +ho_trigger=.... .... .... ..3. +""") +cbox.Config.add_section("fxpreset:piano_reverb", """ +engine=reverb +""") +cbox.Config.add_section("instrument:vintage", """ +engine=sampler +""") + +global Document +Document = cbox.Document + +Document.dump() + +class TestCbox(unittest.TestCase): + def verify_uuid(self, uuid, class_name, path = None): + self.assertEqual(cbox.GetThings(Document.uuid_cmd(uuid, "/get_class_name"), ['class_name'], []).class_name, class_name) + if path is not None: + self.assertEqual(cbox.GetThings(path + "/status", ['uuid'], []).uuid, uuid) + self.assertEqual(cbox.GetThings(Document.uuid_cmd(uuid, "/status"), ['uuid'], []).uuid, uuid) + + def test_scene(self): + scene = Document.get_scene() + self.assertEqual(Document.get_engine().status().scenes[0], scene) + + scene.clear() + scene.add_new_instrument_layer("test_instr", "sampler") + + scene_status = scene.status() + layer = scene_status.layers[0] + self.verify_uuid(scene.uuid, "cbox_scene", "/scene") + self.verify_uuid(layer.uuid, "cbox_layer", "/scene/layer/1") + + layers = scene.status().layers + self.assertEqual(len(layers), 1) + self.assertEqual(layers[0].uuid, layer.uuid) + layers[0].set_consume(0) + self.assertEqual(layers[0].status().consume, 0) + layers[0].set_consume(1) + self.assertEqual(layers[0].status().consume, 1) + layers[0].set_enable(0) + self.assertEqual(layers[0].status().enable, 0) + layers[0].set_enable(1) + self.assertEqual(layers[0].status().enable, 1) + + layer_status = layers[0].status() + instr_uuid = layer_status.instrument.uuid + iname = layer_status.instrument_name + self.assertEqual(iname, 'test_instr') + self.verify_uuid(instr_uuid, "cbox_instrument", "/scene/instr/%s" % iname) + + aux = scene.load_aux("piano_reverb") + module = aux.slot.engine + self.verify_uuid(aux.uuid, "cbox_aux_bus", "/scene/aux/piano_reverb") + scene.delete_aux("piano_reverb") + + def test_aux_scene(self): + engine = Document.new_engine(44100, 1024) + scene = engine.new_scene() + self.assertEqual(engine.status().scenes[0], scene) + scene.add_instrument_layer("vintage") + scene_status = scene.status() + layer = scene_status.layers[0] + self.verify_uuid(scene.uuid, "cbox_scene") + self.verify_uuid(layer.uuid, "cbox_layer", scene.make_path("/layer/1")) + + layers = scene.status().layers + self.assertEqual(len(layers), 1) + self.assertEqual(layers[0].uuid, layer.uuid) + layers[0].set_consume(0) + self.assertEqual(layers[0].status().consume, 0) + layers[0].set_consume(1) + self.assertEqual(layers[0].status().consume, 1) + layers[0].set_enable(0) + self.assertEqual(layers[0].status().enable, 0) + layers[0].set_enable(1) + self.assertEqual(layers[0].status().enable, 1) + + layer_status = layers[0].status() + instr_uuid = layer_status.instrument.uuid + iname = layer_status.instrument_name + self.verify_uuid(instr_uuid, "cbox_instrument", scene.make_path("/instr/%s" % iname)) + + aux = scene.load_aux("piano_reverb") + module = aux.slot.engine + self.verify_uuid(aux.uuid, "cbox_aux_bus", scene.make_path("/aux/piano_reverb")) + scene.delete_aux("piano_reverb") + scene2 = engine.new_scene() + with self.assertRaises(Exception) as context: + layer_status.instrument.move_to(scene2, 1) + self.assertEqual(str(context.exception), "Invalid position 2 (valid are 1..1 or 0 for append)") + layer_status.instrument.move_to(scene2, 0) + + layers = scene.status().layers + self.assertEqual(len(layers), 0) + layers = scene2.status().layers + self.assertEqual(len(layers), 1) + scene.add_instrument_layer("vintage") + with self.assertRaises(Exception) as context: + layer_status.instrument.move_to(scene, 0) + self.assertEqual(str(context.exception), "Instrument 'vintage' already exists in target scene") + + def test_sampler_api(self): + engine = Document.new_engine(44100, 1024) + scene = engine.new_scene() + scene.add_new_instrument_layer("temporary", "sampler") + scene_status = scene.status() + layer = scene_status.layers[0] + self.verify_uuid(scene.uuid, "cbox_scene") + self.verify_uuid(layer.uuid, "cbox_layer", scene.make_path("/layer/1")) + instrument = layer.get_instrument() + self.assertEqual(instrument.status().engine, "sampler") + + program0 = instrument.engine.load_patch_from_file(0, 'synthbass.sfz', 'test_sampler_sfz_loader') + self.assertNotEqual(program0, None) + self.assertEqual(program0.status().in_use, 16) + program1 = instrument.engine.load_patch_from_string(0, '.', ' resonance=3 unknown=123 key=36 sample=impulse.wav cutoff=1000 key=37 cutoff=2000 sample=impulse.wav ', 'test_sfz_parser_trailing_spaces') + self.assertNotEqual(program1, None) + self.assertEqual(program1.status().in_use, 16) + self.assertEqual(program1.status().name, 'test_sfz_parser_trailing_spaces') + self.assertRegex(program1.get_regions()[0].as_string(), 'sample=.*impulse\.wav') + program2 = instrument.engine.load_patch_from_string(0, '.', ' resonance=3 unknown=123 key=36 sample=impulse.wav cutoff=1000.5 key=37 sample=impulse.wav cutoff=2000', 'test_sampler_api') + self.assertNotEqual(program2, None) + self.assertEqual(program2.status().in_use, 16) + try: + program1.status() + self.assertTrue(False) + except Exception as e: + self.assertTrue('UUID not found' in str(e)) + patches = instrument.engine.get_patches() + patches_dict = {} + self.assertEqual(len(patches), 1) + for (patchid, patchdata) in patches.items(): + patchname, program, patchchannelcount = patchdata + self.verify_uuid(program.uuid, 'sampler_program') + self.assertEqual(program.status().program_no, patchid) + self.assertEqual(program.status().name, 'test_sampler_api') + self.assertEqual(program.status().sample_dir, '.') + self.assertEqual(program.status().program_no, 0) + self.assertEqual(program.status().in_use, 16) + instrument.engine.set_patch(1, 0) + self.assertEqual(program.status().in_use, 16) + instrument.engine.set_patch(2, 0) + self.assertEqual(program.status().in_use, 16) + regions = program.get_regions() + patches_dict[patchid] = (patchname, len(regions)) + for region in regions: + region_str = Document.map_uuid(region.uuid).as_string() + print (patchname, region.uuid, region_str) + if patchname == 'test_sampler_api': + self.assertTrue('impulse.wav' in region_str) + self.assertTrue('key=c' in region_str) + if 'key=c2' in region_str: + self.assertTrue('unknown=123' in region_str) + self.assertTrue('cutoff=1000.5' in region_str) + else: + self.assertFalse('unknown=123' in region_str) + self.assertTrue('cutoff=2000' in region_str) + program.add_control_init(11, 64) + self.assertTrue((11,64) in program.get_control_inits()) + program.delete_control_init(11, 0) + program.add_control_init(11, 0) + program.add_control_init(11, 64) + self.assertTrue((11,0) in program.get_control_inits()) + self.assertTrue((11,64) in program.get_control_inits()) + program.delete_control_init(11, 0) + self.assertTrue((11,0) not in program.get_control_inits()) + self.assertTrue((11,64) in program.get_control_inits()) + program.delete_control_init(11, 0) + self.assertTrue((11,0) not in program.get_control_inits()) + self.assertTrue((11,64) not in program.get_control_inits()) + program.add_control_init(11, 0) + program.add_control_init(11, 64) + program.delete_control_init(11, -1) + self.assertTrue((11,0) not in program.get_control_inits()) + self.assertTrue((11,64) not in program.get_control_inits()) + self.assertEqual(patches_dict, {0 : ('test_sampler_api', 2)}) + group = region.status().parent_group + self.assertTrue("resonance=3" in group.as_string()) + region.set_param("cutoff", 9000) + self.assertTrue('cutoff=9000' in region.as_string()) + region.set_param("sample", 'test.wav') + self.assertTrue('test.wav' in region.as_string()) + region.set_param("key", '12') + self.assertTrue('key=c0' in region.as_string()) + print (region.status()) + print (group.as_string()) + print (region.as_string()) + print ("Engine:", instrument.engine) + print ("Patches:", instrument.engine.get_patches()) + program3 = program2.clone_to(instrument, 1) + print ("Program 1") + print (program2.status(), program2) + print (program2.get_groups()) + print ("Program 2") + print (program3.status(), program3) + print (program3.get_groups()) + print (instrument.engine.get_patches()) + program3.delete() + + def test_rt(self): + rt = Document.get_rt() + self.assertEqual(cbox.GetThings(Document.uuid_cmd(rt.uuid, "/status"), ['uuid'], []).uuid, rt.uuid) + + def test_recorder_api(self): + engine = Document.new_engine(44100, 512) + scene = engine.new_scene() + scene.add_new_instrument_layer("temporary", "sampler") + layer = scene.status().layers[0] + instr = layer.status().instrument + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, []) + + meter_uuid = cbox.GetThings("/new_meter", ['uuid'], []).uuid + instr.cmd('/output/1/rec_dry/attach', None, meter_uuid) + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, [meter_uuid]) + instr.cmd('/output/1/rec_dry/detach', None, meter_uuid) + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, []) + if os.path.exists("test.wav"): + os.unlink('test.wav') + + rec = engine.new_recorder('test.wav') + self.assertEqual(rec.status().filename, 'test.wav') + rec_uuid = rec.uuid + instr.cmd('/output/1/rec_dry/attach', None, rec_uuid) + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, [rec_uuid]) + instr.cmd('/output/1/rec_dry/detach', None, rec_uuid) + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, []) + self.assertTrue(os.path.exists('test.wav')) + self.assertTrue(os.path.getsize('test.wav') < 512) + os.unlink('test.wav') + + rec = engine.new_recorder('test.wav') + self.assertEqual(rec.status().filename, 'test.wav') + rec_uuid = rec.uuid + instr.cmd('/output/1/rec_dry/attach', None, rec_uuid) + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, [rec_uuid]) + data = struct.unpack_from("512f", engine.render_stereo(512)) + instr.cmd('/output/1/rec_dry/detach', None, rec_uuid) + self.assertEqual(instr.get_things("/output/1/rec_dry/status", ['*handler']).handler, []) + rec.delete() + self.assertTrue(os.path.exists('test.wav')) + self.assertTrue(os.path.getsize('test.wav') > 512 * 4 * 2) + + def test_song(self): + song = Document.get_song() + song.clear() + tp = song.status() + self.assertEqual(tp.tracks, []) + self.assertEqual(tp.patterns, []) + self.assertEqual(tp.mtis, []) + + track = song.add_track() + pattern = song.load_drum_pattern('pat1') + track.add_clip(0, 0, 192, pattern) + + song = Document.get_song() + tp = song.status() + self.assertEqual(tp.tracks[0].name, 'Unnamed') + self.assertEqual(tp.patterns[0].name, 'pat1') + track = tp.tracks[0].track + pattern = tp.patterns[0].pattern + + track.set_name("Now named") + self.assertEqual(track.status().name, 'Now named') + pattern.set_name("pat1alt") + self.assertEqual(pattern.status().name, 'pat1alt') + + tp = song.status() + self.assertEqual(tp.tracks[0].name, 'Now named') + self.assertEqual(tp.patterns[0].name, 'pat1alt') + + clips = track.status().clips + self.assertEqual(clips[0].pos, 0) + self.assertEqual(clips[0].offset, 0) + self.assertEqual(clips[0].length, 192) + self.assertEqual(clips[0].pattern, pattern) + clip1 = clips[0].clip + + clip2 = track.add_clip(192, 96, 48, pattern) + + clip2_data = clip2.status() + self.assertEqual(clip2_data.pos, 192) + self.assertEqual(clip2_data.offset, 96) + self.assertEqual(clip2_data.length, 48) + self.assertEqual(clip2_data.pattern, pattern) + + clips = track.status().clips + self.assertEqual(clips, [cbox.ClipItem(0, 0, 192, pattern.uuid, clip1.uuid), cbox.ClipItem(192, 96, 48, pattern.uuid, clip2.uuid)]) + + clip1.delete() + + clips = track.status().clips + self.assertEqual(clips, [cbox.ClipItem(192, 96, 48, pattern.uuid, clip2.uuid)]) + + def test_mti(self): + MtiItem = cbox.MtiItem + song = Document.get_song() + song.clear() + tp = song.status() + self.assertEqual(tp.tracks, []) + self.assertEqual(tp.patterns, []) + self.assertEqual(tp.mtis, []) + song.set_mti(0, 120.0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0)]) + song.set_mti(60, 150.0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(60, 150.0, 0, 0)]) + song.set_mti(90, 180.0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(60, 150.0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + song.set_mti(60, 180.0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(60, 180.0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + song.set_mti(65, 210.0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(60, 180.0, 0, 0), MtiItem(65, 210.0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + + song.set_mti(60, 0.0, 0, 0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(65, 210.0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + song.set_mti(65, 0.0, 0, 0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + song.set_mti(68, 0.0, 0, 0) + self.assertEqual(song.status().mtis, [MtiItem(0, 120.0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + song.set_mti(0, 0.0, 0, 0) + self.assertEqual(song.status().mtis, [MtiItem(0, 0, 0, 0), MtiItem(90, 180.0, 0, 0)]) + song.set_mti(90, 0.0, 0, 0) + self.assertEqual(song.status().mtis, [MtiItem(0, 0, 0, 0)]) + + def test_error(self): + thrown = False + try: + Document.get_scene().cmd('transpose', None, cbox) + except ValueError as ve: + self.assertTrue("class 'module'" in str(ve)) + thrown = True + self.assertTrue(thrown) + +unittest.main() + +cbox.stop_audio() +cbox.shutdown_engine() diff --git a/template/calfbox/tests.c b/template/calfbox/tests.c new file mode 100644 index 0000000..a2cba95 --- /dev/null +++ b/template/calfbox/tests.c @@ -0,0 +1,741 @@ +#include "module.h" +#include "engine.h" +#include "sampler.h" +#include "sfzloader.h" +#include "tests.h" + +static struct sampler_module *create_sampler_instance(struct test_env *env, const char *cfg_section, const char *instance_name) +{ + extern struct cbox_module_manifest sampler_module; + + GError *error = NULL; + struct cbox_module *module = cbox_module_manifest_create_module(&sampler_module, cfg_section, env->doc, NULL, env->engine, instance_name, &error); + if (!module) + { + if (error) + fprintf(stderr, "Error: %s\n", error->message); + test_assert(module); + } + test_assert_equal_str(module->engine_name, "sampler"); + test_assert_equal_str(module->instance_name, instance_name); + return (struct sampler_module *)module; +} + +static int count_free_voices(struct test_env *env, struct sampler_module *m) +{ + int count = 0; + for (struct sampler_voice *v = m->voices_free; v; count++, v = v->next) + test_assert(count < MAX_SAMPLER_VOICES); + return count; +} + +static int count_channel_voices_if(struct test_env *env, struct sampler_module *m, int channel, gboolean (*cond_func)(struct sampler_voice *v, void *user_data), void *user_data) +{ + struct sampler_channel *c = &m->channels[channel]; + int count = 0; + for (struct sampler_voice *v = c->voices_running; v; v = v->next) + { + test_assert(count < MAX_SAMPLER_VOICES); + count += cond_func ? (cond_func(v, user_data) ? 1 : 0) : 1; + } + return count; +} + +static void verify_sampler_voices_if(struct test_env *env, struct sampler_module *m, int voices[16], gboolean (*cond_func)(struct sampler_voice *v, void *user_data), void *user_data) +{ + int total = 0; + for (int i = 0; i < 16; ++i) + { + int count = count_channel_voices_if(env, m, i, cond_func, user_data); + test_assert_equal(int, count, voices[i]); + total += count; + } + if (!cond_func) + test_assert_equal(int, count_free_voices(env, m), MAX_SAMPLER_VOICES - total); +} + +static void verify_sampler_voices(struct test_env *env, struct sampler_module *m, int voices[16]) +{ + verify_sampler_voices_if(env, m, voices, NULL, NULL); +} + +static gboolean is_voice_released(struct sampler_voice *v, void *ignore) +{ + return v->released; +} + +static struct sampler_program *load_sfz_into_sampler(struct test_env *env, struct sampler_module *m, const char *sfz_data) +{ + GError *error = NULL; + + struct sampler_program *prg = sampler_program_new(m, 0, "note_test", NULL, NULL, &error); + test_assert(prg); + test_assert_no_error(error); + + test_assert(sampler_module_load_program_sfz(m, prg, sfz_data, 1, &error)); + test_assert_no_error(error); + + sampler_register_program(m, prg); + test_assert(sampler_select_program(m, 0, prg->name, &error)); + test_assert_no_error(error); + + return prg; +} + +//////////////////////////////////////////////////////////////////////////////// + +void test_sampler_setup(struct test_env *env) +{ + struct sampler_module *m = create_sampler_instance(env, "test_setup", "smp1"); + + int expected_voices[16] = {}; + verify_sampler_voices(env, m, expected_voices); + + CBOX_DELETE(&m->module); +} + +//////////////////////////////////////////////////////////////////////////////// + +void test_sampler_midicurve(struct test_env *env) +{ + struct sampler_midi_curve curve; + float values[128]; + for (int i = 0; i < 128; ++i) + { + curve.values[i] = SAMPLER_CURVE_GAP; + values[i] = -100; + } + curve.values[0] = 0; + curve.values[64] = 0; + curve.values[127] = 1; + // Linear + sampler_midi_curve_interpolate(&curve, values, 0, 1, FALSE); + for (int i = 0; i < 128; ++i) + { + float expected = i < 64 ? 0 : (i - 64) / 63.0; + test_assert(fabs(values[i] - expected) < 0.001); + } + // Quadratic + sampler_midi_curve_interpolate(&curve, values, 0, 1, TRUE); + for (int i = 0; i < 128; ++i) + { + float expected = i < 64 ? 0 : (i - 64) / 63.0; + expected = expected * expected; + test_assert(fabs(values[i] - expected) < 0.001); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +void test_sampler_midicurve2(struct test_env *env) +{ + struct sampler_module *m = create_sampler_instance(env, "test_setup", "smp1"); + struct sampler_program *prg = load_sfz_into_sampler(env, m, + " curve_index=8 v0=-1 v32=0 v96=0 v127=1\n" + " curve_index=9 v32=0\n" + ); + for (int i = 0; i < 128; ++i) + { + float expected, actual; + if (i < 32) + expected = (-1 + (i / 32.0)); + else if (i <= 96) + expected = 0; + else + expected = (i - 96) * 1.f / (127 - 96); + actual = sampler_program_get_curve_value(prg, 8, i / 127.0); + test_assert(fabs(actual - expected) < 0.001); + + // Test interpolation + expected = 0.25f * actual + 0.75f * sampler_program_get_curve_value(prg, 8, (i + 1) / 127.0); + actual = sampler_program_get_curve_value(prg, 8, (i + 0.75) / 127.0); + test_assert(fabs(actual - expected) < 0.001); + + // Another curve + expected = i < 32 ? 0 : (i - 32) * 1.f / (127 - 32); + actual = sampler_program_get_curve_value(prg, 9, i / 127.0); + test_assert(fabs(actual - expected) < 0.001); + } + +#define VERIFY_BUILTIN_CURVE(curve, point, expected) \ + test_assert(fabs(sampler_program_get_curve_value(prg, curve, point) - expected) < 0.001) + + VERIFY_BUILTIN_CURVE(0, 0.f, 0.f); + VERIFY_BUILTIN_CURVE(0, 0.5f, 0.5f); + VERIFY_BUILTIN_CURVE(0, 1.0f, 1.0f); + + VERIFY_BUILTIN_CURVE(1, 0.0f, -1.0f); + VERIFY_BUILTIN_CURVE(1, 63.f/127.f, 0.0f); + VERIFY_BUILTIN_CURVE(1, 0.5f, 0.0f); + VERIFY_BUILTIN_CURVE(1, 64.f/127.f, 0.0f); + VERIFY_BUILTIN_CURVE(1, 1.0f, 1.0f); + + VERIFY_BUILTIN_CURVE(2, 0.f, 1.f); + VERIFY_BUILTIN_CURVE(2, 0.5f, 0.5f); + VERIFY_BUILTIN_CURVE(2, 1.0f, 0.f); + + VERIFY_BUILTIN_CURVE(3, 0.0f, 1.0f); + VERIFY_BUILTIN_CURVE(3, 63.f/127.f, 0.0f); + VERIFY_BUILTIN_CURVE(3, 0.5f, 0.0f); + VERIFY_BUILTIN_CURVE(3, 64.f/127.f, 0.0f); + VERIFY_BUILTIN_CURVE(3, 1.0f, -1.0f); + + VERIFY_BUILTIN_CURVE(4, 0.0f, 0.0f); + VERIFY_BUILTIN_CURVE(4, 0.5f, 0.25f); + VERIFY_BUILTIN_CURVE(4, 1.0f, 1.0f); + + VERIFY_BUILTIN_CURVE(5, 0.0f, 0.0f); + VERIFY_BUILTIN_CURVE(5, 0.25f, 0.5f); + VERIFY_BUILTIN_CURVE(5, 1.0f, 1.0f); + + sampler_unselect_program(m, prg); + CBOX_DELETE(prg); + CBOX_DELETE(&m->module); +} + +//////////////////////////////////////////////////////////////////////////////// + +void test_sampler_note_basic(struct test_env *env) +{ + struct sampler_module *m = create_sampler_instance(env, "test_setup", "smp1"); + struct sampler_program *prg = load_sfz_into_sampler(env, m, + " sample=*saw loop_mode=loop_continuous\n"); + + for (int i = 0; i < 5; ++i) + { + uint8_t midi_data[3] = { 0x90, 48 + i, 127 }; + m->module.process_event(&m->module, midi_data, sizeof(midi_data)); + int expected_voices[16] = {[0] = 1 + i}; + verify_sampler_voices(env, m, expected_voices); + } + for (int i = 0; i < 5; ++i) + { + uint8_t midi_data[3] = { 0x91, 48 + i, 127 }; + m->module.process_event(&m->module, midi_data, sizeof(midi_data)); + int expected_voices[16] = {[0] = 5, [1] = 1 + i}; + verify_sampler_voices(env, m, expected_voices); + int expected_released_voices[16] = {}; + verify_sampler_voices_if(env, m, expected_released_voices, is_voice_released, NULL); + } + + // Send some MIDI off to the first channel + for (int i = 0; i < 5; ++i) + { + uint8_t midi_data[3] = { (i & 1) ? 0x90 : 0x80, 48 + i, (i & 1) ? 0 : 127 }; + m->module.process_event(&m->module, midi_data, sizeof(midi_data)); + int expected_voices[16] = {[0] = 5, [1] = 5}; + verify_sampler_voices(env, m, expected_voices); + int expected_released_voices[16] = {[0] = 1 + i}; + verify_sampler_voices_if(env, m, expected_released_voices, is_voice_released, NULL); + } + sampler_unselect_program(m, prg); + CBOX_DELETE(prg); + CBOX_DELETE(&m->module); +} + +//////////////////////////////////////////////////////////////////////////////// + +struct region_logic_test_setup_step +{ + const uint8_t *midi_data; + uint32_t midi_data_len; + uint32_t voices[16]; +}; + +struct region_logic_test_setup +{ + const char *name; + const char *sfz_data; + const struct region_logic_test_setup_step *steps; +}; + +void test_sampler_note_region_logic(struct test_env *env) +{ + struct region_logic_test_setup *setup = env->arg; + struct sampler_module *m = create_sampler_instance(env, "test_setup", "smp1"); + struct sampler_program *prg = load_sfz_into_sampler(env, m, setup->sfz_data); + + int expected_voices[16] = {}; + for (int i = 0; setup->steps[i].midi_data; ++i) + { + env->context = g_strdup_printf("%s[%d]", setup->name, i); + const struct region_logic_test_setup_step *step = &setup->steps[i]; + m->module.process_event(&m->module, step->midi_data, step->midi_data_len); + for (int c = 0; c < 16; ++c) + expected_voices[c] += step->voices[c]; + verify_sampler_voices(env, m, expected_voices); + + g_free(env->context); + env->context = NULL; + } + sampler_unselect_program(m, prg); + CBOX_DELETE(prg); + CBOX_DELETE(&m->module); +} + +//////////////////////////////////////////////////////////////////////////////// + +#define MIDI_DATA_STEP(data, voices) { (const uint8_t *)data, sizeof(data) - 1, {voices} } +#define MIDI_DATA_END { NULL, 0, {} } +#define MIDI_DATA_STEP_MT(data, ...) { (const uint8_t *)data, sizeof(data) - 1, {__VA_ARGS__} } +#define REGION_LOGIC_TEST_SETUP(_name, sfz) \ + struct region_logic_test_setup setup_##_name = { \ + .name = #_name, \ + .sfz_data = sfz, \ + .steps = steps_##_name \ + } + +struct region_logic_test_setup_step steps_lokeyhikey[] = { + MIDI_DATA_STEP("\x90\x24\x7F", 0), + MIDI_DATA_STEP("\x90\x1F\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x21\x7F", 1), + MIDI_DATA_STEP("\x90\x22\x7F", 1), + MIDI_DATA_STEP("\x90\x23\x7F", 1), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(lokeyhikey, + "lokey=32 hikey=35 sample=*saw" +); + +struct region_logic_test_setup_step steps_lokeyhikey2[] = { + MIDI_DATA_STEP("\x90\x0E\x7F", 0), + MIDI_DATA_STEP("\x90\x0F\x7F", 1), + MIDI_DATA_STEP("\x90\x10\x7F", 1), + MIDI_DATA_STEP("\x90\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x1F\x7F", 0), + MIDI_DATA_STEP("\x90\x24\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x21\x7F", 2), + MIDI_DATA_STEP("\x90\x22\x7F", 2), + MIDI_DATA_STEP("\x90\x23\x7F", 1), + MIDI_DATA_STEP("\x90\x47\x7F", 0), + MIDI_DATA_STEP("\x90\x48\x7F", 1), + MIDI_DATA_STEP("\x90\x49\x7F", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(lokeyhikey2, + "lokey=15 hikey=16 sample=*saw\n" + "lokey=32 hikey=35 sample=*saw\n" + "lokey=33 hikey=34 sample=*saw\n" + "key=72 sample=*saw\n" +); + +struct region_logic_test_setup_step steps_lovelhivel[] = { + MIDI_DATA_STEP("\x90\x20\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x24", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\x90\x20\x21", 1), + MIDI_DATA_STEP("\x90\x20\x22", 1), + MIDI_DATA_STEP("\x90\x20\x23", 1), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(lovelhivel, + "lovel=32 hivel=35 sample=*saw" +); + +struct region_logic_test_setup_step steps_lochanhichan[] = { + MIDI_DATA_STEP_MT("\x90\x20\x7F", 0, 0, 0, 0, 0, 0, 0), + MIDI_DATA_STEP_MT("\x91\x20\x7F", 0, 1, 0, 0, 0, 0, 0), + MIDI_DATA_STEP_MT("\x92\x20\x7F", 0, 0, 1, 0, 0, 0, 0), + MIDI_DATA_STEP_MT("\x93\x20\x7F", 0, 0, 0, 0, 0, 0, 0), + MIDI_DATA_STEP_MT("\x94\x20\x7F", 0, 0, 0, 0, 2, 0, 0), + MIDI_DATA_STEP_MT("\x95\x20\x7F", 0, 0, 0, 0, 0, 2, 0), + MIDI_DATA_STEP_MT("\x96\x20\x7F", 0, 0, 0, 0, 0, 0, 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(lochanhichan, + "lochan=2 hichan=3 sample=*saw " + "lochan=5 hichan=6 sample=*saw " + "lochan=5 hichan=6 sample=*saw " +); + +struct region_logic_test_setup_step steps_chanaft[] = { + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xD0\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xD0\x20", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xD0\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xD0\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(chanaft, + "lochanaft=32 hichanaft=33 sample=*saw" +); + +struct region_logic_test_setup_step steps_polyaft[] = { + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xA0\x10\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xA0\x10\x20", 0), // note that this does not care about which key - it uses the last poly aft value + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xA0\x10\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xA0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(polyaft, + "lopolyaft=32 hipolyaft=33 sample=*saw" +); + +struct region_logic_test_setup_step steps_cc[] = { + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x20", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x11\x7F", 0), // try a different CC, just in case (positive test) + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x11\x21", 0), // try a different CC, just in case (negative test) + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_STEP("\xB0\x11\x20", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(cc, + "locc16=32 hicc16=33 sample=*saw" +); + +struct region_logic_test_setup_step steps_cc2[] = { + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x20", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x11\x41", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x11\x42", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x11\x41", 0), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x11\x3F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(cc2, + "locc16=32 hicc16=33 locc17=64 hicc17=65 sample=*saw" +); + +struct region_logic_test_setup_step steps_cc3[] = { // CC16 <= 33, CC17 >= 64 + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x20", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x11\x41", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x11\x71", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x21", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x11\x42", 0), + MIDI_DATA_STEP("\x90\x20\x20", 1), + MIDI_DATA_STEP("\xB0\x11\x41", 0), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x11\x3F", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\x90\x20\x20", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(cc3, + "hicc16=33 locc17=64 sample=*saw" +); + +struct region_logic_test_setup_step steps_oncc[] = { + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_STEP("\xB0\x10\x20", 1), + MIDI_DATA_STEP("\xB0\x10\x21", 1), // should probably be 1 according to test file 16, but that's madness + MIDI_DATA_STEP("\xB0\x10\x22", 0), + MIDI_DATA_STEP("\xB0\x10\x21", 1), + MIDI_DATA_STEP("\xB0\x10\x20", 1), // same + MIDI_DATA_STEP("\xB0\x10\x1F", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(oncc, + "on_locc16=32 on_hicc16=33 sample=*saw" +); + +struct region_logic_test_setup_step steps_release[] = { + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_STEP("\x80\x20\x7F", 1), + MIDI_DATA_STEP("\x80\x20\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x00", 1), + MIDI_DATA_STEP("\x90\x20\x00", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(release, + "trigger=release sample=*saw" +); + +struct region_logic_test_setup_step steps_firstlegato[] = { + MIDI_DATA_STEP_MT("\x90\x20\x7F", 1), + MIDI_DATA_STEP_MT("\x90\x21\x7F", 2), + MIDI_DATA_STEP_MT("\x90\x22\x7F", 2), + MIDI_DATA_STEP_MT("\x91\x20\x7F", 0, 1), // a different channel has its own counter + MIDI_DATA_STEP_MT("\x91\x21\x7F", 0, 2), + MIDI_DATA_STEP_MT("\x91\x22\x7F", 0, 2), + MIDI_DATA_STEP_MT("\x80\x20\x7F", 0), + MIDI_DATA_STEP_MT("\x80\x21\x7F", 0), + MIDI_DATA_STEP_MT("\x80\x22\x7F", 0), + MIDI_DATA_STEP_MT("\x90\x20\x7F", 1), + MIDI_DATA_STEP_MT("\x90\x21\x7F", 2), + MIDI_DATA_STEP_MT("\x90\x22\x7F", 2), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(firstlegato, + "trigger=first sample=*saw" + "trigger=legato sample=*saw" + "trigger=legato sample=*saw" +); + +struct region_logic_test_setup_step steps_switches[] = { + MIDI_DATA_STEP("\x90\x12\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x0F\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x14\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x12\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(switches, + "sw_lokey=16 sw_hikey=19 sw_last=16 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=17 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=17 lokey=32 hikey=35 sample=*saw" +); + +struct region_logic_test_setup_step steps_switches2[] = { + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x80\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_STEP("\x90\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x80\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x80\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x80\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 0), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(switches2, + "sw_lokey=16 sw_hikey=19 sw_down=16 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_down=17 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_down=17 lokey=32 hikey=35 sample=*saw" +); + +struct region_logic_test_setup_step steps_switches3[] = { + MIDI_DATA_STEP("\x90\x20\x7F", 2), // [0] + MIDI_DATA_STEP("\x90\x12\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x0F\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), // [10] + MIDI_DATA_STEP("\x90\x14\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x12\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + + MIDI_DATA_STEP("\x90\x09\x7F", 0), + MIDI_DATA_STEP("\x90\x12\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x90\x11\x7F", 0), // [20] + MIDI_DATA_STEP("\x90\x20\x7F", 4), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x90\x0F\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x90\x14\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x90\x12\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + + MIDI_DATA_STEP("\xB0\x79\x7F", 0), // [30] reset all controllers + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(switches3, + "lokey=32 hikey=35 sample=*saw" + "sw_lokey=8 sw_hikey=9 sw_last=9 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=16 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=17 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=17 lokey=32 hikey=35 sample=*saw" +); + +struct region_logic_test_setup_step steps_switches4[] = { + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 1), + MIDI_DATA_STEP("\x90\x11\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x10\x7F", 0), + MIDI_DATA_STEP("\xB0\x79\x7F", 0), // reset all controllers + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_STEP("\x90\x08\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 3), + MIDI_DATA_STEP("\x90\x09\x7F", 0), + MIDI_DATA_STEP("\x90\x20\x7F", 4), + MIDI_DATA_STEP("\xB0\x79\x7F", 0), // reset all controllers + MIDI_DATA_STEP("\x90\x20\x7F", 2), + MIDI_DATA_END, +}; + +REGION_LOGIC_TEST_SETUP(switches4, + "sw_lokey=16 sw_hikey=19 sw_default=17 sw_last=16 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=17 lokey=32 hikey=35 sample=*saw" + "sw_lokey=16 sw_hikey=19 sw_last=17 lokey=32 hikey=35 sample=*saw" + "sw_lokey=8 sw_hikey=10 sw_last=8 lokey=32 hikey=35 sample=*saw" + "sw_lokey=8 sw_hikey=10 sw_last=9 lokey=32 hikey=35 sample=*saw" + "sw_lokey=8 sw_hikey=10 sw_default=10 sw_last=9 lokey=32 hikey=35 sample=*saw" +); + +//////////////////////////////////////////////////////////////////////////////// + +void test_assert_failed(struct test_env *env, const char *file, int line, const char *check) +{ + if (env->context) + fprintf(stderr, "FAIL @%s:%d Assertion '%s' failed, context: %s.\n", file, line, check, env->context); + else + fprintf(stderr, "FAIL @%s:%d Assertion '%s' failed.\n", file, line, check); + longjmp(env->on_fail, 1); +} + +void test_assert_failed_free(struct test_env *env, const char *file, int line, gchar *check) +{ + if (env->context) + fprintf(stderr, "FAIL @%s:%d %s, context: %s\n", file, line, check, env->context); + else + fprintf(stderr, "FAIL @%s:%d %s.\n", file, line, check); + g_free(check); + longjmp(env->on_fail, 1); +} + +//////////////////////////////////////////////////////////////////////////////// + +struct test_info { + const char *name; + void (*func)(struct test_env *env); + void *arg; +} tests[] = { + { "test_sampler_setup", test_sampler_setup }, + { "test_sampler_midicurve", test_sampler_midicurve }, + { "test_sampler_midicurve2", test_sampler_midicurve2 }, + { "test_sampler_note_basic", test_sampler_note_basic }, + { "test_sampler_note_region_logic/key", test_sampler_note_region_logic, &setup_lokeyhikey }, + { "test_sampler_note_region_logic/key2", test_sampler_note_region_logic, &setup_lokeyhikey2 }, + { "test_sampler_note_region_logic/vel", test_sampler_note_region_logic, &setup_lovelhivel }, + { "test_sampler_note_region_logic/ch", test_sampler_note_region_logic, &setup_lochanhichan }, + { "test_sampler_note_region_logic/chanaft", test_sampler_note_region_logic, &setup_chanaft }, + { "test_sampler_note_region_logic/polyaft", test_sampler_note_region_logic, &setup_polyaft }, + { "test_sampler_note_region_logic/cc", test_sampler_note_region_logic, &setup_cc }, + { "test_sampler_note_region_logic/cc2", test_sampler_note_region_logic, &setup_cc2 }, + { "test_sampler_note_region_logic/cc3", test_sampler_note_region_logic, &setup_cc3 }, + { "test_sampler_note_region_logic/oncc", test_sampler_note_region_logic, &setup_oncc }, + { "test_sampler_note_region_logic/release", test_sampler_note_region_logic, &setup_release }, + { "test_sampler_note_region_logic/firstlegato", test_sampler_note_region_logic, &setup_firstlegato }, + { "test_sampler_note_region_logic/switches", test_sampler_note_region_logic, &setup_switches }, + { "test_sampler_note_region_logic/switches2", test_sampler_note_region_logic, &setup_switches2 }, + { "test_sampler_note_region_logic/switches3", test_sampler_note_region_logic, &setup_switches3 }, + { "test_sampler_note_region_logic/switches4", test_sampler_note_region_logic, &setup_switches4 }, +}; + +int main(int argc, char *argv[]) +{ + uint32_t tests_run = 0, tests_failed = 0; + for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) + { + struct test_env env; + env.doc = cbox_document_new(); + env.engine = cbox_engine_new(env.doc, NULL); + env.arg = tests[i].arg; + env.context = NULL; + cbox_config_init(""); + cbox_wavebank_init(); + tests_run++; + if (0 == setjmp(env.on_fail)) + { + printf("Running %s... ", tests[i].name); + fflush(stdout); + tests[i].func(&env); + printf("PASS\n"); + } + else + tests_failed++; + + CBOX_DELETE(env.engine); + env.engine = NULL; + cbox_document_destroy(env.doc); + env.doc = NULL; + cbox_wavebank_close(); + cbox_config_close(); + if (env.context) + { + g_free(env.context); + env.context = NULL; + } + } + printf("%d tests ran, %d tests failed.\n", tests_run, tests_failed); + return tests_failed != 0; +} + diff --git a/template/calfbox/tests.h b/template/calfbox/tests.h new file mode 100644 index 0000000..3f29996 --- /dev/null +++ b/template/calfbox/tests.h @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +struct test_env +{ + struct cbox_document *doc; + struct cbox_engine *engine; + void *arg; + gchar *context; + jmp_buf on_fail; +}; + +#define test_assert(condition) \ + if (!(condition)) \ + test_assert_failed(env, __FILE__, __LINE__, #condition); + +#define STR_FORMAT_int "%d" +#define STR_FORMAT_unsigned "%u" +#define STR_FORMAT_uint32_t PRIu32 + +#define test_assert_equal(type, val1, val2) \ + do { \ + type _v1 = (val1), _v2 = (val2); \ + if ((_v1) != (_v2)) \ + test_assert_failed_free(env, __FILE__, __LINE__, g_strdup_printf("%s equal to " STR_FORMAT_##type ", not " STR_FORMAT_##type, #val1, _v1, _v2)); \ + } while(0) + +#define test_assert_equal_str(val1, val2) \ + do { \ + const char *_v1 = (val1), *_v2 = (val2); \ + if (strcmp(_v1, _v2)) \ + test_assert_failed_free(env, __FILE__, __LINE__, g_strdup_printf("%s equal to '%s', not '%s'", #val1, _v1, _v2)); \ + } while(0) + +#define test_assert_no_error(error) \ + do { \ + if (error) { \ + gchar *copy = g_strdup(error->message); \ + g_error_free(error); \ + test_assert_failed_free(env, __FILE__, __LINE__, copy); \ + } \ + } while(0) + +extern void test_assert_failed(struct test_env *env, const char *file, int line, const char *check); +extern void test_assert_failed_free(struct test_env *env, const char *file, int line, gchar *check); + diff --git a/template/calfbox/tonectl.c b/template/calfbox/tonectl.c new file mode 100644 index 0000000..d0f19e5 --- /dev/null +++ b/template/calfbox/tonectl.c @@ -0,0 +1,133 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-float.h" +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_PARAMS tone_control_params + +struct tone_control_params +{ + float lowpass, highpass; +}; + +struct tone_control_module +{ + struct cbox_module module; + + struct tone_control_params *params, *old_params; + + struct cbox_onepolef_coeffs lowpass_coeffs, highpass_coeffs; + + struct cbox_onepolef_state lowpass_state[2], highpass_state[2]; + + float tpdsr; // 2 pi / sr +}; + +gboolean tone_control_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct tone_control_module *m = (struct tone_control_module *)ct->user_data; + + EFFECT_PARAM("/lowpass", "f", lowpass, double, , 5, 20000) else + EFFECT_PARAM("/highpass", "f", highpass, double, , 5, 20000) else + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + return cbox_execute_on(fb, NULL, "/lowpass", "f", error, m->params->lowpass) + && cbox_execute_on(fb, NULL, "/highpass", "f", error, m->params->highpass) + && CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error) + ; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +void tone_control_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + // struct tone_control_module *m = (struct tone_control_module *)module; +} + +void tone_control_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct tone_control_module *m = (struct tone_control_module *)module; + + if (m->params != m->old_params) + { + cbox_onepolef_set_lowpass(&m->lowpass_coeffs, m->params->lowpass * m->tpdsr); + cbox_onepolef_set_highpass(&m->highpass_coeffs, m->params->highpass * m->tpdsr); + m->old_params = m->params; + } + + cbox_onepolef_process_to(&m->lowpass_state[0], &m->lowpass_coeffs, inputs[0], outputs[0]); + cbox_onepolef_process_to(&m->lowpass_state[1], &m->lowpass_coeffs, inputs[1], outputs[1]); + cbox_onepolef_process(&m->highpass_state[0], &m->highpass_coeffs, outputs[0]); + cbox_onepolef_process(&m->highpass_state[1], &m->highpass_coeffs, outputs[1]); +} + +MODULE_SIMPLE_DESTROY_FUNCTION(tone_control) + +MODULE_CREATE_FUNCTION(tone_control) +{ + static int inited = 0; + if (!inited) + { + inited = 1; + } + + struct tone_control_module *m = malloc(sizeof(struct tone_control_module)); + CALL_MODULE_INIT(m, 2, 2, tone_control); + m->module.process_event = tone_control_process_event; + m->module.process_block = tone_control_process_block; + + m->tpdsr = 2 * M_PI * m->module.srate_inv; + + m->old_params = NULL; + m->params = malloc(sizeof(struct tone_control_params)); + + m->params->lowpass = cbox_config_get_float(cfg_section, "lowpass", 8000.f); + m->params->highpass = cbox_config_get_float(cfg_section, "highpass", 75.f); + + cbox_onepolef_reset(&m->lowpass_state[0]); + cbox_onepolef_reset(&m->lowpass_state[1]); + cbox_onepolef_reset(&m->highpass_state[0]); + cbox_onepolef_reset(&m->highpass_state[1]); + + return &m->module; +} + + +struct cbox_module_keyrange_metadata tone_control_keyranges[] = { +}; + +struct cbox_module_livecontroller_metadata tone_control_controllers[] = { +}; + +DEFINE_MODULE(tone_control, 2, 2) + diff --git a/template/calfbox/tonewheel.c b/template/calfbox/tonewheel.c new file mode 100644 index 0000000..dd76053 --- /dev/null +++ b/template/calfbox/tonewheel.c @@ -0,0 +1,586 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#include "config-api.h" +#include "cmd.h" +#include "dspmath.h" +#include "module.h" +#include "onepole-int.h" +#include +#include +#include +#include +#include +#include + +// a0 a1 a2 b1 b2 for scanner vibrato filter @4kHz with sr=44.1: 0.057198 0.114396 0.057198 -1.218829 0.447620 + +static int64_t scanner_a0 = (int64_t)(0.057198 * 1048576); +static int64_t scanner_b1 = (int64_t)(-1.218829 * 1048576); +static int64_t scanner_b2 = (int64_t)(0.447620 * 1048576); + +static int sine_table[2048]; +static int complex_table[2048]; +static int distortion_table[8192]; + +struct biquad +{ + int x1; + int y1; + int x2; + int y2; +}; + +struct tonewheel_organ_module +{ + struct cbox_module module; + + uint32_t frequency[91]; + uint32_t phase[91]; + uint64_t pedalmasks; + uint64_t upper_manual, lower_manual; + int amp_scaling[91]; + struct biquad scanner_delay[18]; + struct cbox_onepole_state filter_anticlick, filter_overdrive; + struct cbox_onepole_coeffs filter_anticlick_coeffs, filter_overdrive_coeffs; + float percussion; + int enable_percussion, enable_vibrato_upper, enable_vibrato_lower, vibrato_mode, vibrato_mix, percussion_3rd; + int do_filter; + int cc91; + uint32_t vibrato_phase, vibrato_dphase; + + int pedal_drawbar_settings[2]; + int upper_manual_drawbar_settings[9]; + int lower_manual_drawbar_settings[9]; +}; + +static const int drawbars[9] = {0, 19, 12, 24, 24 + 7, 36, 36 + 4, 36 + 7, 48}; + +static void set_keymask(struct tonewheel_organ_module *m, int channel, int key, int value) +{ + uint64_t mask = 0; + uint64_t *manual = NULL; + if (key >= 24 && key < 36) + { + mask = 1 << (key - 24); + manual = &m->pedalmasks; + } + else if (key >= 36 && key < 36 + 61) + { + manual = (channel == 0) ? &m->upper_manual : &m->lower_manual; + mask = ((int64_t)1) << (key - 36); + } + else + return; + + if (value) + *manual |= mask; + else + *manual &= ~mask; +} + +void tonewheel_organ_process_event(struct cbox_module *module, const uint8_t *data, uint32_t len) +{ + struct tonewheel_organ_module *m = (struct tonewheel_organ_module *)module; + if (len > 0) + { + int cmd = data[0] >> 4; + if (cmd == 9 && data[2]) + { + int channel = data[0] & 0x0F; + int key = data[1] & 127; + set_keymask(m, channel, key, 1); + if (m->percussion < 0 && key >= 36 && m->enable_percussion && channel == 0) + m->percussion = 16.0; + } + if (cmd == 8 || (cmd == 9 && !data[2])) + { + int channel = data[0] & 0x0F; + int key = data[1] & 127; + set_keymask(m, channel, key, 0); + + if (channel == 0 && !m->upper_manual) + m->percussion = -1; + } + if (cmd == 11) + { + int *drawbars = (data[0] & 0xF0) != 0 ? m->lower_manual_drawbar_settings : m->upper_manual_drawbar_settings; + if (data[1] >= 21 && data[1] <= 29) + drawbars[data[1] - 21] = data[2] * 8 / 127; + if (data[1] == 82) + drawbars[8] = data[2] * 8 / 127; + if (data[1] == 64) + m->do_filter = data[2] >= 64; + if (data[1] == 91) + m->cc91 = data[2]; + if (data[1] == 93) + m->vibrato_mix = data[2] > 0; + if (data[1] == 120 || data[1] == 123) + { + for (int i = 24; i < 36 + 61; i++) + set_keymask(m, data[0] & 0xF, i, 0); + } + //if (data[1] == 6) + // cbox_onepole_set_lowpass(&m->filter_overdrive_coeffs, hz2w(data[2] * 10000 / 127, 44100.0)); + } + } +} + +static inline int check_keymask(uint64_t keymasks, int note) +{ + if (note < 0 || note > 127) + return 0; + if (note >= 24 && note < 36) + return 0 != (keymasks & (1 << (note - 24))); + if (note >= 36 && note < 36 + 61) + return 0 != (keymasks & (1ULL << (note - 36))); + return 0; +} + +static inline int tonegenidx_pedals(int note, int shift) +{ + if (note < 24 || note > 24 + 11) + return 91; + + note -= 24; + return note + shift; +} + +static inline int tonegenidx(int note, int shift) +{ + // ignore everything below the lowest key + if (note < 36) + return 91; + + note -= 36; + + // harmonic foldback in the first octave of the manual + if (note < 12 && shift < 12) + return note + 12; + + while (note + shift > 90) + note -= 12; + + return note + shift; +} + +static int drawbar_amp_mapping[9] = { 0, 1, 2, 3, 4, 6, 8, 11, 16 }; + +static void calc_crosstalk(int *wheel1, int *wheel2) +{ + int w1 = *wheel1; + int w2 = *wheel2; + *wheel1 += w2 >> 9; + *wheel2 += w1 >> 9; +} + +static int compress_amp(int iamp, int scaling) +{ + if (iamp > 512) + iamp = 512 + 3 * ((iamp - 512) >> 2); + return (iamp * scaling) >> 10; +} + +static void set_tonewheels(struct tonewheel_organ_module *m, int tonegens[2][92]) +{ + int n, i; + int pshift = m->percussion_3rd ? 24 + 7 : 24; + + int upper_manual_drawbar_amp[9], lower_manual_drawbar_amp[9]; + + for (i = 0; i < 9; i++) + { + upper_manual_drawbar_amp[i] = drawbar_amp_mapping[m->upper_manual_drawbar_settings[i]] * 8; + lower_manual_drawbar_amp[i] = drawbar_amp_mapping[m->lower_manual_drawbar_settings[i]] * 8; + } + + memset(tonegens, 0, 2 * 92 * sizeof(tonegens[0][0])); + // pedalboard + for (n = 24; n < 24 + 12; n++) + { + if (check_keymask(m->pedalmasks, n)) + { + tonegens[0][tonegenidx_pedals(n, 0)] += 3 * 16 * m->pedal_drawbar_settings[0]; + tonegens[0][tonegenidx_pedals(n, 12)] += 3 * 16 * m->pedal_drawbar_settings[1]; + } + } + // manual + for (n = 36; n < 36 + 61; n++) + { + if (check_keymask(m->upper_manual, n)) + { + int tgf = m->enable_vibrato_upper; + for (i = 0; i < 9; i++) + { + int tg = tonegenidx(n, drawbars[i]); + tonegens[tgf][tg] += upper_manual_drawbar_amp[i]; + } + if (m->percussion > 0) + tonegens[0][tonegenidx(n, pshift)] += m->percussion * 10; + } + if (check_keymask(m->lower_manual, n)) + { + int tgf = m->enable_vibrato_lower; + for (i = 0; i < 9; i++) + { + int tg = tonegenidx(n, drawbars[i]); + tonegens[tgf][tg] += lower_manual_drawbar_amp[i]; + } + } + } + for (n = 0; n < 91; n++) + { + int scaling = m->amp_scaling[n]; + tonegens[0][n] = compress_amp(tonegens[0][n], scaling); + tonegens[1][n] = compress_amp(tonegens[1][n], scaling); + } + for (n = 0; n < 36; n++) + { + calc_crosstalk(&tonegens[0][n], &tonegens[0][n + 48]); + calc_crosstalk(&tonegens[1][n], &tonegens[1][n + 48]); + } +} + +void tonewheel_organ_process_block(struct cbox_module *module, cbox_sample_t **inputs, cbox_sample_t **outputs) +{ + struct tonewheel_organ_module *m = (struct tonewheel_organ_module *)module; + int n, i; + + static const uint32_t frac_mask = (1 << 21) - 1; + + int internal_out_for_vibrato[CBOX_BLOCK_SIZE]; + int internal_out[CBOX_BLOCK_SIZE]; + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + internal_out[i] = 0; + internal_out_for_vibrato[i] = 0; + } + // 91 tonewheels + 1 dummy + int tonegens[2][92]; + set_tonewheels(m, tonegens); + if (m->percussion > 0) + m->percussion *= 0.99f; + for (n = 0; n < 91; n++) + { + if (tonegens[0][n] > 0 || tonegens[1][n]) + { + int iamp1, iamp2; + + iamp1 = tonegens[0][n]; + iamp2 = tonegens[1][n]; + + int *table = n < 12 ? complex_table : sine_table; + uint32_t phase = m->phase[n]; + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + uint32_t pos = phase >> 21; + int val0 = table[(pos - 1) & 2047]; + int val1 = table[pos]; + // phase & frac_mask has 21 bits of resolution, but we only have 14 bits of headroom here + int frac_14bit = (phase & frac_mask) >> (21-14); + int val = (val1 * frac_14bit + val0 * ((1 << 14) - frac_14bit)) >> 14; + internal_out[i] += val * iamp1 >> 3; + internal_out_for_vibrato[i] += val * iamp2 >> 3; + phase += m->frequency[n]; + } + } + m->phase[n] += m->frequency[n] * CBOX_BLOCK_SIZE; + } + + static const int v1[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8 }; + static const int v2[] = { 0, 1, 2, 4, 6, 8, 9, 10, 12 }; + static const int v3[] = { 0, 1, 3, 6, 11, 12, 15, 17, 18, 18, 18 }; + static const int *vtypes[] = { v1, v2, v3 }; + const int *dmap = vtypes[m->vibrato_mode]; + int32_t mix = m->vibrato_mix; + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + int x0 = internal_out_for_vibrato[i] >> 1; + int delay[19]; + int64_t accum; + delay[0] = x0; + for (n = 0; n < 18; n++) + { + struct biquad *bq = &m->scanner_delay[n]; + accum = 0; + accum += (x0 + (bq->x1 << 1) + bq->x2) * scanner_a0; + accum -= bq->y1 * scanner_b1; + accum -= bq->y2 * scanner_b2; + accum = accum >> 20; + bq->x2 = bq->x1; + bq->x1 = x0; + bq->y2 = bq->y1; + bq->y1 = accum; + + delay[1 + n] = x0 = accum; + } + m->vibrato_phase += m->vibrato_dphase; + + uint32_t vphase = m->vibrato_phase; + if (vphase >= 0x80000000) + vphase = ~vphase; + uint32_t vphint = vphase >> 28; + + accum = 0; + + accum += delay[dmap[vphint]] * ((1ULL << 28) - (vphase & ~0xF0000000)); + accum += delay[dmap[vphint + 1]] * (vphase & ~0xF0000000ULL); + + + internal_out[i] += (accum >> 28) + mix * delay[0]; + } + + int32_t filtered[CBOX_BLOCK_SIZE]; + cbox_onepole_process_to(&m->filter_overdrive, &m->filter_overdrive_coeffs, internal_out, filtered); + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + int value = filtered[i] >> 9; + int sign = (value >= 0 ? 1 : -1); + int a, b, idx; + + value = abs(value); + if (value > 8192 * 8 - 2 * 8) + value = 8192 * 8 - 2 * 8; + idx = value >> 3; + a = distortion_table[idx]; + b = distortion_table[idx + 1]; + internal_out[i] = (internal_out[i] >> 11) + sign * (a + ((b - a) * (value & 7) >> 3)); + //internal_out[i] = 32767 * value2; + } + + cbox_onepole_process(&m->filter_anticlick, &m->filter_anticlick_coeffs, internal_out); + + for (i = 0; i < CBOX_BLOCK_SIZE; i++) + { + float value = internal_out[i] * (1.0 / 32768.0); + outputs[1][i] = outputs[0][i] = value; + } +} + +static void biquad_init(struct biquad *bq) +{ + bq->x1 = bq->y1 = bq->x2 = bq->y2 = 0; +} + +static void read_drawbars(int *drawbars, int count, const char *registration) +{ + int i; + + memset(drawbars, 0, count * sizeof(int)); + for (i = 0; i < count; i++) + { + if (!registration[i]) + { + g_error("registration too short: %s (%d digits required)", registration, count); + break; + } + if (registration[i] < '0' || registration[i] > '8') + { + g_error("registration invalid: %s (%c is not in 0..8)", registration, registration[i]); + break; + } + drawbars[i] = registration[i] - '0'; + } +} + +static void tonewheel_organ_destroyfunc(struct cbox_module *module) +{ +} + +#define SINGLE_SETTING(flag, max, field) \ + else if (!strcmp(cmd->command, flag) && !strcmp(cmd->arg_types, "i")) \ + { \ + int setting = CBOX_ARG_I(cmd, 0); \ + if (setting >= 0 && setting <= max) \ + m->field = setting; \ + return TRUE; \ + } \ + +gboolean tonewheel_organ_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct tonewheel_organ_module *m = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (int i = 0; i < 9; i++) + { + if (!cbox_execute_on(fb, NULL, "/upper_drawbar", "ii", error, i, m->upper_manual_drawbar_settings[i])) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/lower_drawbar", "ii", error, i, m->lower_manual_drawbar_settings[i])) + return FALSE; + } + for (int i = 0; i < 2; i++) + { + if (!cbox_execute_on(fb, NULL, "/pedal_drawbar", "ii", error, i, m->pedal_drawbar_settings[i])) + return FALSE; + } + return cbox_execute_on(fb, NULL, "/upper_vibrato", "i", error, m->enable_vibrato_upper) && + cbox_execute_on(fb, NULL, "/lower_vibrato", "i", error, m->enable_vibrato_lower) && + cbox_execute_on(fb, NULL, "/vibrato_mode", "i", error, m->vibrato_mode) && + cbox_execute_on(fb, NULL, "/vibrato_chorus", "i", error, m->vibrato_mix) && + cbox_execute_on(fb, NULL, "/percussion_enable", "i", error, m->enable_percussion) && + cbox_execute_on(fb, NULL, "/percussion_3rd", "i", error, m->percussion_3rd) && + CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); + } + else if (!strcmp(cmd->command, "/upper_drawbar") && !strcmp(cmd->arg_types, "ii")) + { + int drawbar = CBOX_ARG_I(cmd, 0); + int setting = CBOX_ARG_I(cmd, 1); + if (drawbar >= 0 && drawbar <= 8 && setting >= 0 && setting <= 8) + m->upper_manual_drawbar_settings[drawbar] = setting; + return TRUE; + } + else if (!strcmp(cmd->command, "/lower_drawbar") && !strcmp(cmd->arg_types, "ii")) + { + int drawbar = CBOX_ARG_I(cmd, 0); + int setting = CBOX_ARG_I(cmd, 1); + if (drawbar >= 0 && drawbar <= 8 && setting >= 0 && setting <= 8) + m->lower_manual_drawbar_settings[drawbar] = setting; + return TRUE; + } + SINGLE_SETTING("/upper_vibrato", 1, enable_vibrato_upper) + SINGLE_SETTING("/lower_vibrato", 1, enable_vibrato_lower) + SINGLE_SETTING("/vibrato_mode", 2, vibrato_mode) + SINGLE_SETTING("/vibrato_chorus", 1, vibrato_mix) + SINGLE_SETTING("/percussion_enable", 1, enable_percussion) + SINGLE_SETTING("/percussion_3rd", 1, percussion_3rd) + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); + return TRUE; +} + +MODULE_CREATE_FUNCTION(tonewheel_organ) +{ + static int inited = 0; + int i, srate; + const char *vibrato_mode; + if (!inited) + { + for (i = 0; i < 2048; i++) + { + float ph = i * M_PI / 1024; + sine_table[i] = (int)(32000 * sin(ph)); + complex_table[i] = (int)(32000 * (sin(ph) + sin(3 * ph) / 3 + sin(5 * ph) / 5 + sin(7 * ph) / 7 + sin(9 * ph) / 9 + sin(11 * ph) / 11)); + } + for (i = 0; i < 8192; i++) + { + float value = atan(sqrt(i * (4.0 / 8192))); + distortion_table[i] = ((int)(i * 2 + 32767 * value * value)) >> 1; + } + inited = 1; + } + + struct tonewheel_organ_module *m = malloc(sizeof(struct tonewheel_organ_module)); + CALL_MODULE_INIT(m, 0, 2, tonewheel_organ); + srate = m->module.srate; + m->module.process_event = tonewheel_organ_process_event; + m->module.process_block = tonewheel_organ_process_block; + cbox_onepole_reset(&m->filter_anticlick); + cbox_onepole_reset(&m->filter_overdrive); + cbox_onepole_set_lowpass(&m->filter_anticlick_coeffs, hz2w(180.0, srate)); + cbox_onepole_set_lowpass(&m->filter_overdrive_coeffs, hz2w(2500.0, srate)); + m->percussion = -1; + m->do_filter = 0; + m->cc91 = 0; + m->vibrato_phase = 0; + read_drawbars(m->upper_manual_drawbar_settings, 9, cbox_config_get_string_with_default(cfg_section, "upper_drawbars", "888000000")); + read_drawbars(m->lower_manual_drawbar_settings, 9, cbox_config_get_string_with_default(cfg_section, "lower_drawbars", "888800000")); + read_drawbars(m->pedal_drawbar_settings, 2, cbox_config_get_string_with_default(cfg_section, "pedal_drawbars", "82")); + m->enable_percussion = cbox_config_get_int(cfg_section, "percussion", 1); + m->enable_vibrato_upper = cbox_config_get_int(cfg_section, "vibrato_upper", 1); + m->enable_vibrato_lower = cbox_config_get_int(cfg_section, "vibrato_lower", 0); + m->percussion_3rd = cbox_config_get_int(cfg_section, "percussion_3rd", 1); + m->vibrato_dphase = (int)(6.6 / srate * 65536 * 65536); + + vibrato_mode = cbox_config_get_string_with_default(cfg_section, "vibrato_mode", "c3"); + if (vibrato_mode[0] == 'c') + m->vibrato_mix = 1; + else if (vibrato_mode[0] == 'v') + m->vibrato_mix = 0; + else + { + g_error("Unknown vibrato mode: %s (allowed: v1, v2, v3, c1, c2, c3)", vibrato_mode); + m->vibrato_mix = 0; + } + if (vibrato_mode[1] >= '1' && vibrato_mode[1] <= '3') + m->vibrato_mode = vibrato_mode[1] - '1'; + else + { + g_error("Unknown vibrato mode: %s (allowed: v1, v2, v3, c1, c2, c3)", vibrato_mode); + m->vibrato_mode = 2; + } + + for (i = 0; i < 18; i++) + { + biquad_init(&m->scanner_delay[i]); + } + for (i = 0; i < 91; i++) + { + float freq_hz = 440 * pow(2.0, (i - 45) / 12.0); + float scaling = freq_hz / 120.0; + if (scaling < 1) + scaling = 1; + if (scaling > 24) + scaling = 24 + ((scaling - 24) / 2.5); + m->frequency[i] = (uint32_t)(freq_hz * 65536 * 65536 / srate); + m->phase[i] = 0; + m->amp_scaling[i] = (int)(1024 * scaling); + } + m->upper_manual = 0; + m->lower_manual = 0; + m->pedalmasks = 0; + + return &m->module; +} + +struct cbox_module_keyrange_metadata tonewheel_organ_keyranges[] = { + { 0, 24, 35, "Pedal keyboard" }, + { 1, 36, 36 + 60, "Upper Manual" }, + { 2, 36, 36 + 60, "Lower Manual" }, +}; + +struct cbox_module_livecontroller_metadata tonewheel_organ_controllers[] = { + { 0, cmlc_onoffcc, 93, "Vib/Chr", NULL}, + + { 1, cmlc_continuouscc, 21, "Upper Drawbar 1", NULL}, + { 1, cmlc_continuouscc, 22, "Upper Drawbar 2", NULL}, + { 1, cmlc_continuouscc, 23, "Upper Drawbar 3", NULL}, + { 1, cmlc_continuouscc, 24, "Upper Drawbar 4", NULL}, + { 1, cmlc_continuouscc, 25, "Upper Drawbar 5", NULL}, + { 1, cmlc_continuouscc, 26, "Upper Drawbar 6", NULL}, + { 1, cmlc_continuouscc, 27, "Upper Drawbar 7", NULL}, + { 1, cmlc_continuouscc, 28, "Upper Drawbar 8", NULL}, + { 1, cmlc_continuouscc, 29, "Upper Drawbar 9", NULL}, + + { 2, cmlc_continuouscc, 21, "Lower Drawbar 1", NULL}, + { 2, cmlc_continuouscc, 22, "Lower Drawbar 2", NULL}, + { 2, cmlc_continuouscc, 23, "Lower Drawbar 3", NULL}, + { 2, cmlc_continuouscc, 24, "Lower Drawbar 4", NULL}, + { 2, cmlc_continuouscc, 25, "Lower Drawbar 5", NULL}, + { 2, cmlc_continuouscc, 26, "Lower Drawbar 6", NULL}, + { 2, cmlc_continuouscc, 27, "Lower Drawbar 7", NULL}, + { 2, cmlc_continuouscc, 28, "Lower Drawbar 8", NULL}, + { 2, cmlc_continuouscc, 29, "Lower Drawbar 9", NULL}, +}; + +DEFINE_MODULE(tonewheel_organ, 0, 2) + diff --git a/template/calfbox/track.c b/template/calfbox/track.c new file mode 100644 index 0000000..d3f3150 --- /dev/null +++ b/template/calfbox/track.c @@ -0,0 +1,279 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "errors.h" +#include "master.h" +#include "pattern.h" +#include "rt.h" +#include "seq.h" +#include "track.h" +#include "song.h" +#include +#include + +CBOX_CLASS_DEFINITION_ROOT(cbox_track) +CBOX_CLASS_DEFINITION_ROOT(cbox_track_item) + +static gboolean cbox_track_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); +static gboolean cbox_track_item_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error); + +void cbox_track_item_destroyfunc(struct cbox_objhdr *hdr) +{ + struct cbox_track_item *item = CBOX_H2O(hdr); + item->owner->items = g_list_remove(item->owner->items, item); + free(item); +} + +struct cbox_track *cbox_track_new(struct cbox_document *document) +{ + struct cbox_track *p = malloc(sizeof(struct cbox_track)); + CBOX_OBJECT_HEADER_INIT(p, cbox_track, document); + + p->name = g_strdup("Unnamed"); + p->items = NULL; + p->pb = NULL; + p->owner = NULL; + p->external_output_set = FALSE; + p->generation = 0; + p->mute = FALSE; + + cbox_command_target_init(&p->cmd_target, cbox_track_process_cmd, p); + CBOX_OBJECT_REGISTER(p); + return p; +} + +#define CBTI(it) ((struct cbox_track_item *)(it)->data) + +void cbox_track_add_item_to_list(struct cbox_track *track, struct cbox_track_item *item) +{ + GList *it = track->items; + while(it != NULL && CBTI(it)->time < item->time) + it = g_list_next(it); + // all items earlier than the new one -> append + if (it == NULL) + { + track->items = g_list_append(track->items, item); + cbox_track_set_dirty(track); + return; + } + // Here, I don't really care about overlaps - it's more important to preserve + // all clips as sent by the caller. + track->items = g_list_insert_before(track->items, it, item); + cbox_track_set_dirty(track); +} + +struct cbox_track_item *cbox_track_add_item(struct cbox_track *track, uint32_t time, struct cbox_midi_pattern *pattern, uint32_t offset, uint32_t length) +{ + struct cbox_track_item *item = malloc(sizeof(struct cbox_track_item)); + CBOX_OBJECT_HEADER_INIT(item, cbox_track_item, CBOX_GET_DOCUMENT(track)); + item->owner = track; + item->time = time; + item->pattern = pattern; + item->offset = offset; + item->length = length; + cbox_command_target_init(&item->cmd_target, cbox_track_item_process_cmd, item); + + cbox_track_add_item_to_list(track, item); + CBOX_OBJECT_REGISTER(item); + return item; +} + +void cbox_track_clear_clips(struct cbox_track *track) +{ + while(track->items) { + cbox_object_destroy(track->items->data); + } + cbox_track_set_dirty(track); +} + +void cbox_track_set_dirty(struct cbox_track *track) +{ + ++track->generation; +} + +void cbox_track_destroyfunc(struct cbox_objhdr *objhdr) +{ + struct cbox_track *track = CBOX_H2O(objhdr); + if (track->owner) + cbox_song_remove_track(track->owner, track); + // XXXKF I'm not sure if I want the lifecycle of track playback objects to be managed by the track itself + if (track->pb && track->pb->ref_count == 1) + cbox_track_playback_destroy(track->pb); + // The items will unlink themselves from the list in destructor + while(track->items) + cbox_object_destroy(track->items->data); + g_free((gchar *)track->name); + free(track); +} + +gboolean cbox_track_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_track *track = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + GList *it = track->items; + while(it != NULL) + { + struct cbox_track_item *trki = it->data; + if (!cbox_execute_on(fb, NULL, "/clip", "iiioo", error, trki->time, trki->offset, trki->length, trki->pattern, trki)) + return FALSE; + it = g_list_next(it); + } + + return cbox_execute_on(fb, NULL, "/mute", "i", error, track->mute) && + cbox_execute_on(fb, NULL, "/name", "s", error, track->name) && + (track->external_output_set ? cbox_uuid_report_as(&track->external_output, "/external_output", fb, error) : TRUE) && + CBOX_OBJECT_DEFAULT_STATUS(track, fb, error); + } + else if (!strcmp(cmd->command, "/clear_clips") && !strcmp(cmd->arg_types, "")) + { + cbox_track_clear_clips(track); + return TRUE; + } + else if (!strcmp(cmd->command, "/add_clip") && !strcmp(cmd->arg_types, "iiis")) + { + int pos = CBOX_ARG_I(cmd, 0); + int offset = CBOX_ARG_I(cmd, 1); + int length = CBOX_ARG_I(cmd, 2); + if (pos < 0) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid pattern position %d (cannot be negative)", pos); + return FALSE; + } + if (offset < 0) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid pattern offset %d (cannot be negative)", offset); + return FALSE; + } + if (length <= 0) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid pattern length %d (must be positive)", length); + return FALSE; + } + struct cbox_objhdr *pattern = CBOX_ARG_O(cmd, 3, track, cbox_midi_pattern, error); + if (!pattern) + return FALSE; + struct cbox_midi_pattern *mp = CBOX_H2O(pattern); + struct cbox_track_item *trki = cbox_track_add_item(track, pos, mp, offset, length); + if (fb) + return cbox_execute_on(fb, NULL, "/uuid", "o", error, trki); + cbox_track_set_dirty(track); + return TRUE; + } + else if (!strcmp(cmd->command, "/name") && !strcmp(cmd->arg_types, "s")) + { + char *old_name = track->name; + track->name = g_strdup(CBOX_ARG_S(cmd, 0)); + g_free(old_name); + cbox_track_set_dirty(track); + return TRUE; + } + else if (!strcmp(cmd->command, "/external_output") && !strcmp(cmd->arg_types, "s")) + { + if (*CBOX_ARG_S(cmd, 0)) + { + if (cbox_uuid_fromstring(&track->external_output, CBOX_ARG_S(cmd, 0), error)) { + track->external_output_set = TRUE; + cbox_track_set_dirty(track); + } + } + else { + track->external_output_set = FALSE; + cbox_track_set_dirty(track); + } + return TRUE; + } + else if (!strcmp(cmd->command, "/mute") && !strcmp(cmd->arg_types, "i")) + { + if (CBOX_ARG_I(cmd, 0) == (int)track->mute) // no-op + return TRUE; + track->mute = CBOX_ARG_I(cmd, 0) != 0; + cbox_track_set_dirty(track); + return TRUE; + } + else + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +gboolean cbox_track_item_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_track_item *trki = ct->user_data; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + return cbox_execute_on(fb, NULL, "/pos", "i", error, trki->time) && + cbox_execute_on(fb, NULL, "/offset", "i", error, trki->offset) && + cbox_execute_on(fb, NULL, "/length", "i", error, trki->length) && + cbox_execute_on(fb, NULL, "/pattern", "o", error, trki->pattern) && + CBOX_OBJECT_DEFAULT_STATUS(trki, fb, error); + } + if (!strcmp(cmd->command, "/delete") && !strcmp(cmd->arg_types, "")) + { + cbox_track_set_dirty(trki->owner); + cbox_object_destroy(CBOX_O2H(trki)); + return TRUE; + } + if (!strcmp(cmd->command, "/pattern") && !strcmp(cmd->arg_types, "s")) + { + struct cbox_objhdr *pattern = CBOX_ARG_O(cmd, 0, trki->owner, cbox_midi_pattern, error); + if (!pattern) + return FALSE; + if (trki->pattern == CBOX_H2O(pattern)) // no-op + return TRUE; + trki->pattern = CBOX_H2O(pattern); + cbox_track_item_set_dirty(trki); + return TRUE; + } + if (!strcmp(cmd->command, "/length") && !strcmp(cmd->arg_types, "i")) + { + if (CBOX_ARG_I(cmd, 0) == (int)trki->length) // no-op + return TRUE; + trki->length = CBOX_ARG_I(cmd, 0); + cbox_track_item_set_dirty(trki); + return TRUE; + } + if (!strcmp(cmd->command, "/pos") && !strcmp(cmd->arg_types, "i")) + { + if (CBOX_ARG_I(cmd, 0) == (int)trki->time) // no-op + return TRUE; + trki->owner->items = g_list_remove(trki->owner->items, trki); + trki->time = CBOX_ARG_I(cmd, 0); + cbox_track_add_item_to_list(trki->owner, trki); + cbox_track_item_set_dirty(trki); + return TRUE; + } + if (!strcmp(cmd->command, "/offset") && !strcmp(cmd->arg_types, "i")) + { + if (CBOX_ARG_I(cmd, 0) == (int)trki->offset) // no-op + return TRUE; + cbox_track_item_set_dirty(trki); + trki->offset = CBOX_ARG_I(cmd, 0); + return TRUE; + } + return cbox_object_default_process_cmd(ct, fb, cmd, error); +} + +extern void cbox_track_item_set_dirty(struct cbox_track_item *track_item) +{ + cbox_track_set_dirty(track_item->owner); +} diff --git a/template/calfbox/track.h b/template/calfbox/track.h new file mode 100644 index 0000000..8147084 --- /dev/null +++ b/template/calfbox/track.h @@ -0,0 +1,62 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_TRACK_H +#define CBOX_TRACK_H + +#include "dom.h" + +CBOX_EXTERN_CLASS(cbox_track_item) +CBOX_EXTERN_CLASS(cbox_track) + +struct cbox_midi_pattern; +struct cbox_track; + +struct cbox_track_item +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + struct cbox_track *owner; + uint32_t time; + struct cbox_midi_pattern *pattern; + uint32_t offset; + uint32_t length; +}; + +struct cbox_track +{ + CBOX_OBJECT_HEADER() + struct cbox_command_target cmd_target; + gchar *name; + gboolean external_output_set; + struct cbox_uuid external_output; + GList *items; + struct cbox_song *owner; + struct cbox_track_playback *pb; + uint32_t generation; + gboolean mute; +}; + +extern struct cbox_track *cbox_track_new(struct cbox_document *document); +extern struct cbox_track_item *cbox_track_add_item(struct cbox_track *track, uint32_t time, struct cbox_midi_pattern *pattern, uint32_t offset, uint32_t length); +extern void cbox_track_update_playback(struct cbox_track *track, struct cbox_master *master); +extern void cbox_track_clear_clips(struct cbox_track *track); +extern void cbox_track_set_dirty(struct cbox_track *track); +extern void cbox_track_item_set_dirty(struct cbox_track_item *track_item); + +#endif diff --git a/template/calfbox/ui.c b/template/calfbox/ui.c new file mode 100644 index 0000000..62abc0d --- /dev/null +++ b/template/calfbox/ui.c @@ -0,0 +1,65 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#include "ui.h" + +#if USE_NCURSES + +#include +#include +#include + +void cbox_ui_start() +{ + initscr(); + cbreak(); + noecho(); + start_color(); + keypad(stdscr, TRUE); +} + +int cbox_ui_run(struct cbox_ui_page *page) +{ + int ch, res; + if (page->draw) + page->draw(page); + if (page->on_idle) + halfdelay(1); + while(1) + { + ch = getch(); + if (ch == ERR && page->on_idle) + { + res = page->on_idle(page); + if (res != 0) + return res; + continue; + } + res = page->on_key(page, ch); + if (res != 0) + return res; + } + cbreak(); +} + +void cbox_ui_stop() +{ + endwin(); +} + +#endif \ No newline at end of file diff --git a/template/calfbox/ui.h b/template/calfbox/ui.h new file mode 100644 index 0000000..1f977b9 --- /dev/null +++ b/template/calfbox/ui.h @@ -0,0 +1,40 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_UI_H +#define CBOX_UI_H + +#include "config.h" + +#if USE_NCURSES + +struct cbox_ui_page +{ + void *user_data; + void (*draw)(struct cbox_ui_page *page); + int (*on_key)(struct cbox_ui_page *page, int ch); + int (*on_idle)(struct cbox_ui_page *page); +}; + +extern void cbox_ui_start(void); +extern int cbox_ui_run(struct cbox_ui_page *page); +extern void cbox_ui_stop(void); + +#endif + +#endif diff --git a/template/calfbox/usb_api_example.py b/template/calfbox/usb_api_example.py new file mode 100644 index 0000000..15e518e --- /dev/null +++ b/template/calfbox/usb_api_example.py @@ -0,0 +1,61 @@ +from calfbox import cbox + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() + +cbox.Config.add_section("drumpattern:pat1", """ +title=Straight - Verse +beats=4 +track1=bd +track2=sd +track3=hh +track4=ho +bd_note=c1 +sd_note=d1 +hh_note=f#1 +ho_note=a#1 +bd_trigger=9... .... 9.6. .... +sd_trigger=.... 9..5 .2.. 9... +hh_trigger=9353 7353 7353 73.3 +ho_trigger=.... .... .... ..3. +""") + +cbox.Config.set("io", "use_usb", 1) +cbox.start_audio(cmd_dumper) + +global Document +Document = cbox.Document + +status = cbox.JackIO.status() +client_name = status.client_name +print ("Client type: %s" % status.client_type) +print ("Client name: %s" % client_name) +print ("Audio inputs: %d, outputs: %d" % (status.audio_inputs, status.audio_outputs)) +print ("Period: %d frames" % (status.buffer_size)) +print ("Sample rate: %d frames/sec" % (status.sample_rate)) +print ("Output resolution: %d bits/sample" % (status.output_resolution)) +print ("MIDI input devices: %s" % (status.midi_input)) +#cbox.JackIO.create_midi_output('drums', 'system:midi_playback_1') + +scene = Document.get_scene() +scene.clear() +instrument = scene.add_new_instrument_layer("test_sampler", "sampler").get_instrument() +pgm_no = instrument.engine.get_unused_program() +pgm = instrument.engine.load_patch_from_file(pgm_no, 'synthbass.sfz', 'SynthBass') +instrument.engine.set_patch(1, pgm_no) +instrument.engine.set_patch(10, pgm_no) + +song = Document.get_song() +track = song.add_track() +pattern = song.load_drum_pattern("pat1") +track.add_clip(0, 0, pattern.status().loop_end, pattern) +song.set_loop(0, pattern.status().loop_end) +song.update_playback() +cbox.Transport.play() + +print("Ready!") + +while True: + cbox.call_on_idle(cmd_dumper) diff --git a/template/calfbox/usbaudio.c b/template/calfbox/usbaudio.c new file mode 100644 index 0000000..deffaa0 --- /dev/null +++ b/template/calfbox/usbaudio.c @@ -0,0 +1,679 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config.h" + +#if USE_LIBUSB + +#include +#include +#include "usbio_impl.h" + +#include +#include +#include + +#define NUM_MULTIMIX_SYNC_PACKETS 10 + +#define MULTIMIX_EP_PLAYBACK 0x02 +//#define MULTIMIX_EP_CAPTURE 0x86 +#define MULTIMIX_EP_SYNC 0x81 + +#define NUM_CPUTIME_ENTRIES 100 + +static int register_cpu_time = 1; +static float real_time_registry[NUM_CPUTIME_ENTRIES]; +static float cpu_time_registry[NUM_CPUTIME_ENTRIES]; +static int cpu_time_write_ptr = 0; +static void usbio_play_buffer_adaptive(struct cbox_usb_io_impl *uii); + +/////////////////////////////////////////////////////////////////////////////// + +static gboolean set_endpoint_sample_rate(struct libusb_device_handle *h, int sample_rate, int ep) +{ + uint8_t freq_data[3]; + freq_data[0] = sample_rate & 0xFF; + freq_data[1] = (sample_rate & 0xFF00) >> 8; + freq_data[2] = (sample_rate & 0xFF0000) >> 16; + if (libusb_control_transfer(h, 0x22, 0x01, 256, ep, freq_data, 3, USB_DEVICE_SETUP_TIMEOUT) != 3) + return FALSE; + return TRUE; +} + +/////////////////////////////////////////////////////////////////////////////// + +gboolean usbio_open_audio_interface(struct cbox_usb_io_impl *uii, struct cbox_usb_audio_info *uainf, struct libusb_device_handle *handle, GError **error) +{ + if (uii->output_resolution != 2 && uii->output_resolution != 3) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Only 16-bit or 24-bit output resolution is supported."); + return FALSE; + } + if (!configure_usb_interface(handle, uainf->udi->bus, uainf->udi->devadr, uainf->intf, uainf->alt_setting, "audio (class driver)", error)) + return FALSE; + if (!set_endpoint_sample_rate(handle, uii->sample_rate, uainf->epdesc.bEndpointAddress)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot set sample rate on class-compliant USB audio device."); + return FALSE; + } + uii->is_hispeed = FALSE; + uii->sync_protocol = uainf->sync_protocol; + uii->play_function = uainf->sync_protocol == USBAUDIOSYNC_PROTOCOL_CLASS ? usbio_play_buffer_asynchronous : usbio_play_buffer_adaptive; + uii->handle_audiodev = handle; + uii->audio_output_endpoint = uainf->epdesc.bEndpointAddress; + uii->audio_output_pktsize = uainf->epdesc.wMaxPacketSize; // 48 * 2 * uii->output_resolution; + uii->audio_sync_endpoint = uainf->sync_protocol == USBAUDIOSYNC_PROTOCOL_CLASS ? uainf->sync_epdesc.bEndpointAddress : 0; + return TRUE; +} + +/////////////////////////////////////////////////////////////////////////////// + +static gboolean claim_multimix_interfaces(struct cbox_usb_io_impl *uii, struct libusb_device_handle *handle, int bus, int devadr, GError **error) +{ + for (int ifno = 0; ifno < 2; ifno++) + { + if (!configure_usb_interface(handle, bus, devadr, ifno, 1, "audio (MultiMix driver)", error)) + return FALSE; + } + return TRUE; +} + +gboolean usbio_open_audio_interface_multimix(struct cbox_usb_io_impl *uii, int bus, int devadr, struct libusb_device_handle *handle, GError **error) +{ + if (uii->output_resolution != 3) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Only 24-bit output resolution is supported."); + return FALSE; + } + if (!claim_multimix_interfaces(uii, handle, bus, devadr, error)) + return FALSE; + if (!set_endpoint_sample_rate(handle, uii->sample_rate, MULTIMIX_EP_PLAYBACK)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot set sample rate on Alesis Multimix."); + return FALSE; + } + + uii->is_hispeed = TRUE; + uii->play_function = usbio_play_buffer_asynchronous; + uii->handle_audiodev = handle; + uii->sync_protocol = USBAUDIOSYNC_PROTOCOL_MULTIMIX8; + uii->audio_output_endpoint = MULTIMIX_EP_PLAYBACK; + uii->audio_output_pktsize = 156; + uii->audio_sync_endpoint = MULTIMIX_EP_SYNC; + return TRUE; +} + +/////////////////////////////////////////////////////////////////////////////// + +static void calc_output_buffer(struct cbox_usb_io_impl *uii) +{ + struct timespec tvs1, tve1, tvs2, tve2; + if (register_cpu_time) + { + clock_gettime(CLOCK_MONOTONIC_RAW, &tvs1); + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tvs2); + } + struct cbox_io *io = uii->ioi.pio; + uint32_t buffer_size = io->io_env.buffer_size; + for (int b = 0; b < uii->output_channels; b++) + memset(io->output_buffers[b], 0, buffer_size * sizeof(float)); + for (GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + if (umi->input_port->hdr.enable_appsink && umi->input_port && umi->input_port->hdr.buffer.count) + cbox_midi_appsink_supply(&umi->input_port->hdr.appsink, &umi->input_port->hdr.buffer, io->free_running_frame_counter); + } + io->cb->process(io->cb->user_data, io, buffer_size); + for (GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + if (umi->input_port) + cbox_midi_buffer_clear(&umi->input_port->hdr.buffer); + } + for (GSList *p = io->midi_outputs; p; p = p->next) + { + struct cbox_usb_midi_output *umo = p->data; + usbio_fill_midi_output_buffer(umo); + } + if (register_cpu_time) + { + clock_gettime(CLOCK_MONOTONIC_RAW, &tve1); + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tve2); + float time1 = tve1.tv_sec - tvs1.tv_sec + (tve1.tv_nsec - tvs1.tv_nsec) / 1000000000.0; + float time2 = tve2.tv_sec - tvs2.tv_sec + (tve2.tv_nsec - tvs2.tv_nsec) / 1000000000.0; + real_time_registry[cpu_time_write_ptr] = time1; + cpu_time_registry[cpu_time_write_ptr] = time2; + cpu_time_write_ptr = (cpu_time_write_ptr + 1) % NUM_CPUTIME_ENTRIES; + if (time1 > 0.0008 || time2 > 0.0008) + g_warning("CPU time = %f ms, real time = %f ms", time2 * 1000, time1 * 1000); + } + io->free_running_frame_counter += buffer_size; +} + +static void fill_playback_buffer(struct cbox_usb_io_impl *uii, struct libusb_transfer *transfer) +{ + struct cbox_io *io = uii->ioi.pio; + uint32_t buffer_size = io->io_env.buffer_size; + uint8_t *data8 = (uint8_t*)transfer->buffer; + int16_t *data = (int16_t*)transfer->buffer; + int resolution = uii->output_resolution; + unsigned int oc = uii->output_channels; + uint32_t rptr = uii->read_ptr; + uint32_t nframes = transfer->length / (resolution * oc); + uint32_t i, b, j; + + for (i = 0; i < nframes; ) + { + if (rptr == buffer_size) + { + calc_output_buffer(uii); + rptr = 0; + } + unsigned int left1 = nframes - i; + unsigned int left2 = buffer_size - rptr; + if (left1 > left2) + left1 = left2; + + for (b = 0; b < oc; b++) + { + float *obuf = io->output_buffers[b] + rptr; + if (resolution == 2) + { + int16_t *tbuf = data + oc * i + b; + for (j = 0; j < left1; j++) + { + float v = 32767 * obuf[j]; + if (v < -32768) + v = -32768; + if (v > +32767) + v = +32767; + *tbuf = (int16_t)v; + tbuf += oc; + } + } + if (resolution == 3) + { + uint8_t *tbuf = data8 + (oc * i + b) * 3; + for (j = 0; j < left1; j++) + { + float v = 0x7FFFFF * obuf[j]; + if (v < -0x800000) + v = -0x800000; + if (v > +0x7FFFFF) + v = +0x7FFFFF; + int vi = (int)v; + tbuf[0] = vi & 255; + tbuf[1] = (vi >> 8) & 255; + tbuf[2] = (vi >> 16) & 255; + tbuf += oc * 3; + } + } + } + i += left1; + rptr += left1; + } + uii->read_ptr = rptr; +} + +static void play_callback_adaptive(struct libusb_transfer *transfer) +{ + struct usbio_transfer *xf = transfer->user_data; + struct cbox_usb_io_impl *uii = xf->user_data; + xf->pending = FALSE; + if (transfer->status == LIBUSB_TRANSFER_CANCELLED) + { + xf->cancel_confirm = 1; + return; + } + if (transfer->status == LIBUSB_TRANSFER_NO_DEVICE) + { + uii->device_removed++; + return; + } + + int resolution = uii->output_resolution; + int oc = uii->output_channels; + gboolean init_finished = uii->playback_counter == uii->playback_buffers; + if (uii->playback_counter < uii->playback_buffers) + { + // send another URB for the next transfer before re-submitting + // this one + usbio_play_buffer_adaptive(uii); + } + // printf("Play Callback! %d %p status %d\n", (int)transfer->length, transfer->buffer, (int)transfer->status); + + int tlen = 0, olen = 0; + for (int i = 0; i < transfer->num_iso_packets; i++) + { + tlen += transfer->iso_packet_desc[i].actual_length; + olen += transfer->iso_packet_desc[i].length; + if (transfer->iso_packet_desc[i].status) + printf("ISO error: index = %d i = %d status = %d\n", (int)xf->index, i, transfer->iso_packet_desc[i].status); + } + uii->samples_played += olen / (oc * resolution); + uint32_t nsamps = uii->sample_rate / 1000; + // If time elapsed is greater than + int lag = uii->desync / (1000 * transfer->num_iso_packets); + if (lag > 0 && nsamps < uii->audio_output_pktsize) + { + nsamps++; + lag--; + } + + transfer->length = nsamps * transfer->num_iso_packets * oc * resolution; + libusb_set_iso_packet_lengths(transfer, nsamps * oc * resolution); + + if (init_finished) + { + fill_playback_buffer(uii, transfer); + } + // desync value is expressed in milli-frames, i.e. desync of 1000 means 1 frame of lag + // It takes 1ms for each iso packet to be transmitted. Each transfer consists of + // num_iso_packets packets. So, this transfer took uii->sample_rate milli-frames. + uii->desync += transfer->num_iso_packets * uii->sample_rate; + // ... but during that time, tlen/4 samples == tlen/4*1000 millisamples have been + // transmitted. + uii->desync -= transfer->num_iso_packets * nsamps * 1000; + + if (uii->no_resubmit) + return; + int err = usbio_transfer_submit(xf); + if (err) + { + if (err == LIBUSB_ERROR_NO_DEVICE) + { + uii->device_removed++; + transfer->status = LIBUSB_TRANSFER_NO_DEVICE; + } + g_warning("Cannot resubmit isochronous transfer, error = %s", libusb_error_name(err)); + } +} + +void usbio_play_buffer_adaptive(struct cbox_usb_io_impl *uii) +{ + struct usbio_transfer *t; + int err; + int packets = uii->iso_packets; + t = usbio_transfer_new(uii->usbctx, "play", uii->playback_counter, packets, uii); + int tsize = uii->sample_rate * 2 * uii->output_resolution / 1000; + uint8_t *buf = (uint8_t *)calloc(packets, uii->audio_output_pktsize); + + libusb_fill_iso_transfer(t->transfer, uii->handle_audiodev, uii->audio_output_endpoint, buf, tsize * packets, packets, play_callback_adaptive, t, 20000); + libusb_set_iso_packet_lengths(t->transfer, tsize); + uii->playback_transfers[uii->playback_counter++] = t; + + err = usbio_transfer_submit(t); + if (!err) + return; + g_warning("Cannot resubmit isochronous transfer, error = %s", libusb_error_name(err)); + uii->playback_counter--; + free(buf); + usbio_transfer_destroy(t); + uii->playback_transfers[uii->playback_counter] = NULL; +} + +////////////////////////////////////////////////////////////////////////////////////////// + +static int calc_packet_lengths(struct cbox_usb_io_impl *uii, struct libusb_transfer *t, int packets) +{ + int packets_per_sec = uii->is_hispeed ? 8000 : 1000; + int tsize = 0; + int i; + // printf("sync_freq = %d\n", sync_freq); + for (i = 0; i < packets; i++) + { + int nsamps = (uii->samples_per_sec - uii->desync) / packets_per_sec; + // assert(nsamps > 0); + if ((uii->samples_per_sec - uii->desync) % packets_per_sec) + nsamps++; + //printf("%d sfreq=%d desync=%d nsamps=%d\n", i, uii->sync_freq, uii->desync, nsamps); + + uii->desync = (uii->desync + nsamps * packets_per_sec) % uii->samples_per_sec; + int v = (nsamps) * 2 * uii->output_resolution; + t->iso_packet_desc[i].length = v; + tsize += v; + } + return tsize; +} + +void play_callback_asynchronous(struct libusb_transfer *transfer) +{ + struct usbio_transfer *xf = transfer->user_data; + struct cbox_usb_io_impl *uii = xf->user_data; + xf->pending = FALSE; + + if (transfer->status == LIBUSB_TRANSFER_CANCELLED) + { + xf->cancel_confirm = TRUE; + return; + } + if (transfer->status == LIBUSB_TRANSFER_NO_DEVICE) + { + xf->cancel_confirm = TRUE; + uii->device_removed++; + return; + } + + gboolean init_finished = uii->playback_counter == uii->playback_buffers; + if (uii->playback_counter < uii->playback_buffers) + { + // send another URB for the next transfer before re-submitting + // this one + usbio_play_buffer_asynchronous(uii); + } + + /* + printf("Play Callback! %d status %d\n", (int)transfer->length, (int)transfer->status); + for (i = 0; i < transfer->num_iso_packets; i++) { + if (transfer->iso_packet_desc[i].actual_length) + { + printf("%d: %d %d\n", i, transfer->iso_packet_desc[i].actual_length, transfer->iso_packet_desc[i].status); + } + } + */ + transfer->length = calc_packet_lengths(uii, transfer, transfer->num_iso_packets); + if (init_finished) + { + fill_playback_buffer(uii, transfer); + } + if (uii->no_resubmit) + return; + int err = usbio_transfer_submit(xf); + if (err) + { + if (err == LIBUSB_ERROR_NO_DEVICE) + { + transfer->status = LIBUSB_TRANSFER_NO_DEVICE; + uii->device_removed++; + } + g_warning("Cannot submit isochronous transfer, error = %s", libusb_error_name(err)); + } +} + +static struct usbio_transfer *sync_stuff_asynchronous(struct cbox_usb_io_impl *uii, int index); + +void usbio_play_buffer_asynchronous(struct cbox_usb_io_impl *uii) +{ + struct usbio_transfer *t; + int err; + int packets = uii->iso_packets_multimix; + t = usbio_transfer_new(uii->usbctx, "play", uii->playback_counter, packets, uii); + int tsize = calc_packet_lengths(uii, t->transfer, packets); + int bufsize = uii->audio_output_pktsize * packets; + uint8_t *buf = (uint8_t *)calloc(1, bufsize); + + if (!uii->playback_counter) + { + for(uii->sync_counter = 0; uii->sync_counter < uii->sync_buffers; uii->sync_counter++) + uii->sync_transfers[uii->sync_counter] = sync_stuff_asynchronous(uii, uii->sync_counter); + } + + libusb_fill_iso_transfer(t->transfer, uii->handle_audiodev, uii->audio_output_endpoint, buf, tsize, packets, play_callback_asynchronous, t, 20000); + uii->playback_transfers[uii->playback_counter++] = t; + err = usbio_transfer_submit(t); + if (err) + { + g_warning("Cannot submit playback urb: %s, error = %s (index = %d, tsize = %d)", libusb_error_name(err), strerror(errno), uii->playback_counter, tsize); + free(buf); + usbio_transfer_destroy(t); + uii->playback_transfers[--uii->playback_counter] = NULL; + } +} + +////////////////////////////////////////////////////////////////////////////////////////// + +/* + * The Multimix device controls the data rate of the playback stream using a + * device-to-host isochronous endpoint. The incoming packets consist of 3 bytes: + * a current value of sample rate (kHz) + 2 historical values. I'm only using + * the first byte, I haven't yet encountered a situation where using the + * second and third byte would be necessary. This seems to work for all + * sample rates supported by the Windows driver - 44100, 48000, 88200 and + * 96000. It is possible to set sample rate to 64000, but it doesn't work + * correctly, and isn't supported by the Windows driver either - it may + * require special handling or may be a half-implemented feature in hardware. + * The isochronous transfer using 10 packets seems to give acceptable resolution + * and latency to avoid over/underruns with supported sample rates. + * + * The non-integer multiples of 1 kHz (like 44.1) are passed as a sequence of + * values that average to a desired value (9 values of 44 and one value of 45). + * + * In order to compensate for clock rate difference + * between host clock and DAC clock, the sample rate values sent by the device + * are either larger (to increase data rate from the host) or smaller than + * the nominal frequency value - the driver uses that to adjust the sample frame + * counts of individual packets in an isochronous transfer. + * + * A similar mechanism is used with USB audio class asynchronous sinks: the + * device sends a 10.14 representation of number of audio samples (frames) + * per USB frame, and this is used to control packet sizes. + */ + +static void use_audioclass_sync_feedback(const uint8_t *data, uint32_t actual_length, int *prate) +{ + // 10.14 representation of 'samples' per ms value, see 5.12.4.2 of the USB spec + uint32_t value = data[0] + 256 * data[1] + 65536 * data[2]; + // printf("Ff estimate = %f Hz\n", value * 1000 / 16384.0); + // only 1 packet + *prate = (1000 * value + 8192) >> 14; +} + +static inline void use_multimix_sync_feedback(const uint8_t *data, uint32_t actual_length, int *prate, const uint8_t *prev_data) +{ + // Those assertions never failed, so my understanding of the protocol was likely + // correct. They should probably be removed just to not crash the program when + // used with flaky devices. + assert(actual_length == 3); + // this is averaged across all packets in the transfer + *prate += 1000 * data[0]; + if (prev_data) + assert(data[1] == prev_data[0]); +} + +static void sync_callback(struct libusb_transfer *transfer) +{ + struct usbio_transfer *xf = transfer->user_data; + struct cbox_usb_io_impl *uii = xf->user_data; + uint8_t *data = transfer->buffer; + int i, ofs, pkts; + int rate_est = 0; + xf->pending = FALSE; + + if (transfer->status == LIBUSB_TRANSFER_CANCELLED) + { + xf->cancel_confirm = 1; + return; + } + if (transfer->status == LIBUSB_TRANSFER_NO_DEVICE) + { + xf->cancel_confirm = 1; + return; + } + // XXXKF handle device disconnected error + + if (uii->debug_sync) + printf("Sync callback! %p %d packets:", transfer, transfer->num_iso_packets); + ofs = 0; + pkts = 0; + rate_est = 0; + const uint8_t *prev_data = NULL; + for (i = 0; i < transfer->num_iso_packets; i++) { + if (transfer->iso_packet_desc[i].status) + { + printf("[%02d: actual length is %4d, status is %2d] ", i, transfer->iso_packet_desc[i].actual_length, transfer->iso_packet_desc[i].status); + continue; + } + else if (transfer->iso_packet_desc[i].actual_length) + { + if (uii->sync_protocol == USBAUDIOSYNC_PROTOCOL_MULTIMIX8) + use_multimix_sync_feedback(&data[ofs], transfer->iso_packet_desc[i].actual_length, &rate_est, prev_data); + else + use_audioclass_sync_feedback(&data[ofs], transfer->iso_packet_desc[i].actual_length, &rate_est); + prev_data = &data[ofs]; + //printf("%d\n", (int)data[ofs]); + if (uii->debug_sync) + printf("%3d ", (int)data[ofs]); + pkts++; + } + else + if (uii->debug_sync) + printf("? "); + ofs += transfer->iso_packet_desc[i].length; + } + if (uii->debug_sync) + printf(" (%d of %d)", pkts, transfer->num_iso_packets); + if (pkts == transfer->num_iso_packets) + { + // Divide by the number of packets to compute the mean rate (in case + // of Multimix we don't have a proper fractional value, just an integer, + // so averaging is required to get more accuracy). + if (rate_est) + uii->samples_per_sec = rate_est / pkts; + if (uii->debug_sync) + printf("rate_est = %d sync_freq = %d\n", rate_est, uii->samples_per_sec); + } + if (uii->no_resubmit) + return; + int err = usbio_transfer_submit(xf); + if (err) + { + if (err == LIBUSB_ERROR_NO_DEVICE) + { + return; + } + } + if (uii->debug_sync) + printf("\n"); +} + +struct usbio_transfer *sync_stuff_asynchronous(struct cbox_usb_io_impl *uii, int index) +{ + struct usbio_transfer *t; + int err; + int syncbufsize = uii->sync_protocol == USBAUDIOSYNC_PROTOCOL_MULTIMIX8 ? 64 : 3; + int syncbufcount = uii->sync_protocol == USBAUDIOSYNC_PROTOCOL_MULTIMIX8 ? NUM_MULTIMIX_SYNC_PACKETS : 1; + t = usbio_transfer_new(uii->usbctx, "sync", index, syncbufcount, uii); + uint8_t *sync_buf = (uint8_t *)calloc(syncbufcount, syncbufsize); + libusb_fill_iso_transfer(t->transfer, uii->handle_audiodev, uii->audio_sync_endpoint, sync_buf, syncbufsize * syncbufcount, syncbufcount, sync_callback, t, 20000); + libusb_set_iso_packet_lengths(t->transfer, syncbufsize); + + err = libusb_submit_transfer(t->transfer); + if (err) + { + g_warning("Cannot submit sync urb: %s", libusb_error_name(err)); + free(sync_buf); + usbio_transfer_destroy(t); + return NULL; + } + return t; +} + +/////////////////////////////////////////////////////////////////////////// + +void cbox_usb_audio_info_init(struct cbox_usb_audio_info *uai, struct cbox_usb_device_info *udi) +{ + uai->udi = udi; + uai->intf = -1; + uai->alt_setting = -1; + uai->epdesc.found = FALSE; + uai->sync_protocol = USBAUDIOSYNC_PROTOCOL_NONE; + uai->sync_epdesc.found = FALSE; +} + +void usbio_start_audio_playback(struct cbox_usb_io_impl *uii) +{ + uii->desync = 0; + uii->samples_played = 0; + uii->read_ptr = uii->ioi.pio->io_env.buffer_size; + + uii->playback_transfers = malloc(sizeof(struct libusb_transfer *) * uii->playback_buffers); + uii->sync_transfers = malloc(sizeof(struct libusb_transfer *) * uii->sync_buffers); + + uii->playback_counter = 0; + uii->device_removed = 0; + uii->samples_per_sec = uii->sample_rate; + uii->play_function(uii); + uii->setup_error = uii->playback_counter == 0; + + if (!uii->setup_error) + { + while(uii->playback_counter < uii->playback_buffers && !uii->device_removed) + libusb_handle_events(uii->usbctx); + } +} + +void usbio_stop_audio_playback(struct cbox_usb_io_impl *uii) +{ + if (uii->device_removed) + { + // Wait until all the transfers pending are finished + while(uii->device_removed < uii->playback_counter) + libusb_handle_events(uii->usbctx); + } + if (uii->device_removed || uii->setup_error) + { + // Run the DSP code and send output to bit bucket until engine is stopped. + // This ensures that the command queue will still be processed. + // Otherwise the GUI thread would hang waiting for the command from + // the queue to be completed. + g_message("USB Audio output device has been disconnected - switching to null output."); + usbio_run_idle_loop(uii); + } + else + { + // Cancel all transfers pending, and wait until they get cancelled + for (unsigned int i = 0; i < uii->playback_counter; i++) + { + if (uii->playback_transfers[i]) + usbio_transfer_shutdown(uii->playback_transfers[i]); + } + } + + // Free the transfers for the buffers allocated so far. In case of setup + // failure, some buffers transfers might not have been created yet. + for (unsigned int i = 0; i < uii->playback_counter; i++) + { + if (uii->playback_transfers[i]) + { + free(uii->playback_transfers[i]->transfer->buffer); + usbio_transfer_destroy(uii->playback_transfers[i]); + uii->playback_transfers[i] = NULL; + } + } + if (uii->playback_counter && uii->audio_sync_endpoint) + { + for (unsigned int i = 0; i < uii->sync_counter; i++) + { + if (uii->sync_transfers[i]) + usbio_transfer_shutdown(uii->sync_transfers[i]); + } + for (unsigned int i = 0; i < uii->sync_counter; i++) + { + if (uii->sync_transfers[i]) + { + free(uii->sync_transfers[i]->transfer->buffer); + usbio_transfer_destroy(uii->sync_transfers[i]); + uii->sync_transfers[i] = NULL; + } + } + } + free(uii->playback_transfers); + free(uii->sync_transfers); +} + +#endif diff --git a/template/calfbox/usbio.c b/template/calfbox/usbio.c new file mode 100644 index 0000000..ee7c13f --- /dev/null +++ b/template/calfbox/usbio.c @@ -0,0 +1,502 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +/* + +Note: this is a silly experimental driver for a number of USB MIDI devices. + +It only supports audio output and MIDI input, as those are my immediate +needs, to be able to use a machine running CalfBox as a standalone MIDI +instrument. Plug-and-play is supported, as long as current user running +calfbox has write access to the USB devices involved. This can be done by +running calfbox as root, or by setting right permissions in udev scripts +- this may be considered a safer method. + +Devices supported: +* Class-compliant audio output devices (tested with Lexicon Omega and some + cheap no-brand C-Media USB soundcard dongle) +* Alesis Multimix 8 USB 2.0 (audio output only) +* Class-compliant MIDI input devices (tested with several devices) + +Yes, code quality is pretty awful, especially in areas involving clock +sync. I'm going to clean it up iteratively later. + +*/ + +#include "config.h" + +#if USE_LIBUSB + +#include "app.h" +#include "config-api.h" +#include "errors.h" +#include "hwcfg.h" +#include "io.h" +#include "meter.h" +#include "midi.h" +#include "recsrc.h" +#include "usbio_impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// + +int cbox_usbio_get_sample_rate(struct cbox_io_impl *impl) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + + return uii->sample_rate; +} + +gboolean cbox_usbio_get_status(struct cbox_io_impl *impl, GError **error) +{ + // XXXKF: needs a flag that would indicate whether device is present + // XXXKF: needs to return that flag with appropriate message + return TRUE; +} + +static void run_audio_loop(struct cbox_usb_io_impl *uii) +{ + while(!uii->stop_engine && !uii->device_removed) { + struct cbox_io *io = uii->ioi.pio; + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 1000 + }; + libusb_handle_events_timeout(uii->usbctx, &tv); + for (GSList *p = io->midi_outputs; p; p = p->next) + { + struct cbox_usb_midi_output *umo = p->data; + usbio_send_midi_to_output(umo); + } + } +} + +void usbio_run_idle_loop(struct cbox_usb_io_impl *uii) +{ + while(!uii->stop_engine) + { + struct cbox_io *io = uii->ioi.pio; + for (int b = 0; b < uii->output_channels; b++) + memset(io->output_buffers[b], 0, io->io_env.buffer_size * sizeof(float)); + io->cb->process(io->cb->user_data, io, io->io_env.buffer_size); + for (GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + cbox_midi_buffer_clear(&umi->input_port->hdr.buffer); + } + for (GSList *p = io->midi_outputs; p; p = p->next) + { + struct cbox_usb_midi_output *umo = p->data; + usbio_send_midi_to_output(umo); + } + + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 1 + }; + libusb_handle_events_timeout(uii->usbctx, &tv); + usleep((int)(io->io_env.buffer_size * 1000000.0 / uii->sample_rate)); + } +} + +static void *engine_thread(void *user_data) +{ + struct cbox_usb_io_impl *uii = user_data; + + usbio_start_midi_capture(uii); + + if (uii->handle_audiodev) + { + uii->no_resubmit = FALSE; + struct sched_param p; + memset(&p, 0, sizeof(p)); + p.sched_priority = cbox_config_get_int("io", "rtpriority", 10); + pid_t tid = syscall(SYS_gettid); + if (0 != sched_setscheduler(tid, SCHED_FIFO, &p)) + g_warning("Cannot set realtime priority for the processing thread: %s.", strerror(errno)); + + usbio_start_audio_playback(uii); + if (!uii->setup_error) + { + run_audio_loop(uii); + } + uii->no_resubmit = TRUE; + memset(&p, 0, sizeof(p)); + p.sched_priority = 0; + if (0 != sched_setscheduler(tid, SCHED_OTHER, &p)) + g_warning("Cannot unset realtime priority for the processing thread: %s.", strerror(errno)); + usbio_stop_audio_playback(uii); + } + else + { + uii->no_resubmit = TRUE; + g_message("No audio device found - running idle loop."); + // notify the UI thread that the (fake) audio loop is running + uii->playback_counter = uii->playback_buffers; + usbio_run_idle_loop(uii); + } + + usbio_stop_midi_capture(uii); + return NULL; +} + +static void cbox_usbio_destroy_midi_out(struct cbox_io_impl *ioi, struct cbox_midi_output *midiout) +{ + g_free(midiout->name); + free(midiout); +} + +static struct cbox_usb_midi_interface *cur_midi_interface = NULL; + +struct cbox_midi_input *cbox_usbio_create_midi_in(struct cbox_io_impl *impl, const char *name, GError **error) +{ + // struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + struct cbox_usb_midi_input *input = calloc(1, sizeof(struct cbox_usb_midi_input)); + input->hdr.name = g_strdup(name); + input->hdr.removing = FALSE; + cbox_uuid_generate(&input->hdr.uuid); + cbox_midi_buffer_init(&input->hdr.buffer); + input->ifptr = cur_midi_interface; + cbox_midi_appsink_init(&input->hdr.appsink, NULL, NULL); + input->hdr.enable_appsink = FALSE; + + return (struct cbox_midi_input *)input; +} + +struct cbox_midi_output *cbox_usbio_create_midi_out(struct cbox_io_impl *impl, const char *name, GError **error) +{ + // struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + struct cbox_usb_midi_output *output = calloc(1, sizeof(struct cbox_usb_midi_output)); + output->hdr.name = g_strdup(name); + output->hdr.removing = FALSE; + cbox_uuid_generate(&output->hdr.uuid); + cbox_midi_buffer_init(&output->hdr.buffer); + cbox_midi_merger_init(&output->hdr.merger, &output->hdr.buffer); + output->ifptr = cur_midi_interface; + + return (struct cbox_midi_output *)output; +} + +static void create_midi_ports(struct cbox_usb_io_impl *uii) +{ + uii->ioi.createmidiinfunc = cbox_usbio_create_midi_in; + uii->ioi.createmidioutfunc = cbox_usbio_create_midi_out; + for (GList *p = uii->midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + char buf[80]; + sprintf(buf, "usb:%03d:%03d", umi->devinfo->bus, umi->devinfo->devadr); + cur_midi_interface = umi; + if (umi->epdesc_in.found) + umi->input_port = (struct cbox_usb_midi_input *)cbox_io_create_midi_input(uii->ioi.pio, buf, NULL); + else + umi->input_port = NULL; + if (umi->epdesc_out.found) + umi->output_port = (struct cbox_usb_midi_output *)cbox_io_create_midi_output(uii->ioi.pio, buf, NULL); + else + umi->output_port = NULL; + } + uii->ioi.createmidiinfunc = NULL; + uii->ioi.createmidioutfunc = NULL; + cur_midi_interface = NULL; +} + +gboolean cbox_usbio_start(struct cbox_io_impl *impl, struct cbox_command_target *fb, GError **error) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + + // XXXKF: needs to queue the playback and capture transfers + + uii->stop_engine = FALSE; + uii->setup_error = FALSE; + uii->playback_counter = 0; + + create_midi_ports(uii); + + struct cbox_io *io = uii->ioi.pio; + // XXXKF There is a short period of time when the playback is in 'started' state + // but is not really processing the event loop. This is likely harmless, but + // should be kept in mind when adding any code between pthread_create + // and usbio_update_port_routing. + if (io->cb->on_started) + io->cb->on_started(io->cb->user_data); + + if (pthread_create(&uii->thr_engine, NULL, engine_thread, uii)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "cannot create engine thread: %s", strerror(errno)); + return FALSE; + } + while(!uii->setup_error && uii->playback_counter < uii->playback_buffers) + usleep(10000); + usbio_update_port_routing(&uii->ioi); + + return TRUE; +} + +gboolean cbox_usbio_stop(struct cbox_io_impl *impl, GError **error) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + + // XXXKF: needs to kill the playback and capture transfers, and + // wait for them to be killed + + uii->stop_engine = TRUE; + pthread_join(uii->thr_engine, NULL); + return TRUE; +} + +void cbox_usbio_poll_ports(struct cbox_io_impl *impl, struct cbox_command_target *fb) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + + // Dry run, just to detect if anything changed + if (usbio_scan_devices(uii, TRUE)) + { + struct cbox_io_callbacks *cb = uii->ioi.pio->cb; + g_debug("Restarting I/O due to device being connected or disconnected"); + cbox_io_stop(uii->ioi.pio); + // Re-scan, this time actually create the MIDI inputs + usbio_scan_devices(uii, FALSE); + cbox_io_start(uii->ioi.pio, cb, fb); + } +} + +gboolean cbox_usbio_cycle(struct cbox_io_impl *impl, struct cbox_command_target *fb, GError **error) +{ + // struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + // XXXKF: this is for restarting the thing; not implemented for now, + // the implementation will be something like in case of JACK - close and + // reopen. + return TRUE; +} + +int cbox_usbio_get_midi_data(struct cbox_io_impl *impl, struct cbox_midi_buffer *destination) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + + cbox_midi_merger_render_to(&uii->midi_input_merger, destination); + return 0; +} + +void cbox_usbio_destroy(struct cbox_io_impl *impl) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)impl; + + GList *prev_keys = g_hash_table_get_values(uii->device_table); + for (GList *p = prev_keys; p; p = p->next) + { + struct cbox_usb_device_info *udi = p->data; + if (udi->status == CBOX_DEVICE_STATUS_OPENED) + usbio_forget_device(uii, udi); + } + g_list_free(prev_keys); + g_hash_table_destroy(uii->device_table); + + libusb_exit(uii->usbctx_probe); + libusb_exit(uii->usbctx); + cbox_midi_merger_close(&uii->midi_input_merger, app.rt); + free(uii); +} + +/////////////////////////////////////////////////////////////////////////////// + +struct usbio_transfer *usbio_transfer_new(struct libusb_context *usbctx, const char *transfer_type, int index, int isopackets, void *user_data) +{ + struct usbio_transfer *p = malloc(sizeof(struct usbio_transfer)); + p->usbctx = usbctx; + p->transfer = libusb_alloc_transfer(isopackets); + p->index = index; + p->cancel_confirm = FALSE; + p->pending = FALSE; + p->transfer_type = transfer_type; + p->user_data = user_data; + return p; +} + +int usbio_transfer_submit(struct usbio_transfer *xfer) +{ + int res = libusb_submit_transfer(xfer->transfer); + if (res != 0) + { + g_warning("usbio_transfer_submit: cannot submit transfer '%s:%d', error = %s", xfer->transfer_type, xfer->index, libusb_error_name(res)); + return res; + } + xfer->pending = TRUE; + return 0; +} + +void usbio_transfer_shutdown(struct usbio_transfer *xfer) +{ + if (xfer->pending) + { + int res = libusb_cancel_transfer(xfer->transfer); + if (res != LIBUSB_ERROR_NO_DEVICE) + { + int tries = 100; + while(!xfer->cancel_confirm && tries > 0 && xfer->pending) + { + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 1000 + }; + libusb_handle_events_timeout(xfer->usbctx, &tv); + tries--; + } + if (!tries) + g_warning("Timed out waiting for transfer '%s:%d' to complete; status = %d", xfer->transfer_type, xfer->index, xfer->transfer->status); + } + } +} + +void usbio_transfer_destroy(struct usbio_transfer *xfer) +{ + libusb_free_transfer(xfer->transfer); + free(xfer); +} + + +/////////////////////////////////////////////////////////////////////////////// + +static gboolean cbox_usb_io_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)ct->user_data; + struct cbox_io *io = uii->ioi.pio; + gboolean handled = FALSE; + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + for (GList *p = uii->midi_ports; p; p = g_list_next(p)) + { + struct cbox_usb_midi_interface *midi = p->data; + struct cbox_usb_device_info *di = midi->devinfo; + if (midi->epdesc_in.found && !cbox_execute_on(fb, NULL, "/usb_midi_input", "iiiiu", error, di->bus, di->devadr, di->vid, di->pid, &midi->input_port->hdr.uuid)) + return FALSE; + if (midi->epdesc_out.found && !cbox_execute_on(fb, NULL, "/usb_midi_output", "iiiiu", error, di->bus, di->devadr, di->vid, di->pid, &midi->output_port->hdr.uuid)) + return FALSE; + } + + return cbox_execute_on(fb, NULL, "/client_type", "s", error, "USB") && + cbox_execute_on(fb, NULL, "/output_resolution", "i", error, 8 * uii->output_resolution) && + cbox_io_process_cmd(io, fb, cmd, error, &handled); + } + else + { + gboolean result = cbox_io_process_cmd(io, fb, cmd, error, &handled); + if (!handled) + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return result; + } +} + +/////////////////////////////////////////////////////////////////////////////// + +gboolean cbox_io_init_usb(struct cbox_io *io, struct cbox_open_params *const params, struct cbox_command_target *fb, GError **error) +{ + struct cbox_usb_io_impl *uii = malloc(sizeof(struct cbox_usb_io_impl)); + if (libusb_init(&uii->usbctx)) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot initialise libusb."); + return FALSE; + } + if (libusb_init(&uii->usbctx_probe)) + { + libusb_exit(uii->usbctx); + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot initialise libusb."); + return FALSE; + } +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(uii->usbctx, LIBUSB_OPTION_LOG_LEVEL, 3); + libusb_set_option(uii->usbctx_probe, LIBUSB_OPTION_LOG_LEVEL, 3); +#else + libusb_set_debug(uii->usbctx, 3); + libusb_set_debug(uii->usbctx_probe, 3); +#endif + uii->device_table = g_hash_table_new(g_direct_hash, NULL); + + uii->sample_rate = cbox_config_get_int(cbox_io_section, "sample_rate", 44100); + uii->sync_buffers = cbox_config_get_int(cbox_io_section, "sync_buffers", 2); + uii->debug_sync = cbox_config_get_int(cbox_io_section, "debug_sync", 0); + uii->playback_buffers = cbox_config_get_int(cbox_io_section, "playback_buffers", 2); + // shouldn't be more than 4, otherwise it will crackle due to limitations of + // the packet length adjustment. It might work better if adjustment + // was per-packet and not per-transfer. + uii->iso_packets = cbox_config_get_int(cbox_io_section, "iso_packets", 1); + // The USB 2.0 device uses a higher packet rate (125us I think), so the + // default number of packets per transfer needs to be different, too - + // 1ms is a minimum reasonable value + uii->iso_packets_multimix = cbox_config_get_int(cbox_io_section, "iso_packets_multimix", 16); + uii->output_resolution = cbox_config_get_int(cbox_io_section, "output_resolution", 16) / 8; + uii->output_channels = 2; + uii->handle_audiodev = NULL; + cbox_midi_merger_init(&uii->midi_input_merger, NULL); + + // fixed processing buffer size, as we have to deal with packetisation anyway + io->io_env.srate = uii->sample_rate; + io->io_env.buffer_size = 64; + io->cb = NULL; + // input and output count is hardcoded for simplicity - in future, it may be + // necessary to add support for the extra inputs (needs to be figured out) + io->io_env.input_count = 2; //cbox_config_get_int("io", "inputs", 0); + io->input_buffers = malloc(sizeof(float *) * io->io_env.input_count); + for (uint32_t i = 0; i < io->io_env.input_count; i++) + io->input_buffers[i] = calloc(io->io_env.buffer_size, sizeof(float)); + io->io_env.output_count = 2; // cbox_config_get_int("io", "outputs", 2); + io->output_buffers = malloc(sizeof(float *) * io->io_env.output_count); + for (uint32_t i = 0; i < io->io_env.output_count; i++) + io->output_buffers[i] = calloc(io->io_env.buffer_size, sizeof(float)); + io->impl = &uii->ioi; + cbox_command_target_init(&io->cmd_target, cbox_usb_io_process_cmd, uii); + + uii->ioi.pio = io; + uii->ioi.getsampleratefunc = cbox_usbio_get_sample_rate; + uii->ioi.startfunc = cbox_usbio_start; + uii->ioi.stopfunc = cbox_usbio_stop; + uii->ioi.getstatusfunc = cbox_usbio_get_status; + uii->ioi.pollfunc = cbox_usbio_poll_ports; + uii->ioi.cyclefunc = cbox_usbio_cycle; + uii->ioi.getmidifunc = cbox_usbio_get_midi_data; + uii->ioi.destroymidioutfunc = cbox_usbio_destroy_midi_out; + uii->ioi.destroyfunc = cbox_usbio_destroy; + uii->ioi.controltransportfunc = NULL; + uii->ioi.getsynccompletedfunc = NULL; + uii->ioi.updatemidiinroutingfunc = usbio_update_port_routing; + uii->midi_ports = NULL; + + usbio_scan_devices(uii, FALSE); + + if (cbox_config_get_int("io", "lockall", 0)) + mlockall(MCL_CURRENT|MCL_FUTURE); + + return TRUE; + +} + +#endif diff --git a/template/calfbox/usbio_impl.h b/template/calfbox/usbio_impl.h new file mode 100644 index 0000000..dd8bc41 --- /dev/null +++ b/template/calfbox/usbio_impl.h @@ -0,0 +1,255 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +/* + +Note: +Those are private structures and functions for the USB driver implementation. +I'm not too happy with the organization of the code yet, it's just a first +try at breaking it up into manageable parts. +*/ + + +#ifndef USBIO_IMPL_H +#define USBIO_IMPL_H + +#include + +#include "errors.h" +#include "io.h" +#include "midi.h" +#include "mididest.h" + +struct usbio_endpoint_descriptor +{ + uint8_t found:1; + uint8_t interrupt:1; + uint8_t reserved:6; + uint8_t bEndpointAddress; + uint16_t wMaxPacketSize; +}; + +struct usbio_transfer +{ + struct libusb_context *usbctx; + struct libusb_transfer *transfer; + void *user_data; + gboolean pending; + gboolean cancel_confirm; + const char *transfer_type; + int index; +}; + +enum usb_sync_protocol_type +{ + USBAUDIOSYNC_PROTOCOL_NONE = 0, + USBAUDIOSYNC_PROTOCOL_CLASS, + USBAUDIOSYNC_PROTOCOL_MULTIMIX8, +}; + +struct cbox_usb_io_impl +{ + struct cbox_io_impl ioi; + + struct libusb_context *usbctx; + struct libusb_context *usbctx_probe; + + GHashTable *device_table; + + struct libusb_device_handle *handle_audiodev; + int sample_rate, buffer_size, output_resolution; + int output_channels; // always 2 for now + + unsigned int playback_buffers; + unsigned int sync_buffers; + unsigned int playback_counter; + unsigned int sync_counter; + unsigned int device_removed; + enum usb_sync_protocol_type sync_protocol; + + unsigned int iso_packets, iso_packets_multimix; + + pthread_t thr_engine; + volatile gboolean stop_engine, setup_error, no_resubmit; + + int desync; + uint64_t samples_played; + struct usbio_transfer **playback_transfers; + struct usbio_transfer **sync_transfers; + int read_ptr; + + GList *midi_ports; + GList *rt_midi_ports; + struct cbox_midi_merger midi_input_merger; + void (*play_function)(struct cbox_usb_io_impl *uii); + int8_t audio_output_endpoint; + int8_t audio_sync_endpoint; + gboolean is_hispeed; + uint32_t samples_per_sec; // current estimated sample rate / 100 + uint32_t audio_output_pktsize; + int debug_sync; +}; + +enum cbox_usb_device_status +{ + CBOX_DEVICE_STATUS_PROBING, + CBOX_DEVICE_STATUS_UNSUPPORTED, + CBOX_DEVICE_STATUS_OPENED, +}; + +struct cbox_usb_device_info +{ + struct libusb_device *dev; + struct libusb_device_handle *handle; + enum cbox_usb_device_status status; + uint8_t bus; + uint8_t devadr; + uint16_t busdevadr; + struct { + uint16_t vid; + uint16_t pid; + }; + int active_config; + gboolean is_midi, is_audio; + uint32_t configs_with_midi; + uint32_t configs_with_audio; + time_t last_probe_time; + int failures; +}; + +struct cbox_usb_audio_info +{ + struct cbox_usb_device_info *udi; + int intf; + int alt_setting; + struct usbio_endpoint_descriptor epdesc; + enum usb_sync_protocol_type sync_protocol; + struct usbio_endpoint_descriptor sync_epdesc; +}; + +enum usb_midi_protocol_type +{ + USBMIDI_PROTOCOL_CLASS, + USBMIDI_PROTOCOL_MPD16, + USBMIDI_PROTOCOL_NOCTURN, +}; + +struct cbox_usb_midi_info +{ + struct cbox_usb_device_info *udi; + int intf; + int alt_setting; + struct usbio_endpoint_descriptor epdesc_in; + struct usbio_endpoint_descriptor epdesc_out; + enum usb_midi_protocol_type protocol; +}; + +#define MAX_MIDI_PACKET_SIZE 256 +#define MAX_SYSEX_SIZE CBOX_MIDI_MAX_LONG_DATA + +struct cbox_usb_midi_interface +{ + struct cbox_usb_io_impl *uii; + struct cbox_usb_device_info *devinfo; + struct libusb_device_handle *handle; + int busdevadr; + struct usbio_endpoint_descriptor epdesc_in, epdesc_out; + struct usbio_transfer *transfer_in, *transfer_out; + uint8_t midi_recv_data[MAX_MIDI_PACKET_SIZE]; + uint8_t sysex_data[MAX_SYSEX_SIZE]; + uint32_t current_sysex_length; + enum usb_midi_protocol_type protocol; + struct cbox_usb_midi_input *input_port; + struct cbox_usb_midi_output *output_port; +}; + +#define USB_MIDI_OUTPUT_QUEUE 256 + +struct cbox_usb_midi_input +{ + struct cbox_midi_input hdr; + struct cbox_usb_midi_interface *ifptr; +}; + +struct cbox_usb_midi_output +{ + struct cbox_midi_output hdr; + struct cbox_usb_midi_interface *ifptr; + uint8_t endpoint_buffer[USB_MIDI_OUTPUT_QUEUE]; + uint32_t endpoint_buffer_pos; +}; + +extern struct usbio_transfer *usbio_transfer_new(struct libusb_context *usbctx, const char *transfer_type, int index, int isopackets, void *user_data); +extern void usbio_transfer_shutdown(struct usbio_transfer *xfer); +extern int usbio_transfer_submit(struct usbio_transfer *xfer); +extern void usbio_transfer_destroy(struct usbio_transfer *xfer); + +extern void cbox_usb_midi_info_init(struct cbox_usb_midi_info *umi, struct cbox_usb_device_info *udi); +extern void usbio_start_midi_capture(struct cbox_usb_io_impl *uii); +extern void usbio_stop_midi_capture(struct cbox_usb_io_impl *uii); +extern struct cbox_usb_midi_interface *usbio_open_midi_interface(struct cbox_usb_io_impl *uii, + const struct cbox_usb_midi_info *uminf, struct libusb_device_handle *handle); +extern void usbio_send_midi_to_output(struct cbox_usb_midi_output *umo); +extern void usbio_fill_midi_output_buffer(struct cbox_usb_midi_output *umo); + +extern void cbox_usb_audio_info_init(struct cbox_usb_audio_info *uai, struct cbox_usb_device_info *udi); +extern void usbio_start_audio_playback(struct cbox_usb_io_impl *uii); +extern void usbio_stop_audio_playback(struct cbox_usb_io_impl *uii); +extern gboolean usbio_open_audio_interface(struct cbox_usb_io_impl *uii, + struct cbox_usb_audio_info *uainf, struct libusb_device_handle *handle, GError **error); +extern gboolean usbio_open_audio_interface_multimix(struct cbox_usb_io_impl *uii, int bus, int devadr, struct libusb_device_handle *handle, GError **error); +extern void usbio_update_port_routing(struct cbox_io_impl *ioi); + + +extern void usbio_play_buffer_asynchronous(struct cbox_usb_io_impl *uii); +extern gboolean usbio_scan_devices(struct cbox_usb_io_impl *uii, gboolean probe_only); + +extern void usbio_forget_device(struct cbox_usb_io_impl *uii, struct cbox_usb_device_info *devinfo); +extern void usbio_run_idle_loop(struct cbox_usb_io_impl *uii); + +#define USB_DEVICE_SETUP_TIMEOUT 2000 + +static inline gboolean configure_usb_interface(struct libusb_device_handle *handle, int bus, int devadr, int ifno, int altset, const char *purpose, GError **error) +{ + int err = 0; + if (libusb_kernel_driver_active(handle, ifno) == 1) + { + err = libusb_detach_kernel_driver(handle, ifno); + if (err) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot detach kernel driver from interface %d on device %03d:%03d: %s. Please rmmod snd-usb-audio as root.", ifno, bus, devadr, libusb_error_name(err)); + return FALSE; + } + } + err = libusb_claim_interface(handle, ifno); + if (err) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot claim interface %d on device %03d:%03d for %s: %s", ifno, bus, devadr, purpose, libusb_error_name(err)); + return FALSE; + } + err = altset ? libusb_set_interface_alt_setting(handle, ifno, altset) : 0; + if (err) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot set alt-setting %d for interface %d on device %03d:%03d for %s: %s", altset, ifno, bus, devadr, purpose, libusb_error_name(err)); + return FALSE; + } + return TRUE; +} + + +#endif diff --git a/template/calfbox/usbmidi.c b/template/calfbox/usbmidi.c new file mode 100644 index 0000000..a546f97 --- /dev/null +++ b/template/calfbox/usbmidi.c @@ -0,0 +1,397 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config.h" +#if USE_LIBUSB + +#include "app.h" +#include "config-api.h" +#include "errors.h" +#include "hwcfg.h" +#include "io.h" +#include "meter.h" +#include "midi.h" +#include "recsrc.h" +#include "usbio_impl.h" + +#include + +static void decode_events_mpd16(struct cbox_usb_midi_interface *umi, struct libusb_transfer *transfer) +{ + for (int i = 0; i < transfer->actual_length;) + { + uint8_t *data = &transfer->buffer[i]; + uint8_t len = data[0] & 15; + if (!len || i + len >= transfer->actual_length) + break; + cbox_midi_buffer_write_inline(&umi->input_port->hdr.buffer, 0, data[1], len > 1 ? data[2] : 0, len > 2 ? data[3] : 0); + i += len + 1; + } +} + +static void decode_events_nocturn(struct cbox_usb_midi_interface *umi, struct libusb_transfer *transfer) +{ + uint8_t control = 0; + for (int i = 0; i < transfer->actual_length;) + { + uint8_t *data = &transfer->buffer[i]; + if (*data >= 128) + { + control = *data; + i++; + continue; + } + if (control != 176 || i + 2 > transfer->actual_length) + { + g_warning("Unrecognized combination of control, data and length: %02x %02x %02x", control, data[0], transfer->actual_length - i); + break; + } + cbox_midi_buffer_write_inline(&umi->input_port->hdr.buffer, 0, control, data[0], data[1]); + i += 2; + } +} + +static void decode_events_class(struct cbox_usb_midi_interface *umi, struct libusb_transfer *transfer) +{ + for (int i = 0; i + 3 < transfer->actual_length; i += 4) + { + uint8_t *data = &transfer->buffer[i]; + uint8_t etype = data[0] & 15; + if (etype >= 8) + { + // normalise: note on with vel 0 -> note off + if ((data[1] & 0xF0) == 0x90 && data[3] == 0) + cbox_midi_buffer_write_inline(&umi->input_port->hdr.buffer, 0, data[1] - 0x10, data[2], data[3]); + else + cbox_midi_buffer_write_event(&umi->input_port->hdr.buffer, 0, data + 1, midi_cmd_size(data[1])); + } + else + if (etype == 2 || etype == 3) + { + cbox_midi_buffer_write_event(&umi->input_port->hdr.buffer, 0, data + 1, etype); + } + else + if (etype == 4) + { + if (umi->current_sysex_length + 3 <= MAX_SYSEX_SIZE) + memcpy(&umi->sysex_data[umi->current_sysex_length], data + 1, 3); + umi->current_sysex_length += 3; + } + else + if (etype >= 5 && etype <= 7) + { + int len = etype - 4; + if (umi->current_sysex_length + len <= MAX_SYSEX_SIZE) + memcpy(&umi->sysex_data[umi->current_sysex_length], data + 1, len); + umi->current_sysex_length += len; + if (umi->current_sysex_length <= MAX_SYSEX_SIZE) + cbox_midi_buffer_write_event(&umi->input_port->hdr.buffer, 0, umi->sysex_data, umi->current_sysex_length); + else + g_warning("Dropping oversized SysEx: length %d", umi->current_sysex_length); + umi->current_sysex_length = 0; + + } + else + g_warning("Unrecognized USB MIDI initiating byte %02x\n", data[0]); + } +} + +static void midi_transfer_cb(struct libusb_transfer *transfer) +{ + struct usbio_transfer *xf = transfer->user_data; + struct cbox_usb_midi_interface *umi = xf->user_data; + xf->pending = FALSE; + + if (transfer->status == LIBUSB_TRANSFER_CANCELLED) + { + xf->cancel_confirm = 1; + return; + } + if (transfer->status == LIBUSB_TRANSFER_TIMED_OUT || transfer->status == LIBUSB_TRANSFER_ERROR || transfer->status == LIBUSB_TRANSFER_STALL) + { + if (transfer->status != LIBUSB_TRANSFER_TIMED_OUT) + g_warning("USB error on device %03d:%03d: transfer status %d", umi->busdevadr >> 8, umi->busdevadr & 255, transfer->status); + if (umi->uii->no_resubmit) + return; + int res = usbio_transfer_submit(xf); + if (res != 0) + g_warning("Error submitting URB to MIDI endpoint: error code %d", res); + return; + } + if (transfer->status == LIBUSB_TRANSFER_NO_DEVICE) + { + g_debug("No device %03d:%03d, unlinking", umi->busdevadr >> 8, umi->busdevadr & 255); + umi->uii->rt_midi_ports = g_list_remove(umi->uii->rt_midi_ports, umi); + return; + } + + if (umi->protocol == USBMIDI_PROTOCOL_MPD16) // MPD16 + decode_events_mpd16(umi, transfer); + else + if (umi->protocol == USBMIDI_PROTOCOL_NOCTURN) // Nocturn + decode_events_nocturn(umi, transfer); + else + decode_events_class(umi, transfer); + + if (umi->uii->no_resubmit) + return; + usbio_transfer_submit(xf); +} + +static gboolean push_data_to_umo(struct cbox_usb_midi_output *umo, const uint8_t *pdata, uint32_t size, uint8_t first_byte) +{ + if (umo->endpoint_buffer_pos + 4 <= USB_MIDI_OUTPUT_QUEUE) + { + umo->endpoint_buffer[umo->endpoint_buffer_pos] = first_byte; + memcpy(&umo->endpoint_buffer[umo->endpoint_buffer_pos + 1], pdata, size); + umo->endpoint_buffer_pos += 4; + } + else + { + g_warning("Class MIDI buffer overflow."); + return FALSE; + } + return TRUE; +} + +static void encode_events_class(struct cbox_usb_midi_output *umo) +{ + for (uint32_t i = 0; i < umo->hdr.buffer.count; i++) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(&umo->hdr.buffer, i); + const uint8_t *pdata = cbox_midi_event_get_data(event); + if (event->size <= 3) + { + if (!push_data_to_umo(umo, pdata, event->size, pdata[0] >> 4)) + return; + } + else + { + int i = 0; + while(i + 3U < event->size) + { + push_data_to_umo(umo, pdata + i, 3, 4); + i += 3; + } + push_data_to_umo(umo, pdata + i, 3, 4 + event->size - i); + } + } +} + +static void encode_events_nocturn(struct cbox_usb_midi_output *umo) +{ + for (uint32_t i = 0; i < umo->hdr.buffer.count; i++) + { + const struct cbox_midi_event *event = cbox_midi_buffer_get_event(&umo->hdr.buffer, i); + const uint8_t *pdata = cbox_midi_event_get_data(event); + // Only encode control change events on channel 1 - it's the only + // recognized type of events + if (event->size == 3 && pdata[0] == 0xB0) + { + if (umo->endpoint_buffer_pos + 2 <= 8) + { + umo->endpoint_buffer[umo->endpoint_buffer_pos] = pdata[1]; + umo->endpoint_buffer[umo->endpoint_buffer_pos + 1] = pdata[2]; + umo->endpoint_buffer_pos += 2; + } + else + g_warning("Nocturn MIDI buffer overflow."); + } + } +} + +void usbio_fill_midi_output_buffer(struct cbox_usb_midi_output *umo) +{ + cbox_midi_merger_render(&umo->hdr.merger); + if (!umo->hdr.buffer.count) + return; + + if (umo->ifptr->protocol == USBMIDI_PROTOCOL_CLASS) + encode_events_class(umo); + else + if (umo->ifptr->protocol == USBMIDI_PROTOCOL_NOCTURN) + encode_events_nocturn(umo); +} + +void usbio_send_midi_to_output(struct cbox_usb_midi_output *umo) +{ + if (!umo->endpoint_buffer_pos) + return; + + int transferred = 0; + int res = 0; + if (umo->ifptr->epdesc_out.interrupt) + res = libusb_interrupt_transfer(umo->ifptr->handle, umo->ifptr->epdesc_out.bEndpointAddress, umo->endpoint_buffer, umo->endpoint_buffer_pos, &transferred, 10); + else + res = libusb_bulk_transfer(umo->ifptr->handle, umo->ifptr->epdesc_out.bEndpointAddress, umo->endpoint_buffer, umo->endpoint_buffer_pos, &transferred, 10); + if (res == 0 && (uint32_t)transferred == umo->endpoint_buffer_pos) + umo->endpoint_buffer_pos = 0; + else + g_warning("Failed to send MIDI events, transferred = %d out of %d, result = %d", (int)transferred, (int)umo->endpoint_buffer_pos, res); +} + +void usbio_update_port_routing(struct cbox_io_impl *ioi) +{ + struct cbox_usb_io_impl *uii = (struct cbox_usb_io_impl *)ioi; + for(GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + if (umi->input_port) + { + if (!umi->input_port->hdr.output_set) + cbox_midi_merger_connect(&uii->midi_input_merger, &umi->input_port->hdr.buffer, app.rt, NULL); + else + cbox_midi_merger_disconnect(&uii->midi_input_merger, &umi->input_port->hdr.buffer, app.rt); + } + } +} + +void usbio_start_midi_capture(struct cbox_usb_io_impl *uii) +{ + uii->rt_midi_ports = g_list_copy(uii->midi_ports); + + for(GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + cbox_midi_buffer_clear(&umi->input_port->hdr.buffer); + if (umi->epdesc_in.found) + { + umi->current_sysex_length = 0; + umi->transfer_in = usbio_transfer_new(uii->usbctx, "MIDI In", 0, 0, umi); + int pktsize = umi->epdesc_in.wMaxPacketSize; + if (pktsize > MAX_MIDI_PACKET_SIZE) + pktsize = MAX_MIDI_PACKET_SIZE; + if (umi->epdesc_in.interrupt) + libusb_fill_interrupt_transfer(umi->transfer_in->transfer, umi->handle, umi->epdesc_in.bEndpointAddress, umi->midi_recv_data, pktsize, midi_transfer_cb, umi->transfer_in, 0); + else + libusb_fill_bulk_transfer(umi->transfer_in->transfer, umi->handle, umi->epdesc_in.bEndpointAddress, umi->midi_recv_data, pktsize, midi_transfer_cb, umi->transfer_in, 0); + } + else + umi->transfer_in = NULL; + if (umi->epdesc_out.found) + umi->transfer_out = usbio_transfer_new(uii->usbctx, "MIDI Out", 0, 0, umi); + else + umi->transfer_out = NULL; + } + for(GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + int res = usbio_transfer_submit(umi->transfer_in); + if (res != 0) + { + usbio_transfer_destroy(umi->transfer_in); + umi->transfer_in = NULL; + } + } +} + +void usbio_stop_midi_capture(struct cbox_usb_io_impl *uii) +{ + for(GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + + if (umi->transfer_in) + { + usbio_transfer_shutdown(umi->transfer_in); + usbio_transfer_destroy(umi->transfer_in); + umi->transfer_in = NULL; + cbox_midi_buffer_clear(&umi->input_port->hdr.buffer); + } + if (umi->transfer_out) + { + usbio_transfer_shutdown(umi->transfer_out); + usbio_transfer_destroy(umi->transfer_out); + umi->transfer_out = NULL; + } + } + for(GList *p = uii->rt_midi_ports; p; p = p->next) + { + struct cbox_usb_midi_interface *umi = p->data; + if (umi->epdesc_in.found) + cbox_midi_merger_disconnect(&uii->midi_input_merger, &umi->input_port->hdr.buffer, NULL); + } + g_list_free(uii->rt_midi_ports); +} + +void cbox_usb_midi_info_init(struct cbox_usb_midi_info *umi, struct cbox_usb_device_info *udi) +{ + umi->udi = udi; + umi->intf = -1; + umi->alt_setting = -1; + umi->epdesc_in.found = FALSE; + umi->epdesc_out.found = FALSE; +} + +struct cbox_usb_midi_interface *usbio_open_midi_interface(struct cbox_usb_io_impl *uii, const struct cbox_usb_midi_info *uminf, struct libusb_device_handle *handle) +{ + struct cbox_usb_device_info *devinfo = uminf->udi; + int bus = devinfo->bus; + int devadr = devinfo->devadr; + GError *error = NULL; + // printf("Has MIDI port\n"); + // printf("Output endpoint address = %02x\n", ep->bEndpointAddress); + if (!configure_usb_interface(handle, bus, devadr, uminf->intf, uminf->alt_setting, "MIDI (class driver)", &error)) + { + g_warning("%s", error->message); + g_error_free(error); + return NULL; + } + + struct cbox_usb_midi_interface *umi = calloc(sizeof(struct cbox_usb_midi_interface), 1); + umi->uii = uii; + umi->devinfo = devinfo; + umi->handle = handle; + umi->busdevadr = devinfo->busdevadr; + uii->midi_ports = g_list_prepend(uii->midi_ports, umi); + umi->protocol = uminf->protocol; + memcpy(&umi->epdesc_in, &uminf->epdesc_in, sizeof(umi->epdesc_in)); + memcpy(&umi->epdesc_out, &uminf->epdesc_out, sizeof(umi->epdesc_out)); + + // Drain the output buffer of the device - otherwise playing a few notes and running the program will cause + // those notes to play. + unsigned char flushbuf[256]; + int transferred = 0; + int len = umi->epdesc_in.wMaxPacketSize; + if (len > MAX_MIDI_PACKET_SIZE) + len = MAX_MIDI_PACKET_SIZE; + if (umi->epdesc_in.interrupt) + { + while(0 == libusb_interrupt_transfer(handle, umi->epdesc_in.bEndpointAddress, flushbuf, len, &transferred, 10) && transferred > 0) + usleep(1000); + } + else + { + while(0 == libusb_bulk_transfer(handle, umi->epdesc_in.bEndpointAddress, flushbuf, len, &transferred, 10) && transferred > 0) + usleep(1000); + } + devinfo->status = CBOX_DEVICE_STATUS_OPENED; + + // Initialise device - only used for Novation Nocturn, and it performs a + // device reset. Perhaps not the best implementation of hot swap - it + // will reset the device even when some other device was connected. + if (umi->protocol == USBMIDI_PROTOCOL_NOCTURN) + { + static uint8_t data1[] = { 0xB0, 0, 0 }; + libusb_interrupt_transfer(handle, umi->epdesc_out.bEndpointAddress, data1, sizeof(data1), &transferred, 10); + } + + return umi; +} + +#endif diff --git a/template/calfbox/usbprobe.c b/template/calfbox/usbprobe.c new file mode 100644 index 0000000..8ee5e7a --- /dev/null +++ b/template/calfbox/usbprobe.c @@ -0,0 +1,751 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2013 Krzysztof Foltman + +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 . +*/ + +#include "config.h" + +#if USE_LIBUSB +#include "usbio_impl.h" +#include + +//////////////////////////////////////////////////////////////////////////////// + +static const struct libusb_endpoint_descriptor *get_midi_endpoint(const struct libusb_interface_descriptor *asdescr, gboolean is_output) +{ + for (int epi = 0; epi < asdescr->bNumEndpoints; epi++) + { + const struct libusb_endpoint_descriptor *ep = &asdescr->endpoint[epi]; + if ((ep->bEndpointAddress >= 0x80) == !is_output) + return ep; + } + return NULL; +} + +static const struct libusb_endpoint_descriptor *get_audio_output_endpoint(const struct libusb_interface_descriptor *asdescr, const struct libusb_endpoint_descriptor **sync_endpoint) +{ + for (int epi = 0; epi < asdescr->bNumEndpoints; epi++) + { + const struct libusb_endpoint_descriptor *ep = &asdescr->endpoint[epi]; + uint8_t ep_type = (ep->bmAttributes & 0xF); + if (ep->bEndpointAddress < 0x80 && (ep_type == 9 || ep_type == 13)) // output, isochronous, adaptive or synchronous + { + if (sync_endpoint) + *sync_endpoint = NULL; + return ep; + } + if (ep->bEndpointAddress < 0x80 && ep_type == 5) // output, isochronous, asynchronous + { + // Look for corresponding synch endpoint. It must be placed after the original output endpoint. + if (sync_endpoint && ep->bSynchAddress >= 0x81) + { + *sync_endpoint = NULL; + for(int epi2 = epi + 1; epi2 < asdescr->bNumEndpoints; epi2++) + { + const struct libusb_endpoint_descriptor *ep2 = &asdescr->endpoint[epi2]; + if (ep2->bEndpointAddress == ep->bSynchAddress && (ep2->bmAttributes & 0xF) == 1) // input, isochronous, no sync + { + *sync_endpoint = ep2; + break; + } + } + } + return ep; + } + } + return NULL; +} + +static void fill_endpoint_desc(struct usbio_endpoint_descriptor *epdesc, const struct libusb_endpoint_descriptor *ep) +{ + epdesc->found = TRUE; + epdesc->interrupt = FALSE; + epdesc->bEndpointAddress = ep->bEndpointAddress; + epdesc->wMaxPacketSize = ep->wMaxPacketSize; +} + +static gboolean fill_endpoint_by_address(const struct libusb_interface_descriptor *asdescr, uint8_t addr, struct usbio_endpoint_descriptor *epdesc) +{ + epdesc->found = FALSE; + epdesc->interrupt = FALSE; + for (int epi = 0; epi < asdescr->bNumEndpoints; epi++) + { + const struct libusb_endpoint_descriptor *ep = &asdescr->endpoint[epi]; + if (ep->bEndpointAddress == addr) + { + fill_endpoint_desc(epdesc, ep); + return TRUE; + } + } + return FALSE; +} + +//////////////////////////////////////////////////////////////////////////////// + +struct usb_ut_descriptor_header +{ + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; +}; + +struct usb_class_specific_header +{ + struct usb_ut_descriptor_header hdr; + uint8_t bcdADC[2]; + uint8_t wTotalLengthLo, wTotalLengthHi; + uint8_t bInCollection; + uint8_t baInterfaceNr[0]; +}; + +struct usbaudio_input_terminal_descriptor +{ + uint8_t bTerminalID; + uint8_t wTerminalTypeLo, wTerminalTypeHi; + uint8_t bAssocTerminal; + uint8_t bNrChannels; + uint8_t wChannelConfigLo, wChannelConfigHi; + uint8_t iChannelNames; + uint8_t iTerminal; +}; + +struct usbaudio_output_terminal_descriptor +{ + uint8_t bTerminalID; + uint8_t wTerminalTypeLo, wTerminalTypeHi; + uint8_t bAssocTerminal; + uint8_t bSourceID; + uint8_t iTerminal; +}; + +struct usbaudio_mixer_unit_descriptor1 +{ + uint8_t bUnitID; + uint8_t bNrInPins; + uint8_t baSourceID[0]; +}; + +struct usbaudio_mixer_unit_descriptor2 +{ + uint8_t bNrChannels; + uint8_t wChannelConfigLo, wChannelConfigHi; + uint8_t bmControls[0]; + // after the last index, 8-bit iMixer +}; + +struct usbaudio_selector_unit_descriptor1 +{ + uint8_t bUnitID; + uint8_t bNrInPins; + uint8_t baSourceID[0]; + // after the last index, 8-bit iSelector +}; + +struct usbaudio_feature_unit_descriptor1 +{ + uint8_t bUnitID; + uint8_t bSourceID; + uint8_t bControlSize; + uint8_t bmaControls[0]; + // after the last index, 8-bit iFeature +}; + +// Unit/Terminal Descriptor header from the spec +#include + +#if 0 +// termt10.pdf +static const char *get_terminal_type_name(int type) +{ + switch(type) + { + case 0x101: return "USB Streaming"; + case 0x1FF: return "USB vendor specific"; + case 0x201: return "Microphone"; + case 0x202: return "Desktop microphone"; + case 0x203: return "Personal microphone"; + case 0x204: return "Omni-directional microphone"; + case 0x205: return "Microphone array"; + case 0x206: return "Processing microphone array"; + case 0x301: return "Speaker"; + case 0x302: return "Headphones"; + case 0x303: return "Head-mounted display audio"; + case 0x304: return "Desktop speaker"; + case 0x305: return "Room speaker"; + case 0x306: return "Communication speaker"; + case 0x307: return "Low-frequency effects speaker"; + case 0x400: return "Bi-directional Undefined"; + case 0x401: return "Handset (handheld)"; + case 0x402: return "Handset (head-mounted)"; + case 0x403: return "Speakerphone"; + case 0x404: return "Echo-suppressing speakerphone"; + case 0x405: return "Echo-cancelling speakerphone"; + case 0x501: return "Phone line"; + case 0x502: return "Telephone"; + case 0x503: return "Down line phone"; + case 0x600: return "External Undefined"; + case 0x601: return "Analog connector"; + case 0x602: return "Digital audio interface"; + case 0x603: return "Line connector"; + case 0x604: return "Legacy audio"; + case 0x605: return "S/PDIF"; + case 0x606: return "1394 D/A Stream"; + case 0x607: return "1394 D/V Stream Soundtrack"; + case 0x700: return "Embedded undefined"; + case 0x701: return "Level calibration noise source"; + case 0x702: return "Equalization noise"; + case 0x703: return "CD player"; + case 0x704: return "DAT"; + case 0x705: return "DCC"; + case 0x706: return "MiniDisc"; + case 0x707: return "Analog tape"; + case 0x708: return "Phono"; + case 0x709: return "VCR Audio"; + case 0x70A: return "VideoDisc Audio"; + case 0x70B: return "DVD Audio"; + case 0x70C: return "TV Tuner Audio"; + case 0x70D: return "Satellite Receiver Audio"; + case 0x70E: return "Cable Tuner Audio"; + case 0x70F: return "DSS Audio"; + case 0x710: return "Radio Receiver"; + case 0x711: return "Radio Transmitter"; + case 0x712: return "Multitrack Recorder"; + case 0x713: return "Synthesizer"; + default: + return "Unknown"; + } +} +#endif + +static gboolean parse_audio_control_class(struct cbox_usb_audio_info *uai, const struct libusb_interface_descriptor *asdescr, gboolean other_config) +{ +#if 0 + if (asdescr->extra_length < 8) + { + g_warning("AudioControl interface descriptor length is %d, should be at least 8", (int)asdescr->extra_length); + return FALSE; + } + const struct usb_class_specific_header *extra = (const struct usb_class_specific_header *)asdescr->extra; + uint16_t wTotalLength = extra->wTotalLengthLo + 256 * extra->wTotalLengthHi; + if (wTotalLength > asdescr->extra_length) + { + g_warning("AudioControl interface descriptor total length is %d, but libusb value is %d", (int)wTotalLength, (int)asdescr->extra_length); + return FALSE; + } + if (extra->hdr.bDescriptorType != 36) + { + g_warning("AudioControl interface descriptor type is %d, but expected 36 (CS_INTERFACE)", (int)extra->hdr.bDescriptorType); + return FALSE; + } + if (extra->hdr.bDescriptorSubtype != 1) + { + g_warning("AudioControl interface descriptor type is %d, but expected 1 (HEADER)", (int)extra->hdr.bDescriptorSubtype); + return FALSE; + } + + printf("Device %04x:%04x\n", uai->udi->vid, uai->udi->pid); + printf("hdrlen=%d dt=%d dst=%d ver=%02x%02x total len=%d\n", extra->hdr.bLength, extra->hdr.bDescriptorType, extra->hdr.bDescriptorSubtype, extra->bcdADC[0], extra->bcdADC[1], wTotalLength); + for(int i = 0; i < extra->bInCollection; i++) + { + printf("interface nr %d = %d\n", i, extra->baInterfaceNr[i]); + } + + for(uint32_t pos = extra->hdr.bLength; pos < wTotalLength; pos += asdescr->extra[pos]) + { + const struct usb_ut_descriptor_header *hdr = (const struct usb_ut_descriptor_header *)(asdescr->extra + pos); + if (hdr->bDescriptorType != 36) + { + g_warning("Skipping unit/terminal descriptor type %d,%d", (int)hdr->bDescriptorType, (int)hdr->bDescriptorSubtype); + continue; + } + printf("hdr %d,%d len %d\n", hdr->bDescriptorType, hdr->bDescriptorSubtype, hdr->bLength); + switch(hdr->bDescriptorSubtype) + { + case 2: // INPUT_TERMINAL + { + const struct usbaudio_input_terminal_descriptor *itd = (const struct usbaudio_input_terminal_descriptor *)(hdr + 1); + int wTerminalType = itd->wTerminalTypeHi * 256 + itd->wTerminalTypeLo; + printf("INPUT TERMINAL %d: type %04x (%s)\n", (int)itd->bTerminalID, wTerminalType, get_terminal_type_name(wTerminalType)); + break; + } + case 3: // OUTPUT_TERMINAL + { + const struct usbaudio_output_terminal_descriptor *otd = (const struct usbaudio_output_terminal_descriptor *)(hdr + 1); + int wTerminalType = otd->wTerminalTypeHi * 256 + otd->wTerminalTypeLo; + printf("OUTPUT TERMINAL %d: type %04x (%s) source %d\n", (int)otd->bTerminalID, wTerminalType, get_terminal_type_name(wTerminalType), otd->bSourceID); + break; + } + case 4: // MIXER_UNIT + { + const struct usbaudio_mixer_unit_descriptor1 *mud = (const struct usbaudio_mixer_unit_descriptor1 *)(hdr + 1); + printf("MIXER UNIT %d\n", (int)mud->bUnitID); + for (int i = 0; i < mud->bNrInPins; i++) + { + printf("Input[%d] = %d\n", i, mud->baSourceID[i]); + } + break; + } + case 5: // SELECTOR_UNIT + { + const struct usbaudio_selector_unit_descriptor1 *sud = (const struct usbaudio_selector_unit_descriptor1 *)(hdr + 1); + printf("SELECTOR UNIT %d (%d pins)\n", (int)sud->bUnitID, sud->bNrInPins); + for (int i = 0; i < sud->bNrInPins; i++) + { + printf("Input[%d] = %d\n", i, sud->baSourceID[i]); + } + break; + } + case 6: // FEATURE_UNIT + { + static const char *features[] = {"Mute", "Volume", "Bass", "Mid", "Treble", "EQ", "AGC", "Delay", "Bass Boost", "Loudness" }; + const struct usbaudio_feature_unit_descriptor1 *fud = (const struct usbaudio_feature_unit_descriptor1 *)(hdr + 1); + printf("FEATURE UNIT %d (source = %d, control size %d)\n", (int)fud->bUnitID, fud->bSourceID, fud->bControlSize); + for (int i = 0; i < fud->bControlSize * 8; i++) + { + if (i >= sizeof(features) / sizeof(features[0])) + break; + if (fud->bmaControls[i >> 3] & (1 << (i & 7))) + printf("Master %s\n", features[i]); + } + for (int ch = 1; ch < (hdr->bLength - 7) / fud->bControlSize; ch++) + { + int chofs = ch * fud->bControlSize; + for (int i = 0; i < fud->bControlSize * 8; i++) + { + if (i >= sizeof(features) / sizeof(features[0])) + break; + if (fud->bmaControls[(i >> 3) + chofs] & (1 << (i & 7))) + printf("Channel %d %s\n", ch, features[i]); + } + } + break; + } + default: + printf("Unsupported unit type %d\n", hdr->bDescriptorSubtype); + break; + } + } +#endif + return FALSE; +} + +struct usbaudio_streaming_interface_descriptor_general +{ + struct usb_ut_descriptor_header hdr; // 7, 36, 1 + uint8_t bTerminalLink; + uint8_t bDelay; + uint8_t wFormatTagLo, wFormatTagHi; +}; + +struct usbaudio_streaming_interface_descriptor_format_type_pcm +{ + struct usb_ut_descriptor_header hdr; // 7, 36, 2 + uint8_t bFormatType; + uint8_t bNrChannels; + uint8_t bSubframeSize; + uint8_t bBitResolution; + uint8_t bSamFreqType; + uint8_t taSamFreq[0][3]; +}; + +static gboolean parse_audio_class(struct cbox_usb_io_impl *uii, struct cbox_usb_audio_info *uai, const struct libusb_interface_descriptor *asdescr, gboolean other_config) +{ + // omit alternate setting 0, as it's used to describe a 'standby' setting of the interface (no bandwidth) + if (asdescr->bAlternateSetting == 0) + return FALSE; + if (asdescr->extra_length < 7) + { + g_warning("AudioStreaming interface descriptor length is %d, should be at least 7", (int)asdescr->extra_length); + return FALSE; + } + const struct usbaudio_streaming_interface_descriptor_general *extra = (struct usbaudio_streaming_interface_descriptor_general *)asdescr->extra; + if (extra->hdr.bLength != 7 || extra->hdr.bDescriptorType != 36 || extra->hdr.bDescriptorSubtype != 1) + { + g_warning("The AudioStreaming descriptor does not start with the general descriptor (len %d, type %d, subtype %d)", (int)extra->hdr.bLength, (int)extra->hdr.bDescriptorType, (int)extra->hdr.bDescriptorSubtype); + return FALSE; + } + if (extra->wFormatTagLo != 1 || extra->wFormatTagHi != 0) + { + g_warning("The AudioStreaming descriptor does not describe a PCM device"); + return FALSE; + } + const struct usbaudio_streaming_interface_descriptor_format_type_pcm *fmt = (struct usbaudio_streaming_interface_descriptor_format_type_pcm *)(asdescr->extra + extra->hdr.bLength); + if (fmt->hdr.bLength < 11 || ((fmt->hdr.bLength - 11) % 3) || fmt->hdr.bDescriptorType != 36 || fmt->hdr.bDescriptorSubtype != 2) + { + g_warning("The AudioStreaming descriptor does not have a format type descriptor after general descriptor (len %d, type %d, subtype %d)", (int)fmt->hdr.bLength, (int)fmt->hdr.bDescriptorType, (int)fmt->hdr.bDescriptorSubtype); + return FALSE; + } + + // We need this to tell inputs from outputs - until I implement reasonable + // Audio Control support + const struct libusb_endpoint_descriptor *sync_ep = NULL; + const struct libusb_endpoint_descriptor *ep = get_audio_output_endpoint(asdescr, &sync_ep); + if (ep) + { + if (fmt->bBitResolution != uii->output_resolution * 8) + { + g_warning("Interface %d alternate setting %d does not support %d bit resolution, only %d", asdescr->bInterfaceNumber, asdescr->bAlternateSetting, uii->output_resolution * 8, fmt->bBitResolution); + return FALSE; + } + if (fmt->bSamFreqType) + { + gboolean found = FALSE; + for (int i = 0; i < fmt->bSamFreqType; i++) + { + int sf = fmt->taSamFreq[i][0] + 256 * fmt->taSamFreq[i][1] + 65536 * fmt->taSamFreq[i][2]; + if (sf == uii->sample_rate) + { + found = TRUE; + break; + } + } + if (!found) + { + g_warning("Interface %d alternate setting %d does not support sample rate of %d Hz", asdescr->bInterfaceNumber, asdescr->bAlternateSetting, uii->sample_rate); + for (int i = 0; i < fmt->bSamFreqType; i++) + { + int sf = fmt->taSamFreq[i][0] + 256 * fmt->taSamFreq[i][1] + 65536 * fmt->taSamFreq[i][2]; + g_warning("Sample rate[%d] = %d Hz", i, sf); + } + return FALSE; + } + } + // XXXKF in case of continuous sample rate, check the limits... assuming + // that there are any devices with continuous sample rate, that is. + if (other_config) + return TRUE; + if (uai->epdesc.found) + return FALSE; + if (sync_ep) + g_warning("Interface %d alt-setting %d endpoint %02x (synched via %02x) looks promising", asdescr->bInterfaceNumber, asdescr->bAlternateSetting, ep->bEndpointAddress, sync_ep->bEndpointAddress); + else + g_warning("Interface %d alt-setting %d endpoint %02x looks promising", asdescr->bInterfaceNumber, asdescr->bAlternateSetting, ep->bEndpointAddress); + uai->intf = asdescr->bInterfaceNumber; + uai->alt_setting = asdescr->bAlternateSetting; + fill_endpoint_desc(&uai->epdesc, ep); + uai->sync_protocol = (sync_ep != NULL) ? USBAUDIOSYNC_PROTOCOL_CLASS : USBAUDIOSYNC_PROTOCOL_NONE; + if (sync_ep) + fill_endpoint_desc(&uai->sync_epdesc, sync_ep); + else + uai->sync_epdesc.found = FALSE; + return TRUE; + } + return FALSE; +} + +static gboolean parse_midi_class(struct cbox_usb_midi_info *umi, const struct libusb_interface_descriptor *asdescr, gboolean other_config, gboolean is_output) +{ + const struct libusb_endpoint_descriptor *ep = get_midi_endpoint(asdescr, is_output); + if (!ep) + return FALSE; + + if (other_config) + return TRUE; + + struct usbio_endpoint_descriptor *epd = is_output ? &umi->epdesc_out : &umi->epdesc_in; + + if (epd->found) + return FALSE; + umi->intf = asdescr->bInterfaceNumber; + umi->alt_setting = asdescr->bAlternateSetting; + fill_endpoint_desc(epd, ep); + return TRUE; +} + +static gboolean inspect_device(struct cbox_usb_io_impl *uii, struct libusb_device *dev, uint16_t busdevadr, gboolean probe_only) +{ + struct libusb_device_descriptor dev_descr; + int bus = busdevadr >> 8; + int devadr = busdevadr & 255; + + if (0 != libusb_get_device_descriptor(dev, &dev_descr)) + { + g_warning("USB device %03d:%03d - cannot get device descriptor (will retry)", bus, devadr); + return FALSE; + } + + struct cbox_usb_device_info *udi = g_hash_table_lookup(uii->device_table, GINT_TO_POINTER(busdevadr)); + if (!udi) + { + struct libusb_config_descriptor *cfg_descr = NULL; + if (0 != libusb_get_active_config_descriptor(dev, &cfg_descr)) + return FALSE; + udi = malloc(sizeof(struct cbox_usb_device_info)); + udi->dev = dev; + udi->handle = NULL; + udi->status = CBOX_DEVICE_STATUS_PROBING; + udi->active_config = cfg_descr->bConfigurationValue; + udi->bus = bus; + udi->devadr = devadr; + udi->busdevadr = busdevadr; + udi->vid = dev_descr.idVendor; + udi->pid = dev_descr.idProduct; + udi->configs_with_midi = 0; + udi->configs_with_audio = 0; + udi->is_midi = FALSE; + udi->is_audio = FALSE; + udi->last_probe_time = time(NULL); + udi->failures = 0; + g_hash_table_insert(uii->device_table, GINT_TO_POINTER(busdevadr), udi); + libusb_free_config_descriptor(cfg_descr); + } + else + if (udi->vid == dev_descr.idVendor && udi->pid == dev_descr.idProduct) + { + // device already open or determined to be + if (udi->status == CBOX_DEVICE_STATUS_OPENED || + udi->status == CBOX_DEVICE_STATUS_UNSUPPORTED) + return FALSE; + // give up after 10 attempts to query or open the device + if (udi->failures > 10) + return FALSE; + // only do ~1 attempt per second + if (probe_only && time(NULL) == udi->last_probe_time) + return FALSE; + udi->last_probe_time = time(NULL); + } + + struct cbox_usb_audio_info uainf; + cbox_usb_audio_info_init(&uainf, udi); + + struct cbox_usb_midi_info uminf; + cbox_usb_midi_info_init(&uminf, udi); + + gboolean is_audio = FALSE; + + // printf("%03d:%03d Device %04X:%04X\n", bus, devadr, dev_descr.idVendor, dev_descr.idProduct); + for (int ci = 0; ci < (int)dev_descr.bNumConfigurations; ci++) + { + struct libusb_config_descriptor *cfg_descr = NULL; + // if this is not the current config, and another config with MIDI input + // has already been found, do not look any further + if (0 != libusb_get_config_descriptor(dev, ci, &cfg_descr)) + { + udi->failures++; + g_warning("%03d:%03d - cannot get configuration descriptor (try %d)", bus, devadr, udi->failures); + return FALSE; + } + + int cur_config = cfg_descr->bConfigurationValue; + gboolean other_config = cur_config != udi->active_config; + uint32_t config_mask = 0; + // XXXKF not sure about legal range for bConfigurationValue + if(cfg_descr->bConfigurationValue >= 0 && cfg_descr->bConfigurationValue < 32) + config_mask = 1 << cfg_descr->bConfigurationValue; + else + g_warning("Unexpected configuration value %d", cfg_descr->bConfigurationValue); + + for (int ii = 0; ii < cfg_descr->bNumInterfaces; ii++) + { + const struct libusb_interface *idescr = &cfg_descr->interface[ii]; + for (int as = 0; as < idescr->num_altsetting; as++) + { + const struct libusb_interface_descriptor *asdescr = &idescr->altsetting[as]; + if (asdescr->bInterfaceClass == LIBUSB_CLASS_AUDIO && asdescr->bInterfaceSubClass == 1) // Audio control + { + if (parse_audio_control_class(&uainf, asdescr, other_config) && other_config) + udi->configs_with_audio |= config_mask; + } + else if (asdescr->bInterfaceClass == LIBUSB_CLASS_AUDIO && asdescr->bInterfaceSubClass == 2) // Audio streaming + { + if (parse_audio_class(uii, &uainf, asdescr, other_config) && other_config) + udi->configs_with_audio |= config_mask; + } + else if (asdescr->bInterfaceClass == LIBUSB_CLASS_AUDIO && asdescr->bInterfaceSubClass == 3) // MIDI streaming + { + if (parse_midi_class(&uminf, asdescr, other_config, FALSE) && other_config) + udi->configs_with_midi |= config_mask; + if (parse_midi_class(&uminf, asdescr, other_config, TRUE) && other_config) + udi->configs_with_midi |= config_mask; + uminf.protocol = USBMIDI_PROTOCOL_CLASS; + } + else if (udi->vid == 0x09e8 && udi->pid == 0x0062) // Akai MPD16 + { + uminf.intf = asdescr->bInterfaceNumber; + uminf.alt_setting = asdescr->bAlternateSetting; + uminf.protocol = USBMIDI_PROTOCOL_MPD16; + fill_endpoint_by_address(asdescr, 0x82, &uminf.epdesc_in); + } + else if (udi->vid == 0x1235 && udi->pid == 0x000a) // Novation Nocturn + { + uminf.intf = asdescr->bInterfaceNumber; + uminf.alt_setting = asdescr->bAlternateSetting; + fill_endpoint_by_address(asdescr, 0x81, &uminf.epdesc_in); + fill_endpoint_by_address(asdescr, 0x02, &uminf.epdesc_out); + uminf.epdesc_in.interrupt = TRUE; + uminf.epdesc_out.interrupt = TRUE; + uminf.protocol = USBMIDI_PROTOCOL_NOCTURN; + } + } + } + libusb_free_config_descriptor(cfg_descr); + } + if (!uminf.epdesc_in.found && !uminf.epdesc_out.found && udi->configs_with_midi) + g_warning("%03d:%03d - MIDI port available on different configs: mask=0x%x", bus, devadr, udi->configs_with_midi); + + if (uainf.epdesc.found) // Class-compliant USB audio device + is_audio = TRUE; + if (udi->vid == 0x13b2 && udi->pid == 0x0030) // Alesis Multimix 8 + { + uainf.sync_protocol = USBAUDIOSYNC_PROTOCOL_MULTIMIX8; // not used later + is_audio = TRUE; + } + + // All configs/interfaces/alts scanned, nothing interesting found -> mark as unsupported + udi->is_midi = uminf.epdesc_in.found || uminf.epdesc_out.found; + udi->is_audio = is_audio; + if (!udi->is_midi && !udi->is_audio) + { + udi->status = CBOX_DEVICE_STATUS_UNSUPPORTED; + return FALSE; + } + + gboolean opened = FALSE; + struct libusb_device_handle *handle = NULL; + int err = libusb_open(dev, &handle); + if (0 != err) + { + g_warning("Cannot open device %03d:%03d: %s; errno = %s", bus, devadr, libusb_error_name(err), strerror(errno)); + udi->failures++; + return FALSE; + } + + if (probe_only) + { + libusb_close(handle); + // Make sure that the reconnection code doesn't bail out due to + // last_probe_time == now. + udi->last_probe_time = 0; + return udi->is_midi || udi->is_audio; + } + + if (uminf.epdesc_in.found || uminf.epdesc_out.found) + { + g_debug("Found MIDI device %03d:%03d, trying to open", bus, devadr); + if (0 != usbio_open_midi_interface(uii, &uminf, handle)) + opened = TRUE; + } + if (uainf.epdesc.found) + { + GError *error = NULL; + if (usbio_open_audio_interface(uii, &uainf, handle, &error)) + { + // should have already been marked as opened by the MIDI code, but + // I might add the ability to disable some MIDI interfaces at some point + udi->status = CBOX_DEVICE_STATUS_OPENED; + opened = TRUE; + } + else + { + g_warning("Cannot open class-compliant USB audio output: %s", error->message); + g_error_free(error); + } + } + else if (udi->vid == 0x13b2 && udi->pid == 0x0030) + { + GError *error = NULL; + if (usbio_open_audio_interface_multimix(uii, bus, devadr, handle, &error)) + { + // should have already been marked as opened by the MIDI code, but + // I might add the ability to disable some MIDI interfaces at some point + udi->status = CBOX_DEVICE_STATUS_OPENED; + opened = TRUE; + } + else + { + g_warning("Cannot open Alesis Multimix audio output: %s", error->message); + g_error_free(error); + } + } + + if (!opened) + { + udi->failures++; + libusb_close(handle); + } + else + udi->handle = handle; + + return opened; +} + +gboolean usbio_scan_devices(struct cbox_usb_io_impl *uii, gboolean probe_only) +{ + struct libusb_device **dev_list; + size_t i, num_devices; + gboolean added = FALSE; + gboolean removed = FALSE; + + num_devices = libusb_get_device_list(probe_only ? uii->usbctx_probe : uii->usbctx, &dev_list); + + uint16_t *busdevadrs = malloc(sizeof(uint16_t) * num_devices); + for (i = 0; i < num_devices; i++) + { + struct libusb_device *dev = dev_list[i]; + int bus = libusb_get_bus_number(dev); + int devadr = libusb_get_device_address(dev); + busdevadrs[i] = (bus << 8) | devadr; + } + + GList *prev_keys = g_hash_table_get_values(uii->device_table); + for (GList *p = prev_keys; p; p = p->next) + { + gboolean found = FALSE; + struct cbox_usb_device_info *udi = p->data; + for (i = 0; !found && i < num_devices; i++) + found = busdevadrs[i] == udi->busdevadr; + if (!found) + { + // Only specifically trigger removal if the device is ours + if (udi->status == CBOX_DEVICE_STATUS_OPENED) + { + g_message("Disconnected: %03d:%03d (%s)", udi->bus, udi->devadr, probe_only ? "probe" : "reconfigure"); + removed = TRUE; + } + if (!probe_only) + usbio_forget_device(uii, udi); + } + } + g_list_free(prev_keys); + + for (i = 0; i < num_devices; i++) + added = inspect_device(uii, dev_list[i], busdevadrs[i], probe_only) || added; + + free(busdevadrs); + libusb_free_device_list(dev_list, 0); + return added || removed; +} + +void usbio_forget_device(struct cbox_usb_io_impl *uii, struct cbox_usb_device_info *devinfo) +{ + g_hash_table_remove(uii->device_table, GINT_TO_POINTER(devinfo->busdevadr)); + for (GList *p = uii->midi_ports, *pNext = NULL; p; p = pNext) + { + pNext = p->next; + struct cbox_usb_midi_interface *umi = p->data; + if (umi->busdevadr == devinfo->busdevadr) + { + uii->midi_ports = g_list_delete_link(uii->midi_ports, p); + free(umi); + } + } + if (uii->handle_audiodev == devinfo->handle) + uii->handle_audiodev = NULL; + libusb_close(devinfo->handle); + free(devinfo); +} + +#endif \ No newline at end of file diff --git a/template/calfbox/wavebank.c b/template/calfbox/wavebank.c new file mode 100644 index 0000000..1b59351 --- /dev/null +++ b/template/calfbox/wavebank.c @@ -0,0 +1,541 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#include "cmd.h" +#include "config.h" +#include "config-api.h" +#include "dspmath.h" +#include "errors.h" +#include "tarfile.h" +#include "wavebank.h" +#include +#include +#include +#include +#include +#include + +#define STD_WAVEFORM_FRAMES 1024 +#define STD_WAVEFORM_BITS 10 + +/////////////////////////////////////////////////////////////////////////////// + +// Sine table +static complex float euler_table[STD_WAVEFORM_FRAMES]; + +// Bit reversal table +static int map_table[STD_WAVEFORM_FRAMES]; + +// Initialise tables using for FFT +static void init_tables(void) +{ + int rev = 1 << (STD_WAVEFORM_BITS - 1); + for (int i = 0; i < STD_WAVEFORM_FRAMES; i++) + { + euler_table[i] = cos(i * 2 * M_PI / STD_WAVEFORM_FRAMES) + I * sin(i * 2 * M_PI / STD_WAVEFORM_FRAMES); + int ni = 0; + for (int j = 0; j < STD_WAVEFORM_BITS; j++) + { + if (i & (rev >> j)) + ni = ni | (1 << j); + } + map_table[i] = ni; + } +} + +// Trivial implementation of Cooley-Tukey, only works for even values of ANALYSIS_BUFFER_BITS +static void my_fft_main(complex float output[STD_WAVEFORM_FRAMES]) +{ + complex float temp[STD_WAVEFORM_FRAMES]; + + for (int i = 0; i < STD_WAVEFORM_BITS; i++) + { + complex float *src = (i & 1) ? temp : output; + complex float *dst = (i & 1) ? output : temp; + int invi = STD_WAVEFORM_BITS - i - 1; + int disp = 1 << i; + int mask = disp - 1; + + for (int j = 0; j < STD_WAVEFORM_FRAMES / 2; j++) + { + int jj1 = (j & mask) + ((j & ~mask) << 1); // insert 0 at i'th bit to get the left arm of the butterfly + int jj2 = jj1 + disp; // insert 1 at i'th bit to get the right arm + assert((jj1 + disp) == (jj1 | disp)); + + // e^iw + complex float eiw1 = euler_table[(jj1 << invi) & (STD_WAVEFORM_FRAMES - 1)]; + complex float eiw2 = euler_table[(jj2 << invi) & (STD_WAVEFORM_FRAMES - 1)]; + + // printf("%d -> %d, %d\n", j, jj, jj + disp); + butterfly(&dst[jj1], &dst[jj2], src[jj1], src[jj2], eiw1, eiw2); + } + } +} + +static void my_fft_r2c(complex float output[STD_WAVEFORM_FRAMES], int16_t input[STD_WAVEFORM_FRAMES]) +{ + assert(!(STD_WAVEFORM_BITS&1)); + // Copy + bit reversal addressing + for (int i = 0; i < STD_WAVEFORM_FRAMES; i++) + output[i] = input[map_table[i]] * (1.0 / STD_WAVEFORM_FRAMES); + + my_fft_main(output); + +} + +static void my_ifft_c2r(int16_t output[STD_WAVEFORM_FRAMES], complex float input[STD_WAVEFORM_FRAMES]) +{ + complex float temp2[STD_WAVEFORM_FRAMES]; + for (int i = 0; i < STD_WAVEFORM_FRAMES; i++) + temp2[i] = input[map_table[i]]; + assert(!(STD_WAVEFORM_BITS&1)); + + my_fft_main(temp2); + // Copy + bit reversal addressing + float maxv = 0; + for (int i = 0; i < STD_WAVEFORM_FRAMES; i++) + { + float value = creal(temp2[i]); + if (value < -32768) value = -32768; + if (value > 32767) value = 32767; + if (fabs(value) > maxv) + maxv = fabs(value); + output[i] = (int16_t)value; + } +} + +struct wave_bank +{ + int64_t bytes, maxbytes, serial_no; + GHashTable *waveforms_by_name, *waveforms_by_id; + GSList *std_waveforms; + uint32_t streaming_prefetch_size; +}; + +static struct wave_bank bank; + +GQuark cbox_waveform_error_quark(void) +{ + return g_quark_from_string("cbox-waveform-error-quark"); +} + +float func_sine(float v, void *user_data) +{ + return sin(2 * M_PI * v); +} + +float func_silence(float v, void *user_data) +{ + return 0.f; +} + +float func_sqr(float v, void *user_data) +{ + return v < 0.5 ? -1 : 1; +} + +float func_saw(float v, void *user_data) +{ + return 2 * v - 1; +} + +float func_tri(float v, void *user_data) +{ + if (v <= 0.25f) + return v * 4; + if (v <= 0.75f) + return 1 - (v - 0.25f) * 4; + return -1 + 4 * (v - 0.75f); +} + +void cbox_waveform_generate_levels(struct cbox_waveform *waveform, int levels, double ratio) +{ + complex float output[STD_WAVEFORM_FRAMES], bandlimited[STD_WAVEFORM_FRAMES]; + my_fft_r2c(output, waveform->data); + int N = STD_WAVEFORM_FRAMES; + + waveform->levels = calloc(levels, sizeof(struct cbox_waveform_level)); + double rate = 65536.0 * 65536.0; // / waveform->info.frames; + double orig_rate = 65536.0 * 65536.0; // / waveform->info.frames; + for (int i = 0; i < levels; i++) + { + int harmonics = N / 2 / (rate / orig_rate); + bandlimited[0] = 0; + + if (harmonics > 0) + { + for (int j = 1; j <= harmonics; j++) + { + bandlimited[j] = output[j]; + bandlimited[N - j] = output[N - j]; + } + for (int j = harmonics; j <= N / 2; j++) + bandlimited[j] = bandlimited [N - j] = 0; + } + + waveform->levels[i].data = calloc(N + MAX_INTERPOLATION_ORDER, sizeof(int16_t)); + my_ifft_c2r(waveform->levels[i].data, bandlimited); + memcpy(waveform->levels[i].data + N, waveform->levels[i].data, MAX_INTERPOLATION_ORDER * sizeof(int16_t)); + waveform->levels[i].max_rate = (uint64_t)(rate); + rate *= ratio; + } + waveform->level_count = levels; +} + +void cbox_wavebank_add_std_waveform(const char *name, float (*getfunc)(float v, void *user_data), void *user_data, int levels) +{ + int nsize = STD_WAVEFORM_FRAMES; + int16_t *wave = calloc(nsize, sizeof(int16_t)); + for (int i = 0; i < nsize; i++) + { + float v = getfunc(i * 1.0 / nsize, user_data); + if (fabs(v) > 1) + v = (v < 0) ? -1 : 1; + // cannot use full scale here, because bandlimiting will introduce + // some degree of overshoot + wave[i] = (int16_t)(25000 * v); + } + struct cbox_waveform *waveform = calloc(1, sizeof(struct cbox_waveform)); + waveform->data = wave; + waveform->info.channels = 1; + waveform->preloaded_frames = waveform->info.frames = nsize; + waveform->info.samplerate = (int)(nsize * 261.6255); + waveform->id = ++bank.serial_no; + waveform->bytes = waveform->info.channels * 2 * (waveform->info.frames + 1); + waveform->refcount = 1; + waveform->canonical_name = g_strdup(name); + waveform->display_name = g_strdup(name); + waveform->has_loop = TRUE; + waveform->loop_start = 0; + waveform->loop_end = nsize; + waveform->levels = NULL; + waveform->level_count = 0; + + if (levels) + cbox_waveform_generate_levels(waveform, levels, 2); + + g_hash_table_insert(bank.waveforms_by_name, waveform->canonical_name, waveform); + g_hash_table_insert(bank.waveforms_by_id, &waveform->id, waveform); + bank.std_waveforms = g_slist_prepend(bank.std_waveforms, waveform); + // These waveforms are not included in the bank size, I don't think it has + // much value for the user. +} + +void cbox_wavebank_init() +{ + init_tables(); + + bank.bytes = 0; + bank.maxbytes = 0; + bank.serial_no = 0; + bank.waveforms_by_name = g_hash_table_new(g_str_hash, g_str_equal); + bank.waveforms_by_id = g_hash_table_new(g_int_hash, g_int_equal); + bank.std_waveforms = NULL; + bank.streaming_prefetch_size = cbox_config_get_int("streaming", "prefetch_size", 65536); + + cbox_wavebank_add_std_waveform("*sine", func_sine, NULL, 0); + // XXXKF this should not be a real waveform + cbox_wavebank_add_std_waveform("*silence", func_silence, NULL, 0); + cbox_wavebank_add_std_waveform("*saw", func_saw, NULL, 11); + cbox_wavebank_add_std_waveform("*sqr", func_sqr, NULL, 11); + cbox_wavebank_add_std_waveform("*tri", func_tri, NULL, 11); +} + +struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struct cbox_tarfile *tarfile, const char *sample_dir, const char *filename, GError **error) +{ + if (!filename) + { + g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED, "%s: no filename specified", context_name); + return NULL; + } + + // Built in waveforms don't go through path canonicalization + if (filename[0] == '*') + { + gpointer value = g_hash_table_lookup(bank.waveforms_by_name, filename); + if (value) + { + struct cbox_waveform *waveform = value; + cbox_waveform_ref(waveform); + return waveform; + } + } + + gchar *value_copy = g_strdup(filename); + for (int i = 0; value_copy[i]; i++) + { + if (value_copy[i] == '\\') + value_copy[i] = '/'; + } + gchar *pathname = value_copy[0] == '/' ? g_strdup(value_copy) : g_build_filename(sample_dir, value_copy, NULL); + g_free(value_copy); + + char *canonical = NULL; + if (tarfile) + canonical = g_strdup_printf("sbtar:%s;%s", tarfile->file_pathname, pathname); + else + { + // make sure canonical is always allocated on the same (glib) heap + char *p = realpath(pathname, NULL); + if (p) + { + canonical = g_strdup(p); + free(p); + } + } + if (!canonical) + { + g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED, "%s: cannot find a real path for '%s': %s", context_name, pathname, strerror(errno)); + g_free(pathname); + return NULL; + } + gpointer value = g_hash_table_lookup(bank.waveforms_by_name, canonical); + if (value) + { + g_free(pathname); + g_free(canonical); + + struct cbox_waveform *waveform = value; + cbox_waveform_ref(waveform); + return waveform; + } + + struct cbox_waveform *waveform = calloc(1, sizeof(struct cbox_waveform)); + SNDFILE *sndfile = NULL; + struct cbox_taritem *taritem = NULL; + if (tarfile) + { + taritem = cbox_tarfile_get_item_by_name(tarfile, pathname, TRUE); + if (taritem) + sndfile = cbox_tarfile_opensndfile(tarfile, taritem, &waveform->sndstream, &waveform->info); + } + else + sndfile = sf_open(pathname, SFM_READ, &waveform->info); + if (!sndfile) + { + g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "%s: cannot open '%s'", context_name, pathname); + g_free(pathname); + g_free(canonical); + free(waveform); + return NULL; + } + + SF_INSTRUMENT instrument; + uint32_t nshorts; + if (waveform->info.channels != 1 && waveform->info.channels != 2) + { + g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED, + "%s: cannot open file '%s': unsupported channel count %d", context_name, pathname, (int)waveform->info.channels); + sf_close(sndfile); + free(canonical); + g_free(pathname); + return NULL; + } + g_free(pathname); + uint32_t preloaded_frames = waveform->info.frames; + // If sample is larger than 2x prefetch buffer size, then load only + // a prefetch buffer worth of data, and stream the rest. + if (preloaded_frames > 2 * bank.streaming_prefetch_size) + preloaded_frames = bank.streaming_prefetch_size; + waveform->id = ++bank.serial_no; + waveform->bytes = waveform->info.channels * 2 * preloaded_frames; + waveform->data = malloc(waveform->bytes); + waveform->refcount = 1; + waveform->canonical_name = canonical; + waveform->display_name = g_filename_display_name(canonical); + waveform->has_loop = FALSE; + waveform->levels = NULL; + waveform->level_count = 0; + waveform->preloaded_frames = preloaded_frames; + waveform->tarfile = tarfile; + waveform->taritem = taritem; + + if (sf_command(sndfile, SFC_GET_INSTRUMENT, &instrument, sizeof(SF_INSTRUMENT))) + { + for (int i = 0; i < instrument.loop_count; i++) + { + if (instrument.loops[i].mode == SF_LOOP_FORWARD) + { + waveform->loop_start = instrument.loops[i].start; + waveform->loop_end = instrument.loops[i].end; + waveform->has_loop = TRUE; + break; + } + } + } + + nshorts = waveform->info.channels * preloaded_frames; + for (uint32_t i = 0; i < nshorts; i++) + waveform->data[i] = 0; + sf_readf_short(sndfile, waveform->data, preloaded_frames); + sf_close(sndfile); + bank.bytes += waveform->bytes; + if (bank.bytes > bank.maxbytes) + bank.maxbytes = bank.bytes; + g_hash_table_insert(bank.waveforms_by_name, waveform->canonical_name, waveform); + g_hash_table_insert(bank.waveforms_by_id, &waveform->id, waveform); + + return waveform; +} + +int64_t cbox_wavebank_get_bytes() +{ + return bank.bytes; +} + +int64_t cbox_wavebank_get_maxbytes() +{ + return bank.maxbytes; +} + +int cbox_wavebank_get_count() +{ + return g_hash_table_size(bank.waveforms_by_id); +} + +struct cbox_waveform *cbox_wavebank_peek_waveform_by_id(int id) +{ + return g_hash_table_lookup(bank.waveforms_by_id, &id); +} + +void cbox_wavebank_foreach(void (*cb)(void *, struct cbox_waveform *), void *user_data) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, bank.waveforms_by_id); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + (*cb)(user_data, value); + } +} + +void cbox_wavebank_close() +{ + if (bank.bytes > 0) + g_warning("Warning: %lld bytes in unfreed samples", (long long int)bank.bytes); + while(bank.std_waveforms) + { + cbox_waveform_unref((struct cbox_waveform *)bank.std_waveforms->data); + bank.std_waveforms = g_slist_delete_link(bank.std_waveforms, bank.std_waveforms); + } + g_hash_table_destroy(bank.waveforms_by_id); + g_hash_table_destroy(bank.waveforms_by_name); + bank.waveforms_by_id = NULL; + bank.waveforms_by_name = NULL; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// + +void cbox_waveform_ref(struct cbox_waveform *waveform) +{ + ++waveform->refcount; +} + +void cbox_waveform_unref(struct cbox_waveform *waveform) +{ + if (--waveform->refcount > 0) + return; + + g_hash_table_remove(bank.waveforms_by_name, waveform->canonical_name); + g_hash_table_remove(bank.waveforms_by_id, &waveform->id); + bank.bytes -= waveform->bytes; + + g_free(waveform->display_name); + g_free(waveform->canonical_name); + for (int i = 0; i < waveform->level_count; i++) + free(waveform->levels[i].data); + free(waveform->levels); + free(waveform->data); + free(waveform); + +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +struct waves_foreach_data +{ + struct cbox_command_target *fb; + GError **error; + gboolean success; +}; + +void wave_list_cb(void *user_data, struct cbox_waveform *waveform) +{ + struct waves_foreach_data *wfd = user_data; + + wfd->success = wfd->success && cbox_execute_on(wfd->fb, NULL, "/waveform", "i", wfd->error, (int)waveform->id); +} + +static gboolean waves_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error) +{ + if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + // XXXKF this only supports 4GB - not a big deal for now yet? + return cbox_execute_on(fb, NULL, "/bytes", "i", error, (int)cbox_wavebank_get_bytes()) && + cbox_execute_on(fb, NULL, "/max_bytes", "i", error, (int)cbox_wavebank_get_maxbytes()) && + cbox_execute_on(fb, NULL, "/count", "i", error, (int)cbox_wavebank_get_count()) + ; + } + else if (!strcmp(cmd->command, "/list") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + struct waves_foreach_data wfd = { fb, error, TRUE }; + cbox_wavebank_foreach(wave_list_cb, &wfd); + return wfd.success; + } + else if (!strcmp(cmd->command, "/info") && !strcmp(cmd->arg_types, "i")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + + int id = CBOX_ARG_I(cmd, 0); + struct cbox_waveform *waveform = cbox_wavebank_peek_waveform_by_id(id); + if (waveform == NULL) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Waveform %d not found", id); + return FALSE; + } + assert(id == waveform->id); + if (!cbox_execute_on(fb, NULL, "/filename", "s", error, waveform->canonical_name)) // XXXKF convert to utf8 + return FALSE; + if (!cbox_execute_on(fb, NULL, "/name", "s", error, waveform->display_name)) + return FALSE; + if (!cbox_execute_on(fb, NULL, "/bytes", "i", error, (int)waveform->bytes)) + return FALSE; + if (waveform->has_loop && !cbox_execute_on(fb, NULL, "/loop", "ii", error, (int)waveform->loop_start, (int)waveform->loop_end)) + return FALSE; + return TRUE; + } + else + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types); + return FALSE; + } +} + +struct cbox_command_target cbox_waves_cmd_target = +{ + .process_cmd = waves_process_cmd, + .user_data = NULL +}; diff --git a/template/calfbox/wavebank.h b/template/calfbox/wavebank.h new file mode 100644 index 0000000..62239d8 --- /dev/null +++ b/template/calfbox/wavebank.h @@ -0,0 +1,77 @@ +/* +Calf Box, an open source musical instrument. +Copyright (C) 2010-2011 Krzysztof Foltman + +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 . +*/ + +#ifndef CBOX_WAVEBANK_H +#define CBOX_WAVEBANK_H + +#include +#include +#include + +#define MAX_INTERPOLATION_ORDER 3 + +#define CBOX_WAVEFORM_ERROR cbox_waveform_error_quark() + +enum CboxWaveformError +{ + CBOX_WAVEFORM_ERROR_FAILED, +}; + +struct cbox_waveform_level +{ + int16_t *data; + uint64_t max_rate; +}; + +struct cbox_waveform +{ + int16_t *data; + SF_INFO info; + int id; + int refcount; + size_t bytes; + size_t preloaded_frames; + gchar *canonical_name; + gchar *display_name; + gboolean has_loop; + uint32_t loop_start, loop_end; + struct cbox_tarfile *tarfile; + struct cbox_taritem *taritem; + struct cbox_tarfile_sndstream sndstream; + + struct cbox_waveform_level *levels; + int level_count; +}; + +extern struct cbox_command_target cbox_waves_cmd_target; + +extern void cbox_wavebank_init(void); +extern struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struct cbox_tarfile *tf, const char *sample_dir, const char *filename, GError **error); +extern struct cbox_waveform *cbox_wavebank_peek_waveform_by_id(int id); +extern void cbox_wavebank_foreach(void (*cb)(void *user_data, struct cbox_waveform *waveform), void *user_data); +extern void cbox_wavebank_add_std_waveform(const char *name, float (*getfunc)(float v, void *user_data), void *user_data, int levels); +extern int cbox_wavebank_get_count(void); +extern int64_t cbox_wavebank_get_bytes(void); +extern int64_t cbox_wavebank_get_maxbytes(void); +extern void cbox_wavebank_close(void); + +extern void cbox_waveform_ref(struct cbox_waveform *waveform); +extern void cbox_waveform_unref(struct cbox_waveform *waveform); + + +#endif diff --git a/template/configure.template b/template/configure.template new file mode 100644 index 0000000..079db4e --- /dev/null +++ b/template/configure.template @@ -0,0 +1,63 @@ + + +#this file is sourced by ../configure +#therefore all paths are on that level, not our own. + +#debugsym=true +prefix=/usr/local +required_version_python=3.6 +required_version_pyqt=5.0 + + +for arg in "$@"; do + case "$arg" in + --prefix=*) + prefix=`echo $arg | sed 's/--prefix=//'` + ;; + + #--enable-debug) + # debugsym=true;; + #--disable-debug) + # debugsym=false;; + + --help) + echo 'usage: ./configure [options]' + echo 'options:' + echo ' --prefix=: installation prefix' + #echo ' --enable-debug: include debug symbols' + #echo ' --disable-debug: do not include debug symbols' + echo 'all invalid options are silently ignored' + exit 0 + ;; + esac +done + +echo "PREFIX=$prefix" + +echo "Checking Dependencies. No output is good" + +function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } + +command -v python3 >/dev/null 2>&1 || { echo >&2 "Python3 >= $required_version_python is required but it's not installed. Aborting."; exit 1; } +PY3VERSION=$(python3 -c 'import platform; print(platform.python_version())') +if version_gt $required_version_python $PY3VERSION; then echo "Python3 must be version >= $required_version_python but is $PY3VERSION. Aborting."; exit 1; fi + +python3 -c 'import PyQt5' >/dev/null 2>&1 || { echo >&2 "PyQt for Python3 >= $required_version_pyqt is required but it's not installed. Aborting."; exit 1; } +PYQTVERSION=$(python3 -c 'from PyQt5.QtCore import QT_VERSION_STR; print(QT_VERSION_STR)') +if version_gt $required_version_pyqt $PYQTVERSION; then echo "PyQt must be version >= $required_version_pyqt but is $PYQTVERSION. Aborting."; exit 1; fi + +echo "Sub-Configure for calfbox" +set -e +#We need to be in the directory, +cd template/calfbox && ./autogen.sh && ./configure --prefix=$(pwd)/../../sitepackages --without-ncurses --without-python --without-libusb $cboxconfigure > /dev/null +cd ../.. + +echo "generating makefile" +printf "PREFIX=$prefix\nPROGRAM=$program\nVERSION=$version\n" >Makefile + +#if $debugsym; then +# echo 'dbg = -g' >>Makefile +#fi +cat template/Makefile.in >>Makefile + +echo 'configuration complete, type make to build.' diff --git a/template/documentation/LICENSE b/template/documentation/LICENSE new file mode 100644 index 0000000..aa44e5a --- /dev/null +++ b/template/documentation/LICENSE @@ -0,0 +1,430 @@ +from: https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt + +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/template/documentation/build.py b/template/documentation/build.py new file mode 100644 index 0000000..f07fff2 --- /dev/null +++ b/template/documentation/build.py @@ -0,0 +1,140 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +This documentation is licensed under Creative Commons-BY-SA-4.0. +Please read the provided documentation/LICENSE file or visit +https://creativecommons.org/licenses/by-sa/4.0/legalcode + +The documentation is built statically and does not belong to the normal build process with configure and make +Its updating is part of the development process, not packaging and running. +The correct out/ dir is already part of git. + +.adoc is asciidoctor, not simple asciidoc. +""" + + +#We still split between template and non-template, eventhough this is not the case in Agordejo. +#However, this enables an easier diff across projects to keep track of changes that we need to do manually here +#TODO: Unify in the future by creating calfbox and NSM client as documentation "modules" + +#Make the readme + +import sys +sys.path.append("../../engine") +from config import METADATA +import subprocess +from os import getcwd +import os.path +assert os.path.exists(os.path.join(getcwd(), __file__)), (getcwd(), __file__) +import datetime + + +#Readme + +with open("readme.template", "r") as r: + template_readme = r.read() + +template_readme = template_readme.replace("", datetime.datetime.now().isoformat()) +template_readme = template_readme.replace("", METADATA["name"]) +template_readme = template_readme.replace("", METADATA["version"]) +template_readme = template_readme.replace("", METADATA["shortName"]) +template_readme = template_readme.replace("", METADATA["description"]) +template_readme = template_readme.replace("", METADATA["dependencies"]) +template_readme = template_readme.replace("", METADATA["author"]) + +with open ("../../README.md", "w") as w: + w.write(template_readme) + +print ("Built /README.md") + +#Documentation index + +with open("index.adoc.template", "r") as r: + template_index = r.read() + +template_index = template_index.replace("", METADATA["name"]) +template_index = template_index.replace("", METADATA["shortName"]) +template_index = template_index.replace("", METADATA["version"]) +template_index = template_index.replace("", METADATA["author"]) + +with open ("../../documentation/index.adoc", "w") as w: + w.write(template_index) + +#print ("Built /documentation/index.adoc. You still need to run /documentation/build-documentation.sh manually") +#print ("Built /documentation/index.adoc") + +#Documentation + +METADATA["supportedLanguages"].update({"English":""}) +for language in METADATA["supportedLanguages"].keys(): + + language = language.lower() + + try: + with open(f"{language}.adoc.template", "r") as r: + template = r.read() + except: + continue #language not yet supported as manual + + for key, value in METADATA.items(): #all strings + if type(value) is str: + template = template.replace(f"<{key}>", value) + + if language == "english": + template = template.replace("", "== Introduction\n\n" + METADATA["description"]) + + + with open (f"../../documentation/{language}.part.adoc", "r") as clientPart: + template = template.replace("", clientPart.read()) + + with open (f"../../documentation/{language}.adoc", "w") as w: + w.write(template) + + +#Create manpage +#Needs help2man +manpage_template = f""" +[name] +{METADATA["name"]} - {METADATA["tagline"]} + +[usage] +{METADATA["description"]} + +[Reporting bugs] +https://www.laborejo.org/bugs + +[copyright] +{METADATA["name"]} {METADATA["version"]} - Copyright {METADATA["year"]} +{METADATA["author"]} +https://www.laborejo.org/ + +[examples] +Start {METADATA["shortName"]} through NSM, e.g. through Agordejo. This will take care of all +settings and save directories. + +Other modes of operations, mostly for testing, are: + +Run without session management and save in /tmp. + {METADATA["shortName"]} --save /tmp + +Run without audio and midi. Skips all JACK checks. Used to just look at the GUI, e.g. to make screenshots + {METADATA["shortName"]} --mute + +[see also] +The full documentation for {METADATA["name"]} is maintained as a multi-lingual html site to your systems doc-dir. +For example: + xdg-open file:///usr/share/doc/{METADATA["shortName"]}/index.html + +The documentation can also be found online https://www.laborejo.org/documentation/{METADATA["shortName"]} +""" + +with open ("../../documentation/manpageinclude.h2m", "w") as w: + w.write(manpage_template) + +command = f"help2man ../../{METADATA['shortName']} --no-info --include ../../documentation/manpageinclude.h2m > ../../documentation/{METADATA['shortName']}.1" +subprocess.run(command, capture_output=True, text=True, shell=True) + + +#print ("Built /documentation. You still need to run /documentation/build-documentation.sh manually") +print ("Built template part of documentation.") diff --git a/template/documentation/css/normalize.css b/template/documentation/css/normalize.css new file mode 100644 index 0000000..b26c100 --- /dev/null +++ b/template/documentation/css/normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v6.0.0 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * 1. Remove the bottom border in Chrome 57- and Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + margin: 0; +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/template/documentation/css/style.css b/template/documentation/css/style.css new file mode 100644 index 0000000..c92707a --- /dev/null +++ b/template/documentation/css/style.css @@ -0,0 +1,203 @@ +/* https://github.com/markdowncss/retro */ + +pre, +code { + font-family: Menlo, Monaco, "Courier New", monospace; + background-color: rgb(20,20,20); +} + +pre { + padding: .5rem; + line-height: 1.25; + overflow-x: scroll; +} + +@media print { + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; + box-shadow: none !important; + text-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } +} + +a, +a:visited { + color: #01ff70; +} + +a:hover, +a:focus, +a:active { + color: #2ecc40; +} + +.retro-no-decoration { + text-decoration: none; +} + +html { + font-size: 12px; +} + +@media screen and (min-width: 32rem) and (max-width: 48rem) { + html { + font-size: 15px; + } +} + +@media screen and (min-width: 48rem) { + html { + font-size: 16px; + } +} + +body { + line-height: 1.85; +} + +p, +.retro-p { + font-size: 1rem; + margin-bottom: 1.3rem; +} + +h1, +.retro-h1, +h2, +.retro-h2, +h3, +.retro-h3, +h4, +.retro-h4 { + margin: 1.414rem 0 .5rem; + font-weight: inherit; + line-height: 1.42; +} + +h1, +.retro-h1 { + margin-top: 0; + font-size: 2.698rem; +} + +h2, +.retro-h2 { + font-size: 1.827rem; +} + +h3, +.retro-h3 { + font-size: 1.0rem; +} + +h4, +.retro-h4 { + font-size: 0.9em; +} + +h5, +.retro-h5 { + font-size: 0.7rem; +} + +h6, +.retro-h6 { + font-size: .88rem; +} + +small, +.retro-small { + font-size: .707em; +} + +/* https://github.com/mrmrs/fluidity */ + +img, +canvas, +iframe, +video, +svg, +select, +textarea { + max-width: 100%; +} + +html, +body { + background-color: #222; + min-height: 100%; +} + +html { + font-size: 18px; +} + +body { + color: #fafafa; + font-family: "Courier New"; + line-height: 1.45; + margin: 6rem auto 1rem; + max-width: 48rem; + padding: .25rem; +} + +pre { + background-color: #333; +} + +blockquote { + border-left: 3px solid #01ff70; + padding-left: 1rem; +} diff --git a/template/documentation/english.adoc.template b/template/documentation/english.adoc.template new file mode 100644 index 0000000..1813ecd --- /dev/null +++ b/template/documentation/english.adoc.template @@ -0,0 +1,114 @@ +:Author: +:Version: +:iconfont-remote!: +:!webfonts: + + +//// +This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a +letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +A copy of the license has been provided in the file documentation/LICENSE. +//// + +//// +https://powerman.name/doc/asciidoc +https://asciidoctor.org/docs/user-manual/ +//// + +:sectnums: + +:toc: left +:toc-title: Table of Contents +:toclevels: 3 + += + +// Don't write in the empty line above line. It will be interpreted as author html tag +For program version + + + + + +== Installation and Start + + is exclusive for Linux. The best way to install is to use your package manager. +If it is not there, or only in an outdated version, please ask your Linux distribution to provide a recent version. + +If available in the package repository, please continue reading directly at "Start from Agordejo / New Session Manager". +If not, you can build yourself. + +.Build and Install +* Please check the supplied README.md for dependencies. +* You can download a release or clone the git version + ** Download the latest version from https://www.laborejo.org/downloads and extract it. + ** git clone https://git.laborejo.org/lss/.git +* Change into the new directory and use these commands: +* `./configure --prefix=/usr` + ** The default prefix is /usr/local +* `make` +* `sudo make install` + +.Start from Agordejo (New Session Manager, NSM) +* Run `agordejo` +* Press the `New` button, and enter a name for your piece of music. +* Use the launcher to add `` to the session. +* Add any compatible programs, e.g. synthesizers. + +Please read README.md for other ways of starting , which are impractical for actual use but can +be helpful for testing and development. + +== Help and Development +You can help in several ways: Testing and reporting errors, translating, marketing, support, programming and more. + +=== Testing and Reporting Errors +If you find a bug in the program (or it runs too slow) please contact us in a way that suits you best. +We are thankful for any help. + +.How to contact us +* Report bugs and issues: https://www.laborejo.org/bugs +* Website: https://www.laborejo.org +* E-Mail: info@laborejo.org +* If you see the opportunity and know that a developer will read it also forums, social media etc.. + +=== Programming +If you want to do some programming and don't know where to start please get in contact with us directly. +The short version is: clone the git, change the code, create a git patch or point me to your public git. + +=== Translations + is very easy to translate with the help of the Qt-Toolchain, without any need for programming. +The easiest way is to contact the developers and they will setup the new language. + +However, here are the complete instructions for doing a translation completely on your own and integrating it into the program. +The program is split in two parts. A shared "template" between the Laborejo Software Suite and the actual program. + +The process is the same for both parts, but needs to be done in different directories: +`template/qtgui` and plain `/qtgui`, relative to the root directory, where the executable is. + +Everytime you see "template/qtgui" below you can substitute that with just "qtgui" to translate the other part of . + +You can add a new language like this: + +* Open a terminal and navigate to template/qtgui/resources/translations +* Edit the file `config.pro` with a text editor + ** Append the name of your language in the last line, in the form `XY.ts`, where XY is the language code. + ** Make sure to leave a space between the individual languages entries. +* Run `sh update.sh` in the same directory + ** The program has now generated a new `.ts` file in the same directory. +* Start Qt Linguist with `linguist-qt5` (may be named differently) and open your newly generated file +* Select your "Target Language" and use the program to create a translation +* Send us the `.ts` file, such as by e-mail to info@laborejo.org + +You can also incorporate the translation into for testing purposes. This requires rudimentary Python knowledge. + +* Run the "Release" option in QtLinguists "File" menu. It creates a `.qm` file in the same directory as your `.ts` file. +* Edit `template/qtgui/resources/resources.qrc` and duplicate the line `translations/de.qm` but change it to your new .qm file. +* run `sh buildresources.sh` +* Edit `engine/config.py`: add your language to the line that begins with "supportedLanguages" like this: `{"German": "de.qm", "Esperanto: "eo.qm"}` + ** To find out your language string (German, Esperanto etc.) open the `python3` interpreter in a terminal and run the following command: + ** `from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())` + +To test the new translation you can either run the program normally, if your system is set to that language. Alternatively start via the terminal: + +* `LANGUAGE=de_DE.UTF-8 ./ -V --save /dev/null` diff --git a/template/documentation/german.adoc.template b/template/documentation/german.adoc.template new file mode 100644 index 0000000..5f935b7 --- /dev/null +++ b/template/documentation/german.adoc.template @@ -0,0 +1,121 @@ +:Author: +:Version: +:iconfont-remote!: +:!webfonts: + + +//// +This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a +letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +A copy of the license has been provided in the file documentation/LICENSE. +//// + +//// +https://powerman.name/doc/asciidoc +https://asciidoctor.org/docs/user-manual/ +//// + +:sectnums: + +:toc: left +:toc-title: Inhaltsverzeichnis +:toclevels: 3 + += + +// Don't write in the empty line above line. It will be interpreted as author html tag +Für Programmversion + + + + +== Installation und Start + + ist exklusiv für Linux. Am besten installieren Sie über deinen Paketmanager. +Falls es dort nicht vorhanden ist, oder nur in einer veralteten Version, bitten sie ihre +Linuxdistribution bereitzustellen. + +Falls in den Paketquellen vorhanden bitte direkt bei " in Agordejo (New Session Manager)" weiterlesen. +Falls nicht kann man auch selbst "bauen". + +.Abhängigkeiten* +* Eine Liste der Abhängigkeit befindet sich in der README.md +* Kompilieren und Installieren geht entweder mit einem Releasedownload oder mit der Git-Version: + ** Laden Sie von https://www.laborejo.org/downloads die aktuelle Version herunter und entpacken Sie sie. + ** `git clone https://git.laborejo.org/lss/.git` +* Wechseln Sie in das neue Verzeichnis und benutzen diese Befehle: +*`./configure --prefix=/usr` + ** Das Standardprefix is /usr/local +* `make` +* `sudo make install` + +. im Agordejo (New Session Manager, NSM) starten +* Starten Sie `agordejo` +* Erstellen Sie eine neue Session, geben Sie einen Namen für das Musikstück ein. +* Benutzen Sie den Launcher um `` hinzuzufügen. +* Fügen Sie beliebige kompatible Programme hinzu, z.B. Synthesizer. + +In der Datei README.md befinden sich weitere Möglichkeiten zu starten. Diese sind zum +Musikmachen nicht praktikabel, aber nützlich für Tests und Entwicklung. + +== Helfen und Entwicklung +Sie können auf viele Arten und Weisen helfen: Testen und Fehler melden, übersetzen, marketing, +anderen Nutzern helfen und schließlich programmieren. + +=== Testen und Programmfehler +Falls Sie einen Fehler im Programm entdecken (oder es zu langsam läuft) melden Sie diese bitte auf +eine Art und Weise, die ihnen am besten passt. + +.Kontaktmöglichkeiten +* Bugs und Probleme melden: https://www.laborejo.org/bugs +* Webseite: https://www.laborejo.org +* E-Mail: info@laborejo.org +* Wenn Sie die Gelegenheiten sehen und wissen, dass ein Entwickler es lesen wird sind auch Foren, Socialmedia etc. möglich. + +== Entwicklung +Falls Sie an der Entwicklung interessiert sind, melden Sie sich am besten direkt bei uns (s.o.) +Kurzversion: clone git, programmieren, einen git-patch erstellen oder uns eine git URL zukommen lassen. + +=== Übersetzungen + ist mit Hilfe der Qt-Toolchain sehr einfach zu übersetzen, ohne, dass man dafür Programmieren muss. +Die einfachste Variante ist es einfach die Entwickler anzusprechen und sie werden die neue Sprache einrichten. + +Hier ist dennoch die komplette Anleitung, um eine Übersetzung komplett alleine anzufertigen und in +das Programm einzubinden. Das Programm ist in zwei Teile aufgeteilt: Ein gemeinsames "Template" +(für alle Laborejo Software Suit Programme) sowie das eigentliche Programm. + +Der Übersetzungsprozess ist der gleiche für beide Teile, man muss ihn jedoch in unterschiedlichen +Verzeichnissen durchführen: +`template/qtgui` und nur `/qtgui`, relativ zum Stammverzeichnis, wo sich die ausführbare Datei + befindet. + +Jedes "template/qtgui" hier kann durch nur "qtgui" ersetzt werden um den zweiten Teil zu übersetzen. + +So fügt man eine neue Sprache hinzu: + +* Öffnen Sie ein Terminal und navigieren zu template/qtgui/resources/translations +* Bearbeiten Sie die Datei `config.pro` in einem Texteditor. + ** Hängem Sie in der letzten Zeile den Namen der neuen Sprache an, in der Form `XY.ts`, wobei XY der Sprachcode ist. + ** Achten Sie bitte darauf ein Leerzeichen zwischen den einzelnen Sprachen zu lassen +* Führen Sie `sh update.sh` im selben Verzeichnis aus. + ** Das Programm hat nun eine neue `.ts`-Datei im Verzeichnis erstellt. +* Starten Sie Qt Linguist mit `linguist-qt5` (kann evtl. anders heißen) und öffnen von dort die neu generierte Datei. +* Wählen Sie die "Target Language", also Zielsprache, aus und benutzen das Programm um eine Übersetzung anzufertigen. +* Senden Sie uns bitte die .ts Datei, z.B. per E-Mail an info@laborejo.org (s.u bei Bugs und Programmfehler für mehr Kontaktmöglichkeiten) + +Die Übersetzung können Sie auch selbst, zum Testen, einbinden. Dafür sind rudimentäre +Python Kentnisse nötig. + +* Im Qt Linguist "Datei" Menü ist eine "Release" Option. Das erstellt eine `.qm` Datei im gleichen Verzeichnis wie die `.ts` Datei.* Bearbeiten Sie `template/qtgui/resources/resources.qrc` und kopieren die Zeile `translations/de.qm` . Dabei das Länderkürzel zum Neuen ändern. +* Führen Sie `sh buildresources.sh` aus +* Bearbeiten Sie `engine/config.py`: Die neue Sprache hinzufügen. z.B. `{"German":"de.qm", "Esperanto:"eo.qm"}` + ** Um den Sprachstring herauszufinden öffnen Sie den `python3`-Interpreter im Terminal und führen aus: + ** `from PyQt5 import QtCore;QtCore.QLocale().languageToString(QtCore.QLocale().language())` + + +Um die neue Übersetzung zu testen starten Sie das Programm, falls ihr System bereits auf diese +Sprache eingstellt ist. +Ansonsten starten Sie mit diesem Befehl, Sprachcode ändern, vom Terminal aus: + +* `LANGUAGE=de_DE.UTF-8 ./ -V` diff --git a/template/documentation/index.adoc.template b/template/documentation/index.adoc.template new file mode 100644 index 0000000..724b532 --- /dev/null +++ b/template/documentation/index.adoc.template @@ -0,0 +1,37 @@ +:Author: +:Version: +:iconfont-remote!: +:!webfonts: + +//// +This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a +letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +A copy of the license has been provided in the file documentation/LICENSE. +//// + +//// +https://powerman.name/doc/asciidoc +https://asciidoctor.org/docs/user-manual/ +//// + +:nofooter: + + +== Multi-Language Documentation +image::logo.png["logo", 320, 180] + +For program version + +This site is part of the https://www.laborejo.org[Laborejo Software Suite] + + +Please choose a language + +* link:english.html[English] +* link:german.html[Deutsch (German)] + + +Further Links +* https://laborejo.org/bugs/[Bug and Issues, and other Feedback] +* Write to info@laborejo.org for any comment or question. diff --git a/template/documentation/readme.template b/template/documentation/readme.template new file mode 100644 index 0000000..c058d1f --- /dev/null +++ b/template/documentation/readme.template @@ -0,0 +1,123 @@ + +[//]: # (Generated . Changes belong into template/documentation/readme.template) + +# +Program version + + +![Screenshot](https://git.laborejo.org/lss//raw/branch/master/documentation/screenshot.png "Screenshot") + + + + +This README is just a short introduction. Consult the manual (see below) for more information. + +# Contact and Information + +* Website https://www.laborejo.org +* Bugs and Issues: https://www.laborejo.org/bugs +* Git Repositories for all programs: https://git.laborejo.org +* Documentation and Manual https://www.laborejo.org/documentation/ + +# Installation and Starting + +## Download + +### Release Version +If the latest release is not available through your package manger you can build it yourself: +Download the latest code release on https://www.laborejo.org/downloads and extract it. + +### Git Version +It is possible to clone a git repository. + +`git clone https://git.laborejo.org/lss/.git` + +## Dependencies +* Glibc +* Python 3.6 (maybe earlier) +* PyQt5 for Python 3 +* DejaVu Sans Sarif TTF (Font) (recommended, but not technically necessary) + + +#### Build Dependencies +* Bash +* GCC (development is done on 8.2, but most likely you can use a much earlier version) + +### Environment: +* Jack Audio Connection Kit must be running +* Agordejo / New Session Manager ("NSM") + +## Build + ./configure --prefix=/usr/local + make + sudo make install + + +## Starting + +There are multiple ways to run which should give you the flexibility to configure your +system as you want. + +We make no distinction if you installed yourself or through the distributions package-manager. + +The differences are: With or without Agordejo, with or without sound, installed or from the source dir. + +### Installed , running through Agordejo (New Session Manager) (recommended) +Starting through Agordejo after you installed system-wide +is the recommended and only supported way. Start agordejo and load or create a new +session. Then use the program launcher to add ``. +It should appear with an icon in the list and open its GUI. + +### Installed without Agordejo +If you start directly it will present you with a dialog to choose your session directory. + +You can also start from a terminal (or create a starter script). + +` --save DIRECTORY` + +Uses the given directory to save. The dir will be created or loaded from if already present. Use +the applications file menu to save (Ctrl+s). + +You can use this to load and save the files from an existing NSM session. If you create a new +directory you can copy it manually to an NSM session directory, but that requires renaming the +directory to append the unique ID provided by NSM. + +Sending SIGUSR1 to the program in this mode will trigger a save. + +Closing through your window manager in this mode will actually quit the application without a +prompt to save changes. + +## From source directory +You can run after extracting the release archive or cloning from git, without installation. + +### Calfbox +"Calfbox" is the name of our internal realtime midi/audio python module. + +* It is bundled with the application for a normal install. +* Or you could run `./configure` and `make calfbox` without subsequent install, which creates a `sitepackages` directory in the source dir. You can then run `./` directly from the source. +* A third option is `./ --mute` which runs without sound at all and does not need calfbox. + +### From source directory with NSM +The developer uses this way to develop and use the software, so it will always be as stable as the +compiled version. But it is a bit less performant than building and installing it. + +After extracting the release archive create a symlink from `` into your PATH. e.g. /usr/bin +or ~/bin, if that exists on your system. + +If you compiled without installing you can also symlink to `./.bin` + +### From source dir without NSM +Use `./ --save` (see above). If you compiled without installing you can also run `./.bin` + +### No NSM, no Make, No Sound +Combining the above options you can start the program directly after unpacking or cloning from git: + +`./ --save /tmp --mute` + +Or even shorter: + +`./ -s /tmp -m` + +This is the minimal run mode which is only useful for testing and development. But if you only want +to look at the GUI and are not in the mood to install anything -including dependencies-, go ahead. + diff --git a/template/engine/__init__.py b/template/engine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/template/engine/api.py b/template/engine/api.py new file mode 100644 index 0000000..e280d75 --- /dev/null +++ b/template/engine/api.py @@ -0,0 +1,498 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ) + +This application 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 . +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Python Standard Library +import os.path +from datetime import timedelta + +#Third Party Modules +from calfbox import cbox + +#Our own template modules +from .session import Session +from .duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, D256, D512, D1024, jackBBTicksToDuration +from ..helper import nothing + +class Callbacks(object): + """GUI methods register themselves here. + These methods get called by us, the engine. + + None of these methods produce any return value. + + The lists may be unordered. + + We need the lists for audio feedbacks in parallel to GUI updates. + Or whatever parallel representations we run.""" + + def __init__(self): + self.debugChanged = [] #only for testing and debug. + self.setPlaybackTicks = [] + self.playbackStatusChanged = [] + self.bbtStatusChanged = [] + self.barBeatTempo = [] + self.clock = [] + self.historyChanged = [] + self.historySequenceStarted = [] + self.historySequenceStopped = [] + self.message = [] + + #Live Midi Recording + self.recordingModeChanged = [] + + #Sequencer + self.numberOfTracksChanged = [] + self.metronomeChanged = [] + + #Sf2 Sampler + self.soundfontChanged = [] + self.channelChanged = [] + self.channelActivity = [] + self.ignoreProgramChangesChanged = [] + + #Not callbacks + self._rememberPlaybackStatus = None #Once set it will be True or False + self._rememberBBT = None #Will be a dict + self._rememberBarBeatTempo = None #Specialized case of BBT + + + def _dataChanged(self): + """Only called from within the callbacks or template api. + This is about data the user cares about. In other words this is the indicator if you need + to save again. + + Insert, delete edit are real data changes. Cursor movement or playback ticks are not.""" + session.nsmClient.announceSaveStatus(False) + self._historyChanged() + + + def _historyChanged(self): + """processQueue + Only called from within the callbacks. + + sends two lists of strings. + the first is the undoHistory, the last added item is [-1]. We can show that to a user to + indicate what the next undo will do. + + the second is redoHistory, same as undo: [-1] shows the next redo action.""" + undoHistory, redoHistory = session.history.asList() + for func in self.historyChanged: + func(undoHistory, redoHistory) + + def _historySequenceStarted(self): + """sends a signal when a sequence of high level api functions will be executed next. + Also valid for their undo sequences. + A GUI has the chance to disable immediate drawing, e.g. qt Graphics Scene could stop + scene updates and allow all callbacks to come in first. + historySequenceStopped will be sent when the sequence is over and a GUI could reactivate + drawing to have the buffered changes take effect. + This signal is automatically sent by the history sequence context""" + for func in self.historySequenceStarted: + func() + + def _historySequenceStopped(self): + """see _historySequenceStarted""" + for func in self.historySequenceStopped: + func() + + def _message(self, title, text): + """Send a message of any kind to get displayed. + Enables an api function to display a message via the GUI. + Does _not_ support translations, therefore ued for errors mostly""" + for func in self.message: + func(title, text) + + def _debugChanged(self): + for func in self.debugChanged: + func() + self._dataChanged() #includes _historyChanged + + + def _setPlaybackTicks(self): + """This gets called very very often (~60 times per second). + Any connected function needs to watch closely + for performance issues""" + ppqn = cbox.Transport.status().pos_ppqn + status = playbackStatus() + for func in self.setPlaybackTicks: + func(ppqn, status) + + def _playbackStatusChanged(self): + """Returns a bool if the playback is running. + Under rare circumstances it may send the same status in a row, which means + you actually need to check the result and not only toggle as a response. + + This callback cannot be called manually. Instead it will be called automatically to make + it possible to react to external jack transport changes. + + This is deprecated. Append to _checkPlaybackStatusAndSendSignal which is checked by the + event loop. + """ + raise NotImplementedError("this function was deprecated. use _checkPlaybackStatusAndSendSignal") + + pass #only keep for the docstring and to keep the pattern. + + def _checkPlaybackStatusAndSendSignal(self): + """Added to the event loop. + We don'T have a jack callback to inform us of this so we drive our own polling system + which in turn triggers our own callback, when needed.""" + status = playbackStatus() + if not self._rememberPlaybackStatus == status: + self._rememberPlaybackStatus = status + for func in self.playbackStatusChanged: + func(status) + + def _checkBBTAndSendSignal(self): + """Added to the event loop. + We don'T have a jack callback to inform us of this so we drive our own polling system + which in turn triggers our own callback, when needed. + + We are interested in: + bar + beat #first index is 1 + tick + bar_start_tick + beats_per_bar [4.0] + beat_type [4.0] + ticks_per_beat [960.0] #JACK ticks, not cbox. + beats_per_minute [120.0] + + int bar is the current bar. + int beat current beat-within-bar + int tick current tick-within-beat + double bar_start_tick number of ticks that have elapsed between frame 0 and the first beat of the current measure. + """ + data = cbox.JackIO.jack_transport_position() #this includes a lot of everchanging data. If no jack-master client set /bar and the others they will simply not be in the list + t = (data.beats_per_bar, data.ticks_per_beat) + if not self._rememberBBT == t: #new situation, but not just frame position update + self._rememberBBT = t + export = {} + if data.beats_per_bar: + offset = (data.beat-1) * data.ticks_per_beat + data.tick #if timing is good this is the same as data.tick because beat is 1. + offset = jackBBTicksToDuration(data.beat_type, offset, data.ticks_per_beat) + export["nominator"] = data.beats_per_bar + export["denominator"] = jackBBTicksToDuration(data.beat_type, data.ticks_per_beat, data.ticks_per_beat) #the middle one is the changing one we are interested in + export["measureInTicks"] = export["nominator"] * export["denominator"] + export["offsetToMeasureBeginning"] = offset + #export["tickposition"] = cbox.Transport.status().pos_ppqn #this is a different position than our current one because it takes a few cycles and ticks to calculate + export["tickposition"] = cbox.Transport.samples_to_ppqn(data.frame) + for func in self.bbtStatusChanged: + func(export) + + #Send bar beats tempo, for displays + #TODO: broken + """ + bbtExport = {} + if data.beat and not self._rememberBarBeatTempo == data.beat: + + bbtExport["timesig"] = f"{int(data.beats_per_bar)}/{int(data.beat_type)}" #for displays + bbtExport["beat"] = data.beat #index from 1 + bbtExport["tempo"] = int(data.beats_per_minute) + bbtExport["bar"] = int(data.bar) + self._rememberBarBeatTempo = data.beat #this should be enough inertia to not fire every 100ms + for func in self.barBeatTempo: + func(bbtExport) + elif not data.beat: + for func in self.barBeatTempo: + func(bbtExport) + self._rememberBarBeatTempo = data.beat + """ + + clock = str(timedelta(seconds=data.frame / data.frame_rate)) + for func in self.clock: + func(clock) + + + #Live Midi Recording + def _recordingModeChanged(self): + if session.recordingEnabled: + session.nsmClient.changeLabel("Recording") + else: + session.nsmClient.changeLabel("") + for func in self.recordingModeChanged: + func(session.recordingEnabled) + + #Sequencer + def _numberOfTracksChanged(self): + """New track, delete track, reorder + Sent the current track order as list of ids, combined with their structure. + This is also used when tracks get created or deleted, also on initial load. + """ + session.data.updateJackMetadataSorting() + lst = [track.export() for track in session.data.tracks] + for func in self.numberOfTracksChanged: + func(lst) + self._dataChanged() #includes _historyChanged + + + def _metronomeChanged(self): + """returns a dictionary with meta data such as the mute-state and the track name""" + exportDict = session.data.metronome.export() + for func in self.metronomeChanged: + func(exportDict) + + #Sf2 Sampler + def _soundfontChanged(self): + """User loads a new soundfont or on load. Resets everything.""" + exportDict = session.data.export() + session.data.updateAllChannelJackMetadaPrettyname() + session.nsmClient.changeLabel(exportDict["name"]) + if exportDict: + for func in self.soundfontChanged: + func(exportDict) + + def _channelChanged(self, channel): + """A single channel changed its parameters. The soundfont stays the same.""" + exportDict = session.data.exportChannel(channel) + session.data.updateChannelAudioJackMetadaPrettyname(channel) + session.data.updateChannelMidiInJackMetadaPrettyname(channel) + for func in self.channelChanged: + func(channel, exportDict) + + def _ignoreProgramChangesChanged(self): + state = session.data.midiInput.scene.status().layers[0].status().ignore_program_changes + for func in self.ignoreProgramChangesChanged: + func(state) + + def _channelActivity(self, channel): + """send all note on to the GUI""" + for func in self.channelActivity: + func(channel) + +def startEngine(nsmClient): + """ + This function gets called after initializing the GUI, calfbox + and loading saved data from a file. + + It gets called by client applications before their own startEngine. + + Stopping the engine is done via pythons atexit in the session. + """ + logger.info("Starting template api engine") + assert session + assert callbacks + + session.nsmClient = nsmClient + session.eventLoop.fastConnect(callbacks._checkPlaybackStatusAndSendSignal) + session.eventLoop.fastConnect(callbacks._setPlaybackTicks) + session.eventLoop.fastConnect(cbox.get_new_events) #global cbox.get_new_events does not eat dynamic midi port events. + session.eventLoop.slowConnect(callbacks._checkBBTAndSendSignal) + #session.eventLoop.slowConnect(lambda: print(cbox.Transport.status().tempo)) + #asession.eventLoop.slowConnect(lambda: print(cbox.Transport.status())) + + cbox.Document.get_song().update_playback() + + callbacks._recordingModeChanged() #recording mode is in the save file. + callbacks._historyChanged() #send initial undo status to the GUI, which will probably deactivate its undo/redo menu because it is empty. + logger.info("Template api engine started") + + +def _deprecated_updatePlayback(): + """The only place in the program to update the cbox playback besides startEngine. + We only need to update it after a user action, which always goes through the api. + Even if triggered through a midi in or other command. + Hence there is no need to update playback in the session or directly from the GUI.""" + #if session.nsmClient.cachedSaveStatus = False: #dirty #TODO: wait for cbox optimisations. The right place to cache and check if an update is necessary is in cbox, not here. + cbox.Document.get_song().update_playback() + +def save(): + """Saves the file in place. This is mostly here for psychological reasons. Users like to hit + Ctrl+S from muscle memory. + But it can also be used if we run with fake NSM. In any case, it does not accept paths""" + session.nsmClient.serverSendSaveToSelf() + +def undo(): + """No callbacks need to be called. Undo is done via a complementary + function, already defined, which has all the callbacks in it.""" + session.history.undo() + callbacks._dataChanged() #this is in most of the functions already but high-level scripts that use the history.sequence context do not trigger this and get no redo without. + +def redo(): + """revert undo if nothing new has happened so far. + see undo""" + session.history.redo() + callbacks._dataChanged() #this is in most of the functions already but high-level scripts that use the history.sequence context do not trigger this and get no redo without. + + +def getUndoLists(): + #undoHistory, redoHistory = session.history.asList() + return session.history.asList() + + +#Calfbox Sequencer Controls +def playbackStatus()->bool: + #status = "[Running]" if cbox.Transport.status().playing else "[Stopped]" #it is not that simple. + cboxStatus = cbox.Transport.status().playing + if cboxStatus == 1: + #status = "[Running]" + return True + elif cboxStatus == 0: + #status = "[Stopped]" + return False + elif cboxStatus == 2: + #status = "[Stopping]" + return False + elif cboxStatus is None: + #status = "[Uninitialized]" + return False + elif cboxStatus == "": + #running with cbox dummy module + return False + else: + raise ValueError("Unknown playback status: {}".format(cboxStatus)) + +def playPause(): + """There are no internal callback to start and stop playback. + The api, or the session, do not call that. + Playback can be started externally via jack transport. + We use the jack transport callbacks instead and trigger our own callbacks directly from them, + in the callback class above""" + if playbackStatus(): + cbox.Transport.stop() + else: + cbox.Transport.play() + #It takes a few ms for playbackStatus to catch up. If you call it here again it will not be updated. + +def getPlaybackTicks()->int: + return cbox.Transport.status().pos_ppqn + +def seek(value): + if value < 0: + value = 0 + cbox.Transport.seek_ppqn(value) + +def toStart(): + seek(0) + +def playFrom(ticks): + seek(ticks) + if not playbackStatus(): + cbox.Transport.play() + +def playFromStart(): + toStart() + if not playbackStatus(): + cbox.Transport.play() + +def toggleRecordingMode(): + session.recordingEnabled = not session.recordingEnabled + callbacks._recordingModeChanged() + +# Sequencer Metronome +def setMetronome(data, label): + session.data.metronome.generate(data, label) + callbacks._metronomeChanged() + +def enableMetronome(value): + session.data.metronome.setEnabled(value) #has side effects + callbacks._metronomeChanged() + +def isMetronomeEnabled(): + return session.data.metronome.enabled + +def toggleMetronome(): + enableMetronome(not session.data.metronome.enabled) #handles callback etc. + + +#Sf2 Sampler +def loadSoundfont(filePath): + """User callable function. Load from saved state is done directly in the session with callbacks + in startEngine + The filePath MUST be in our session dir. + """ + filePathInOurSession = os.path.commonprefix([filePath, session.nsmClient.ourPath]) == session.nsmClient.ourPath + if not filePathInOurSession: + raise Exception("api loadSoundfont tried to load .sf2 from outside session dir. Forbidden") + + success, errormessage = session.data.loadSoundfont(filePath) + + if success: + callbacks._soundfontChanged() + session.history.clear() + callbacks._historyChanged() + callbacks._dataChanged() + else: + callbacks._message("Load Soundfont Error", errormessage) + + return success + +def setIgnoreProgramAndBankChanges(state): + state = bool(state) + #there is no session wrapper function. we use cbox directly. Save file and callbacks will fetch the current value on its own + session.data.midiInput.scene.status().layers[0].set_ignore_program_changes(state) + assert session.data.midiInput.scene.status().layers[0].status().ignore_program_changes == state + callbacks._ignoreProgramChangesChanged() + callbacks._dataChanged() + + +def setPatch(channel, bank, program): + if not 1 <= channel <= 16: + raise ValueError (f"Channel must be a number between 1 and 16. Yours: {channel}") + #Bank is split into CC0 and CC32. That makes it a 14bit value (2**14 or 128 * 128) = 16384 + if not 0 <= bank <= 16384: + raise ValueError (f"Program must be a number between 0 and 16384. Yours: {bank}") + if not 0 <= program <= 127: + raise ValueError (f"Program must be a number between 0 and 127. Yours: {program}") + + session.data.setPatch(channel, bank, program) + callbacks._channelChanged(channel) + callbacks._dataChanged() + +#Debug, Test and Template Functions +class TestValues(object): + value = 0 + +def history_test_change(): + """ + We simulate a function that gets its value from context. + Here it is random, but it may be the cursor position in a real program.""" + from random import randint + value = 0 + while value == 0: + value = randint(-10,10) + + if value > 0: + session.history.setterWithUndo(TestValues, "value", TestValues.value + value, "Increase Value", callback=callbacks._debugChanged) #callback includes dataChanged which inlucdes historyChanged + else: + session.history.setterWithUndo(TestValues, "value", TestValues.value + value, "Decrease Value", callback=callbacks._debugChanged) #callback includes dataChanged which inlucdes historyChanged + +def history_test_undoSequence(): + with session.history.sequence("Change Value Multiple Times"): + history_test_change() + history_test_change() + history_test_change() + history_test_change() + callbacks._historyChanged() + + +#Module Level Data +callbacks = Callbacks() #This needs to be defined before startEngine() so a GUI can register its callbacks. The UI will then startEngine and wait to reveice the initial round of callbacks +session = Session() + +session.history.apiCallback_historySequenceStarted = callbacks._historySequenceStarted +session.history.apiCallback_historySequenceStopped = callbacks._historySequenceStopped + +#Import complete. Now the parent module, like a gui, will call startEngine() and provide an event loop. + + diff --git a/template/engine/data.py b/template/engine/data.py new file mode 100644 index 0000000..168bd8d --- /dev/null +++ b/template/engine/data.py @@ -0,0 +1,60 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +class Data(object): + """Base class to all Data. Data is the class that gets added to the session. Consider this an + interface. It can be used as an object or parent class to get a working program, even if it + doesn't do much. + """ + + def __init__(self, parentSession): + self.parentSession = parentSession + super().__init__() + + def updateJackMetadataSorting(self): + pass + + #Save / Load / Export + def serialize(self)->dict: + return { + } + + @classmethod + def instanceFromSerializedData(cls, parentSession, serializedData): + """The entry function to create a score from saved data. It is called by the session. + + This functions triggers a tree of other createInstanceFromSerializedData which finally + return the score, which gets saved in the session. + + The serializedData is already converted to primitive python types from json, + but nothing more. Here we create the actual objects.""" + self = cls(parentSession=parentSession, + ) + + return self + + def export(self)->dict: + return { + } diff --git a/template/engine/duration.py b/template/engine/duration.py new file mode 100644 index 0000000..66fa758 --- /dev/null +++ b/template/engine/duration.py @@ -0,0 +1,129 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +""" +This file handles various durations and their conversions. +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +from engine.config import METADATA + +#Suggested import in other files: +#from template.engine.duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128 +#import template.engine.duration as duration + + +MAXIMUM_TICK_DURATION = 2**31-1 + + +D4 = METADATA["quarterNoteInTicks"] +D8 = int(D4 / 2) +D16 = int(D8 / 2) +D32 = int(D16 / 2) +D64 = int(D32 / 2) +D128 = int(D64 / 2) +D256 = int(D128 / 2) +D512 = int(D256 / 2) +D1024 = int(D512 / 2) # set this to a number with many factors, like 210. According to http://homes.sice.indiana.edu/donbyrd/CMNExtremes.htm this is the real world limit. + +if not int(D1024) == D1024: + logger.error(f"Warning: Lowest duration D0124 has decimal places: {D1024} but should be a plain integer. Your D4 value: {D4} has not enough 2^n factors. ") + +D2 = D4 *2 +D1 = D2 *2 +DB = D1 * 2 #Breve +DL = DB * 2 #Longa +DM = DL * 2 #Maxima + + +if not int(D128) == D128: + raise ValueError(f"Durations must be integers (or .0). Yours: {D128}") + + +D_BASE_DURATIONS = (D1024, D512, D256, D64, D32, D16, D8, D4, D2, D1, DB, DL, DM) + +#Duration Keywords +D_DEFAULT = 0 +D_STACCATO = 1 +D_TENUTO = 2 +D_TIE = 3 + +def _baseDurationToTraditionalNumber(baseDuration): + """4 = Quarter, 8 = Eighth, 0 = Brevis, -1= Longa + Created in the loop below on startup""" + if baseDuration == D4: + return 4 + elif baseDuration == D8: + return 8 + elif baseDuration == D16: + return 16 + elif baseDuration == D2: + return 2 + + elif baseDuration == D1: + return 1 + elif baseDuration == DB: + return 0 + elif baseDuration == DL: + return -1 + elif baseDuration == DM: + return -2 + + elif baseDuration == D32: + return 32 + elif baseDuration == D64: + return 64 + elif baseDuration == D128: + return 128 + elif baseDuration == D256: + return 256 + elif baseDuration == D512: + return 512 + elif baseDuration == D1024: + return 1024 + + elif baseDuration == 0: #for KeySignatures and chords on init etc. + return 0 + else: + raise ValueError("baseDuration does not match any traditioanl note duration value") + +baseDurations = (0, D1024, D512, D256, D128, D64, D32, D16, D8, D4, D2, D1, DB, DL, DM) #from constants.py + +baseDurationToTraditionalNumber = {} +for baseDuration in baseDurations: + baseDurationToTraditionalNumber[baseDuration] = _baseDurationToTraditionalNumber(baseDuration) + + +traditionalNumberToBaseDuration = {} +for baseDuration in baseDurations: + traditionalNumberToBaseDuration[_baseDurationToTraditionalNumber(baseDuration)] = baseDuration + +def jackBBTicksToDuration(beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks): + if None in (beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks): + return None + else: + beatTypeDuration = traditionalNumberToBaseDuration[beatTypeAsTraditionalNumber] + #print (beatTypeAsTraditionalNumber, beatTypeDuration, jackTicks, jackBeatTicks) + factor = beatTypeDuration / jackBeatTicks + return jackTicks * factor diff --git a/template/engine/history.py b/template/engine/history.py new file mode 100644 index 0000000..0af3650 --- /dev/null +++ b/template/engine/history.py @@ -0,0 +1,149 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +from contextlib import contextmanager + +class History(object): + """Keep track of changes in the session. From file load to quit. + + All functions that need undo and redo follow the same idiom: + + A fuzzy and vague user function starts the process. It may depend + on the context, like the cursor pitch or the current key signature. + + This function itself, e.g. Note.sharpen, saves its exact old values + and instances and returns a lambda: undo function with these exact + values already inserted. + + The undo function must be able to undo itself. + Redo is undo for undo, it uses the same system. Each undo function + needs to register an undo itself. + + In other words: The user function, like "sharpen", is only called + once. After that an internal setter function, without context and + state, handles undo and redo by using exact values on the exact + instances (for example note instances). + + We still set and reset the cursor position in undo, but that doesn't + mean getting the right context or track-state back. + It is only to enable the user to edit from the same place again + and also to find the right track to instruct the callbacks to update. + + + The history does not know, and subsequently does not differentiate, + between apply-to-selection or single-item commands. Each history + function does a set of operations. The history itself is agnostic + about them. + """ + def __init__(self): + self.commandStack = [] # elements are (description, listOfFunctions). listOfFunctions is a real python list. + self.redoStack = [] #same as self.commandStack + self.doNotRegisterRightNow = False #prevents self.undo from going into an infinite loop. For example if you undo api.delete it will register insertItem again (which has a register itself with delete as undo function, which will...). There is only one history in the session so this attribute is shared by all undo functions. + self.duringRedo = False + + self.apiCallback_historySequenceStarted = None + self.apiCallback_historySequenceStopped = None + + @contextmanager + def sequence(self, descriptionString): + """Convenience function. All registrations from now on will be undone at once. + call stopSequence when done. + This is meant for high level scripts and not for user-interaction. + + This context can also be used to force a custom name for the undo description, even if it is + only one step. + """ + + self.apiCallback_historySequenceStarted() + + + def sequenceRegister(registeredUndoFunction, descriptionString, listOfFunctions): + """Modifies a list in place""" + listOfFunctions.append(registeredUndoFunction) + + listOfFunctions = [] + originalRegister = self.register + self.register = lambda registeredUndoFunction, descriptionString, l=listOfFunctions: sequenceRegister(registeredUndoFunction, descriptionString, l) + + yield + + self._register(descriptionString, listOfFunctions) + self.register = originalRegister + self.apiCallback_historySequenceStopped() + + + def register(self, registeredUndoFunction, descriptionString): + """Register a single undo function but use the same syntax as many functions""" + self._register(descriptionString, [registeredUndoFunction,]) + + def _register(self, descriptionString, listOfFunctions): + assert type(listOfFunctions) is list, (type(listOfFunctions), listOfFunctions) + if self.doNotRegisterRightNow: + self.redoStack.append((descriptionString, listOfFunctions)) + else: + self.commandStack.append((descriptionString, listOfFunctions)) + if not self.duringRedo: + self.redoStack = [] #no redo after new commands. + + def clear(self): + """User-scripts which do not implement their own undo must + call this to not disturb the history. Better no history + than a wrong history.""" + self.commandStack = [] + self.redoStack = [] + + def undo(self): + if self.commandStack: + self.doNotRegisterRightNow = True + + descriptionString, listOfFunctions = self.commandStack.pop() + + with self.sequence(descriptionString): + for registeredUndoFunction in reversed(listOfFunctions): + registeredUndoFunction() + + self.doNotRegisterRightNow = False + #print ("pop", len(self.commandStack)) + + def redo(self): + if self.redoStack: + self.duringRedo = True + + descriptionString, listOfFunctions = self.redoStack.pop() + with self.sequence(descriptionString): + for registeredUndoFunction in reversed(listOfFunctions): + registeredUndoFunction() + + self.duringRedo = False + + def asList(self): + return [descr for descr, listOfFunctions in self.commandStack], [descr for descr, listOfFunctions in self.redoStack] + + + def setterWithUndo(self, object, attribute, value, descriptionString, callback): + oldValue = getattr(object, attribute) + setattr(object, attribute, value) + undoFunction = lambda oldValue=oldValue: self.setterWithUndo(object, attribute, oldValue, descriptionString, callback) + self.register(undoFunction, descriptionString = descriptionString) + callback() diff --git a/template/engine/input_midi.py b/template/engine/input_midi.py new file mode 100644 index 0000000..c979977 --- /dev/null +++ b/template/engine/input_midi.py @@ -0,0 +1,247 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Python Standard Library + +#Third Party Modules +from calfbox import cbox + +#Template Modules +from . import pitch + + +class MidiInput(object): + """MidiIn provides a port from calfboxes realtime midi in parsing to python data and functions. + Please note that it is not directly related to a track and is not responsible for actual midi + recording. This is the base class if you want to have midi control your program, like controlling + a GUI, step entry like in Laborejo or visualizers with multiple tracks. + + You can therefore choose how you interpret the incoming data. It is possible to only have one + midi in port for the whole program and decide where to route the incoming events based on e.g. + GUI status (which track is selected). + + Or you could mirror your internal tracks one by one with a midi input. + + MidiIn is also the base class for instruments. However, these are subclasses. + + It is recommended that you add a MidiIn object to your Python structures. + Don't subclass it because you might want to have MidiOut and audios as well, this + will produce a messy multiple inheritance situation. + + One usecase is Laborejo: A single global step midi input that calls api functions. + In this case MidiInput is an alternative to the GUI. It does not belong in the engine but at + lowest in the api. + """ + + def __init__(self, session, portName): + """The processor adds itself to the event loop. + You need to call prepareDelete before removing a midi output again""" + + self.session = session + self.portName = portName + self.scene = cbox.Document.get_engine().new_scene() + self.scene.clear() + self.scene.set_enable_default_song_input(False) + self.cboxMidiPortUid = cbox.JackIO.create_midi_input(portName) + self.realtimeMidiThroughLayer = self.scene.add_new_midi_layer(self.cboxMidiPortUid) #Create a midi layer for our input port. That layer support manipulation like transpose or channel routing. + cbox.JackIO.set_appsink_for_midi_input(self.cboxMidiPortUid, True) #This enables forwarding to Python for our midiProcessors get_new_events(self.parentInput.cboxMidiPortUid) + cbox.JackIO.route_midi_input(self.cboxMidiPortUid, self.scene.uuid) #Route midi input to the scene. Without this we have no sound, but the python processor will still work. + + self._currentMidiThruOutputMidiPortUid = None #RT midi thru + self.setMidiThruChannel(1) + + self.midiProcessor = MidiProcessor(parentInput = self) + self.session.eventLoop.fastConnect(self.midiProcessor.processEvents) + self.readyToDelete = False + + def prepareDelete(self): + self.session.eventLoop.fastDisconnect(self.midiProcessor.processEvents) + self.readyToDelete = True + + def setMidiThru(self, cboxMidiOutUuid): + """ + This is the output portion of the program. Everything in init is only for input. + + Instruct the RT part to echo midi in directly to the connect output ports + so we hear the current track with the tracks instrument. + + e.g. if you have a single global midi input but multiple track based outputs you can use + this to route the input RT to a track output while still getting the python data to process. + + If you use such a configuration for data entry the user will hear his inputs echoed by + the actual target instrument and not e.g. a generic piano or sine wave sound. + """ + if not self._currentMidiThruOutputMidiPortUid == cboxMidiOutUuid: #most of the time this stays the same e.g. cursor left/right. we only care about up and down + self._currentMidiThruOutputMidiPortUid = cboxMidiOutUuid + self.realtimeMidiThroughLayer.set_external_output(cboxMidiOutUuid) + + def setMidiThruChannel(self, channel): + if channel < 1 or channel > 16: + raise ValueError("Channels are from 1 to 16 (inclusive). You sent " + str(channel)) + self.realtimeMidiThroughLayer.set_out_channel(channel) + + +class MidiProcessor(object): + """ + There are two principal modes: Step Entry and Live Recording. + + Add your function to callbacks3 for notes and CC or callbacks2 for program change and Channel Pressure. + + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.NOTE_ON)] = lambda: print(channel, note, velocity)""" + + + SIMPLE_EVENT = "/io/midi/simple_event" + TRANSPORT_PLAY = "/io/midi/event_time_ppqn" + TRANSPORT_STOPPED = "/io/midi/event_time_samples" + M_NOTE_ON = 0x90 + M_NOTE_OFF = 0x80 + M_ACTIVE_SENSE = 0xFE #254 + + M_AFTERTOUCH = 0xA0 #160 + M_CONTROL_CHANGE = 0xB0 #176 + M_PROGRAMCHANGE = 0xC0 #192 + M_CHANNELPRESSURE = 0xD0 #208 + M_PITCHBEND = 0xE0 #224 + + CC_BANKCHANGE_COARSE = 32 + CC_BANKCHANGE_FINE = 0 + + def __init__(self, parentInput): + self.parentInput = parentInput + self.callbacks3 = {} #keys are tuples + self.callbacks2 = {} #keys are tuples + self.active = True + self.ccState = {} + self.lastTimestamp = None #None for Transport not rolling, Value while rolling + #for cc in range(128): #0-127 inclusive + # self.ccState[cc] = None #start with undefined. + + + def processEvents(self): + """events come in packages. + If you press just a key you'll get a list with length 2: timestamp, midi-message + For each event received in the same timeslot you'll get two events more. + A chord of four keys, played staccato simultaniously will yield 16 events. + 2 for each key (=8), times two for note on and note off each.(=16) + + We don't want the whole function too many indentation levels deep so we take a few shortcuts. + + This function gets called very often. So every optimisation is good. + """ + events = cbox.JackIO.get_new_events(self.parentInput.cboxMidiPortUid) + if not self.active: + return + if not events: + return + for message, stuff, dataList in events: + l = len(dataList) + + #Check recording mode. These message only get sent in front of another event, like a note. + #That means this is not a playback state detector because it only works after receiving a midi event + if message == MidiProcessor.TRANSPORT_PLAY: + #if self.lastTimestamp is None: + # print ("switching to live recording") + self.lastTimestamp = dataList[0] + #print (self.lastTimestamp) + + elif message == MidiProcessor.TRANSPORT_STOPPED : + #if not self.lastTimestamp is None: + # print ("switching to step recording") + self.lastTimestamp = None + + #Process Data + elif l == 3: #notes and CC + if message == MidiProcessor.SIMPLE_EVENT: + m_type, m_note, m_velocity = dataList #of course these can be different than a note, but this is easier to read then "byte 1", "byte 2" + m_channel = m_type & 0x0F + m_mode = m_type & 0xF0 #0x90 note on, 0x80 note off and so on. + key = (message, m_mode) + if m_mode == 0xB0: + self.ccState[m_note] = m_velocity #note is CCn , like 7=Volume, velocity is value. + + if key in self.callbacks3: + self.callbacks3[key](self.lastTimestamp, m_channel, m_note, m_velocity) + + elif l == 2: #program change, aftertouch, + if message == MidiProcessor.SIMPLE_EVENT: + m_type, m_value = dataList + m_channel = m_type & 0x0F + m_mode = m_type & 0xF0 + key = (message, m_mode) + if key in self.callbacks2: + self.callbacks2[key](self.lastTimestamp, m_channel, m_value) + + #Active Sense (Keep Alive Signal) + #TOOD: this can be commented out for performance reasons if we don't need to print out messages for debug reasons. + #TODO: Sadly there is no compile time. Use the compiledPrefix instead? + #elif l == 1 and message == MidiProcessor.SIMPLE_EVENT and dataList == [MidiProcessor.M_ACTIVE_SENSE]: + # pass + + #else: + # print (message, stuff, dataList) + + + + #Convenience Functions + def register_NoteOn(self, functionWithFourParametersTimestampChannelNoteVelocity): + """A printer would be: + lambda timestamp, channel, note, velocity: print(timestamp, channel, note, velocity) + """ + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = functionWithFourParametersTimestampChannelNoteVelocity + + + def register_NoteOff(self, functionWithFourParametersTimestampChannelNoteVelocity): + """A printer would be: + lambda timestamp, channel, note, velocity: print(timestamp, channel, note, velocity) + """ + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_OFF)] = functionWithFourParametersTimestampChannelNoteVelocity + + def register_PolyphonicAftertouch(self, functionWithFourParametersTimestampChannelTypeValue): + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_AFTERTOUCH)] = functionWithFourParametersTimestampChannelTypeValue + + def register_CC(self, functionWithFourParametersTimestampChannelTypeValue): + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_CONTROL_CHANGE)] = functionWithFourParametersTimestampChannelTypeValue + + def register_ProgramChange(self, functionWithThreeParametersTimestampChannelValue): + self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_PROGRAMCHANGE)] = functionWithThreeParametersTimestampChannelValue + + def register_ChannelPressure(self, functionWithThreeParametersTimestampChannelValue): + self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_CHANNELPRESSURE)] = functionWithThreeParametersTimestampChannelValue + + def register_PitchBend(self, functionWithFourParametersTimestampChannelTypeValue): + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_PITCHBEND)] = functionWithFourParametersTimestampChannelTypeValue + + #Hier weiter machen. After Touch, aber wie darstellen? Das ist ja per Taste. Wie CC Type? Oder unten im Velocity View? + + def notePrinter(self, state:bool): + if state: + def _printer(timestamp, channel, note, velocity): + print(f"[{timestamp}] Chan: {channel} Note: {pitch.midi_notenames_english[note]}: Vel: {velocity}") + self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = _printer + else: + try: + del self.callbacks[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] + except KeyError: + pass + diff --git a/template/engine/ly2cbox.py b/template/engine/ly2cbox.py new file mode 100644 index 0000000..7e3ecbe --- /dev/null +++ b/template/engine/ly2cbox.py @@ -0,0 +1,86 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +""" +A helper module to quickly generate some cbox patterns and notes with lilypond and simple functions. +This is mostly meant for development and testing. +""" + +#Standard Library Modules +import re + +#Third Party Modules +from calfbox import cbox + +#Template Modules +from . import pitch +from .duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128 + + +lyToMidi = {} #filled for all pitches on startup, below +for ly, _pitch in pitch.ly2pitch.items(): + octOffset = (pitch.octave(_pitch) +1) * 12 #twelve tones per midi octave + lyToMidi[ly] = octOffset + pitch.halfToneDistanceFromC(_pitch) + + +lyToTicks = { + "16" : D16, + "8" : D8, + "4" : D4, + "2" : D2, + "1" : D1, + } + +def ly(lilypondString:str): + """Take string of simple lilypond notes, return midi pitches as generator of (pitch, ticks)""" + lastDur = "4" + for lyNote in lilypondString.split(" "): + try: + lyPitch, lyDur = re.split(r'(\d+)', lyNote)[0:2] + lastDur = lyDur + except ValueError: + lyPitch = re.split(r'(\d+)', lyNote)[0] + lyDur = lastDur + + yield (lyToMidi[lyPitch], lyToTicks[lyDur]) + + +def pattern(lilypondString:str, channel, velocity:int=100): + #Return (pbytes, durationInTicks) + """ + Return a cbox pattern + a python byte data type with midi data for cbox. + + Channel is 1-16.""" + #cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity)) + channel -= 1 + patternBlob = bytes() + tickCounter = 0 + for midiPitch, durationInTicks in ly(lilypondString): + endTick = tickCounter + durationInTicks - 1 #-1 ticks to create a small logical gap. This is nothing compared to our tick value dimensions, but it is enough for the midi protocol to treat two notes as separate ones. Imporant to say that this does NOT affect the next note on. This will be mathematically correct anyway. + patternBlob += cbox.Pattern.serialize_event(tickCounter, 0x90+channel, midiPitch, velocity) # note on + patternBlob += cbox.Pattern.serialize_event(endTick, 0x80+channel, midiPitch, velocity) # note off + tickCounter = tickCounter + durationInTicks #no -1 for the next note + + pattern = cbox.Document.get_song().pattern_from_blob(patternBlob, tickCounter) + #return patternBlob, startTick + return pattern + diff --git a/template/engine/metronome.py b/template/engine/metronome.py new file mode 100644 index 0000000..f795e1b --- /dev/null +++ b/template/engine/metronome.py @@ -0,0 +1,129 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Standard Library Modules +from typing import Tuple +import os.path + +#Third Party Modules +from calfbox import cbox + +#Our template modules +from ..start import PATHS +from . import sequencer +from ..helper import cache_unlimited, flatList +from .duration import D1024 #to add a note off spacer. + + +class Metronome(object): + """ + A metronome uses calfbox to generate a click track. + All calfbox handling and midi generation are internally. + + The metronome has multiple components, each can be switched on and off on creation: + -stereo audio out (stereo for cbox reasons) + -midi out + -midi in + + You can configure the midi notes representing a stressed and normal tick. + There is no half-stressed signal. + + Stressing can be switched off. + + The metronome is a real midi track, not generated on the fly. Therefore it needs to be set + with new data when music changes, which happens quite often. In general you want you metronome + to be as long as your song. + + You can choose to loop the last measure, which makes simple "give me 4/4 at 120" possible. + + """ + + def __init__(self, parentData, normalMidiNote=77, stressedMidiNote=76, midiChannel=9): + self.parentData = parentData + #self.sequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome") + self.sfzInstrumentSequencerInterface = sequencer.SfzInstrumentSequencerInterface(parentTrack=self, name="metronome", absoluteSfzPath=os.path.join(PATHS["templateShare"], "metronome", "metronome.sfz")) + #testing: self.sfzInstrumentSequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome"); self.sfzInstrumentSequencerInterface.setEnabled(True) needs activating below!!! + self._soundStresses = True #Change through soundStresses function + self._normalTickMidiNote = normalMidiNote #no changing after instance got created + self._stressedTickMidiNote = stressedMidiNote #no changing after instance got created + self._midiChannel = midiChannel #GM drums #1-16 #no changing after instance got created + self._cachedData = None #once we have a track it gets saved here so the midi output can be regenerated in place. + self.label = "" #E.g. current Track Name, but can be anything. + self.setEnabled(False) #TODO: save load + + def soundStresses(self, value:bool): + self._soundStresses = value + self.generate(self._cachedData) + + def generate(self, data, label:str): + """Data is ordered: Iterable of (positionInTicks, isMetrical, treeOfMetricalInstructions) + as tuple. Does not check if we truly need an update + + Label typically is the track name""" + assert not data is None + self.label = label + self._cachedData = data + result = [] + for position, isMetrical, treeOfMetricalInstructions in data: + isMetrical = False if not self._soundStresses else True + blob, length = self.instructionToCboxMeasure(isMetrical, treeOfMetricalInstructions) + result.append((blob, position, length)) + self.sfzInstrumentSequencerInterface.setTrack(result) + #we skip over instructions which have no proto-measure, metrical or not. This basically creates a zone without a metronome. + #This might be difficult to use when thinking of Laborejo alone but in combination with a real time audio recording in another program this becomes very useful. + #TODO: repeat last instruction as loop + + @cache_unlimited + def instructionToCboxMeasure(self, isMetrical, metricalInstruction:tuple)->Tuple[bytes,int]: + """Convert a metrical instruction to a metronome measure""" + measureBlob = bytes() + workingTicks = 0 + ranOnce = False + for duration in flatList(metricalInstruction): + if ranOnce or not isMetrical: #normal tick. Always normal if this measure has no stressed positions, in other words it is not metrical + measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 77, 127) #full velocity + measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 77, 127) #note off is the shortest value possible in this program. + else: #Measure "one" + measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 76, 127) #different pitch + measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 76, 127) + ranOnce = True + workingTicks += duration + return (measureBlob, workingTicks) + + @property + def enabled(self)->bool: + return self.sfzInstrumentSequencerInterface.enabled + + def setEnabled(self, value:bool): + self.sfzInstrumentSequencerInterface.enable(value) + + def export(self)->dict: + return { + # "sequencerInterface" : self.sequencerInterface.export(), + "enabled" : self.enabled, + "label" : self.label, + } + + diff --git a/template/engine/midi.py b/template/engine/midi.py new file mode 100644 index 0000000..87447bf --- /dev/null +++ b/template/engine/midi.py @@ -0,0 +1,314 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +""" +This file handles various pitches and their conversions. +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Third Party Modules +#Template Modules +#Our modules + +#https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2 + +ccList = """Bank Select +Modulation Wheel or Lever +Breath Controller +Undefined +Foot Controller +Portamento Time +Data Entry MSB +Channel Volume +Balance +Undefined +Pan +Expression Controller +Effect Control 1 +Effect Control 2 +Undefined +Undefined +General Purpose Controller 1 +General Purpose Controller 2 +General Purpose Controller 3 +General Purpose Controller 4 +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +LSB for Control 0 (Bank Select) +LSB for Control 1 (Modulation Wheel or Lever) +LSB for Control 2 (Breath Controller) +LSB for Control 3 (Undefined) +LSB for Control 4 (Foot Controller) +LSB for Control 5 (Portamento Time) +LSB for Control 6 (Data Entry) +LSB for Control 7 (Channel Volume, formerly Main Volume) +LSB for Control 8 (Balance) +LSB for Control 9 (Undefined) +LSB for Control 10 (Pan) +LSB for Control 11 (Expression Controller) +LSB for Control 12 (Effect control 1) +LSB for Control 13 (Effect control 2) +LSB for Control 14 (Undefined) +LSB for Control 15 (Undefined) +LSB for Control 16 (General Purpose Controller 1) +LSB for Control 17 (General Purpose Controller 2) +LSB for Control 18 (General Purpose Controller 3) +LSB for Control 19 (General Purpose Controller 4) +LSB for Control 20 (Undefined) +LSB for Control 21 (Undefined) +LSB for Control 22 (Undefined) +LSB for Control 23 (Undefined) +LSB for Control 24 (Undefined) +LSB for Control 25 (Undefined) +LSB for Control 26 (Undefined) +LSB for Control 27 (Undefined) +LSB for Control 28 (Undefined) +LSB for Control 29 (Undefined) +LSB for Control 30 (Undefined) +LSB for Control 31 (Undefined) +Damper Pedal on/off (Sustain) ≤63 off, ≥64 on +Portamento On/Off ≤63 off, ≥64 on +Sostenuto On/Off ≤63 off, ≥64 on +Soft Pedal On/Off ≤63 off, ≥64 on +Legato Footswitch ≤63 Normal, ≥64 Legato +Hold 2 ≤63 off, ≥64 on --- +Sound Controller 1 (default: Sound Variation) 0-127 LSB +Sound Controller 2 (default: Timbre/Harmonic Intens.) 0-127 LSB +Sound Controller 3 (default: Release Time) 0-127 LSB +Sound Controller 4 (default: Attack Time) 0-127 LSB +Sound Controller 5 (default: Brightness) 0-127 LSB +Sound Controller 6 (default: Decay Time - see MMA RP-021) 0-127 LSB +Sound Controller 7 (default: Vibrato Rate - see MMA RP-021) 0-127 LSB +Sound Controller 8 (default: Vibrato Depth - see MMA RP-021) 0-127 LSB +Sound Controller 9 (default: Vibrato Delay - see MMA RP-021) 0-127 LSB +Sound Controller 10 (default undefined - see MMA RP-021) 0-127 LSB +General Purpose Controller 5 +General Purpose Controller 6 +General Purpose Controller 7 +General Purpose Controller 8 +Portamento Control +Undefined +Undefined +Undefined +High Resolution Velocity Prefix 0-127 LSB +Undefined +Undefined +Effects 1 Depth (default: Reverb Send Level +Effects 2 Depth (formerly Tremolo Depth) +Effects 3 Depth (default: Chorus Send Level +Effects 4 Depth (formerly Celeste [Detune] Depth) +Effects 5 Depth (formerly Phaser Depth) +Data Increment (Data Entry +1) +Data Decrement (Data Entry -1) +Non-Registered Parameter Number (NRPN) - LSB +Non-Registered Parameter Number (NRPN) - MSB +Registered Parameter Number (RPN) - LSB +Registered Parameter Number (RPN) - MSB +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +Undefined +[Channel Mode Message] All Sound Off +[Channel Mode Message] Reset All Controllers +[Channel Mode Message] Local Control On/Off +[Channel Mode Message] All Notes Off +[Channel Mode Message] Omni Mode Off (+ all notes off) +[Channel Mode Message] Omni Mode On (+ all notes off) +[Channel Mode Message] Mono Mode On (+ poly off, + all notes off) +[Channel Mode Message] Poly Mode On (+ mono off, +all notes off)""".split("\n") + +enumeratedCCList = [str(i).zfill(3) + ": " + s for i,s in enumerate(ccList)] + + +programList = [ + "Acoustic Grand Piano", + "Bright Acoustic Piano", + "Electric Grand Piano", + "Honky-tonk Piano", + "Electric Piano 1", + "Electric Piano 2", + "Harpsichord", + "Clavinet", + + "Celesta", + "Glockenspiel", + "Music Box", + "Vibraphone", + "Marimba", + "Xylophone", + "Tubular Bells", + "Dulcimer", + + "Drawbar Organ", + "Percussive Organ", + "Rock Organ", + "Church Organ", + "Reed Organ", + "Accordion", + "Harmonica", + "Tango Accordion", + + "Acoustic Guitar (nylon)", + "Acoustic Guitar (steel)", + "Electric Guitar (jazz)", + "Electric Guitar (clean)", + "Electric Guitar (muted)", + "Overdriven Guitar", + "Distortion Guitar", + "Guitar harmonics", + + "Acoustic Bass", + "Electric Bass (finger)", + "Electric Bass (pick)", + "Fretless Bass", + "Slap Bass 1", + "Slap Bass 2", + "Synth Bass 1", + "Synth Bass 2", + + "Violin", + "Viola", + "Cello", + "Contrabass", + "Tremolo Strings", + "Pizzicato Strings", + "Orchestral Harp", + "Timpani", + + "String Ensemble 1", + "String Ensemble 2", + "Synth Strings 1", + "Synth Strings 2", + "Choir Aahs", + "Voice Oohs", + "Synth Voice", + "Orchestra Hit", + + "Trumpet", + "Trombone", + "Tuba", + "Muted Trumpet", + "French Horn", + "Brass Section", + "Synth Brass 1", + "Synth Brass 2", + + "Soprano Sax", + "Alto Sax", + "Tenor Sax", + "Baritone Sax", + "Oboe", + "English Horn", + "Bassoon", + "Clarinet", + + "Piccolo", + "Flute", + "Recorder", + "Pan Flute", + "Blown Bottle", + "Shakuhachi", + "Whistle", + "Ocarina", + + "Lead 1 (square)", + "Lead 2 (sawtooth)", + "Lead 3 (calliope)", + "Lead 4 (chiff)", + "Lead 5 (charang)", + "Lead 6 (voice)", + "Lead 7 (fifths)", + "Lead 8 (bass + lead)", + + "Pad 1 (new age)", + "Pad 2 (warm)", + "Pad 3 (polysynth)", + "Pad 4 (choir)", + "Pad 5 (bowed)", + "Pad 6 (metallic)", + "Pad 7 (halo)", + "Pad 8 (sweep)", + + "FX 1 (rain)", + "FX 2 (soundtrack)", + "FX 3 (crystal)", + "FX 4 (atmosphere)", + "FX 5 (brightness)", + "FX 6 (goblins)", + "FX 7 (echoes)", + "FX 8 (sci-fi)", + + "Sitar", + "Banjo", + "Shamisen", + "Koto", + "Kalimba", + "Bag pipe", + "Fiddle", + "Shanai", + + "Tinkle Bell", + "Agogo", + "Steel Drums", + "Woodblock", + "Taiko Drum", + "Melodic Tom", + "Synth Drum", + + "Reverse Cymbal", + "Guitar Fret Noise", + "Breath Noise", + "Seashore", + "Bird Tweet", + "Telephone Ring", + "Helicopter", + "Applause", + "Gunshot", +] + +enumeratedProgramList = [str(i).zfill(3) + ": " + s for i,s in enumerate(programList)] diff --git a/template/engine/pitch.py b/template/engine/pitch.py new file mode 100644 index 0000000..defef08 --- /dev/null +++ b/template/engine/pitch.py @@ -0,0 +1,1174 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +""" +This file handles various pitches and their conversions. +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +#Standard Library +from collections import defaultdict + +#Third Party Modules +#Template Modules +#Our modules + + +#Constants +OCTAVE = 350 +STEP = 50 +MAX = 3140 +MIN = 0 + +#Without a keysignature +def plain(pitch): + """ Extract the note from a note-number, without any octave but with the tailing zero. + This means we double-use the lowest octave as abstract version.""" + #Dividing through the octave, 350, results in the number of the octave and the note as remainder. + return divmod(pitch, 350)[1] + +def octave(pitch): + """Return the octave of given note. Lowest 0 is X,,,""" + return divmod(pitch, 350)[0] + +def toOctave(pitch, octave): + """Take a plain note and give the octave variant. Starts with 0""" + return pitch + octave * 350 + +def mirror(pitch, axis): + """Calculate the distance between the pitch and the axis-pitch and + set the new pitch twice as far, which creates a mirror effect: + Half the distancen is object->mirror and then mirror->object on the + other side again.""" + #1420 = c', 1520 = e', 1620 = g' + + #1420 to 1520 is a third so the mirror would be a fifth. + #1420 + 2 * (1520 - 1420) + #1420 + 2 * 100 + #1420 + 200 = 1620 + return pitch + 2 * (axis - pitch) + +def diatonicIndex(pitch): + """Return an int between 0 and 6, resembling the diatonic position + of the given pitch without octave, accidentals. 0 is c""" + return divmod(plain(toWhite[pitch]), 50)[0] + +def absoluteDiatonicIndex(pitch): + """Like diatonicIndex but works from pitch 20 which gets index 0 + middle c is 28 + tuning a is 33 + these two are indeed 5 steps apart + (not traditional interval steps, real step counting from 1)""" + return divmod(toWhite[pitch], 50)[0] + +def distanceInDiatonicSteps(first, second): + """root is a pitch like 1720. Pitch as well + Returns not a signed int. If the first is lower than the second + you get a negative return value.""" + return absoluteDiatonicIndex(first) - absoluteDiatonicIndex(second) + +def diatonicIndexToPitch(index, octave): + """supports indices from - to +. """ + while index < 0: + index += 7 #plus one octave. index -1 becomes b + octave -= 1 + while index > 6: + index -= 7 #minus one octave. index 7 becomes c again. + octave += 1 + return toOctave(index * 50 + 20, octave) #0 is cesces, 20 is c + +def upStepsFromRoot(root, pitch): + """Like diatonicIndex but assumes a different root than C. So it is: + 'stepcount upward in white keys from root to pitch'. + It is always assumed it should go up. + Also it will never report anything over an octave since it + uses diatonicIndex()""" + assert root <= pitch + if root == pitch: + return 0 + else: + r = diatonicIndex(root) + p = diatonicIndex(pitch) + if plain(root) > plain(pitch): #we have an octave break g' to d'' + p += 7 + return p - r + +def fromMidi(midipitch, keysig): + """Convert a midi pitch to internal pitch. + Nearest to pillar of fifth""" + if (midipitch, keysig) in cache_fromMidi: + return cache_fromMidi[(midipitch, keysig)] + else: + midioctave, pitch = divmod(midipitch, 12) + table = [ + 20, #c + 60, #des + 70, #d + 110, #ees + 120, #e + 170, #f + 180, #fis + 220, #g + 260, #aas + 270, #a + 310, #bes + 320, #b / h + ] + + #Sample note Gis/Aes in D Major should become Gis, which is the same as Fis in C Maj. + midiRoot = toMidi[keysig.root] - 12 #in D Major thats 2 (halftone steps away from C) + simpleConverted = toOctave(table[pitch], midioctave -1) #in D Maj it is still Aes + fromC = halfToneDistanceFromC(simpleConverted) + soundingDistanceToKeysigRoot = fromC - midiRoot #8 half tone steps - 2 from root = 6 (Tritonus) + #We now need to know how the 6 steps / tritonus look like in the current keysignature/root enviroment. This is told by the table above. + pitchInCMaj = table[soundingDistanceToKeysigRoot] #fis + + #Interval between keysig root and c. + plainInterval, intervalOctaveOffset = interval(20, keysig.root) + newInterval = (plainInterval, midioctave -1 ) #tuplets are immutable + + #Transpose it by this interval + pitchInOriginalKey = intervalUp(pitchInCMaj, newInterval, midiIn = True) + + #Back to the correct Octave. This is what we wanted. + #bugfix/workaround. 320 b/h in keysigs <= Bes Major is "ces", but somehow one octave to low. Compensate here. + if pillarOfFifth.index(keysig.root) <= 13 and pitch == 11: + returnValue = pitchInOriginalKey + 350 + else: + returnValue = pitchInOriginalKey + + cache_fromMidi[(midipitch, keysig)] = returnValue + return returnValue + + + +def halfToneDistanceFromC(pitch): + """Return the half-tone step distance from C. The "sounding" interval""" + return { + #00 : 10, # ceses,,, -> bes + #10 : 11, # ces,,, -> b + + 00 : -2, # ceses,,, -> bes + 10 : -1, # ces,,, -> b + 20 : 0, # c,,, + 30 : 1, # cis,,, + 40 : 2, # cisis,,, -> d ... + 50 : 0, # deses,,, + 60 : 1, # des,,, + 70 : 2, # d,,, + 80 : 3, # dis,,, + 90 : 4, # disis,,, + 100 : 2, # eeses,,, + 110 : 3, # ees,,, + 120 : 4, # e,,, + 130 : 5, # eis,,, + 140 : 6, # eisis,,, + 150 : 3, # feses,,, + 160 : 4, # fes,,, + 170 : 5, # f,,, + 180 : 6, # fis,,, + 190 : 7, # fisis,,, + 200 : 5, # geses,,, + 210 : 6, # ges,,, + 220 : 7, # g,,, + 230 : 8, # gis,,, + 240 : 9, # gisis,,, + 250 : 7, # aeses,,, + 260 : 8, # aes,,, + 270 : 9, # a,,, + 280 : 10, # ais,,, + 290 : 11, # aisis,,, + 300 : 9, # beses,,, + 310 : 10, # bes,,, + 320 : 11, # b,,, + 330 : 12, # bis,,, + 340 : 13, # bisis,,, + #330 : 0, # bis,,, + #340 : 1, # bisis,,, + }[plain(pitch)] + + +def sharpen(pitch): + """Sharpen the pitch until double crossed""" + sharper = pitch + 10 + if toWhite[sharper] == toWhite[pitch]: #still the same base note? + return sharper + else: + return pitch #too sharp, do nothing. + +def flatten(pitch): + """Flatten the pitch until double flat""" + flatter = pitch - 10 + if toWhite[flatter] == toWhite[pitch]: #still the same base note? + return flatter + else: + return pitch #too flat, do nothing. + +def interval(pitch1, pitch2): + """Return the distance between two pitches as steps in the pillar of fifths. + Intervals are tuplets with two members x = 1,0 #fifth in the same octave + x[0] = interval. Steps in the pillar of fifths. + x[1] = octave in between. Octave is always >= 0 because an interval has no direction. + Just a base note and the other note, which is higher per definition""" + if pitch1 > pitch2: #bring the notes in right order. We want to calculate from higher to lower + return (pillarOfFifth.index(plain(pitch1)) - pillarOfFifth.index(plain(pitch2)), octave(pitch1 - pitch2)) + else: + return (pillarOfFifth.index(plain(pitch2)) - pillarOfFifth.index(plain(pitch1)), octave(pitch2 - pitch1)) + +def intervalUp(pitch, interval, midiIn = False): + """Return a pitch which is _interval_ higher than the given pitch""" + octv = octave(pitch) + indexNumber = pillarOfFifth.index(plain(pitch)) + targetPitch = pillarOfFifth[indexNumber + interval[0]] + octv*350 #return to the old octave + if not midiIn and targetPitch < pitch: #the new note is lower than where we started. This is wrong. +1 octave!. Reason: it was the break between two octaves. + targetPitch += 350 + targetPitch += interval[1]*350 + return targetPitch + +def intervalDown(pitch, interval): + """Return a pitch which is _interval_ lower than the given pitch + intervalUp(20, (-12,1)) #c,,, to deses,,""" + + octv = octave(pitch) + indexNumber = pillarOfFifth.index(plain(pitch)) + targetPitch = pillarOfFifth[indexNumber - interval[0]] + octv*350 #return to the old octave. + if targetPitch > pitch: #the new note is higher than where we started. This is wrong. -1 octave!. Reason: it was the break between two octaves. + targetPitch -= 350 + targetPitch -= interval[1]*350 + return targetPitch + +def intervalAutomatic(originalPitch, rootPitch, targetPitch): + """Return the original pitch transposed by the interval + between rootPitch and targetPitch""" + iv = interval(rootPitch, targetPitch) + if rootPitch >= targetPitch: + return intervalDown(originalPitch, iv) + elif rootPitch < targetPitch: + return intervalUp(originalPitch, iv) + + +#With a Key Signature +def toScale(pitch, keysig): + """Return a pitch which is the in-scale variant of the given one. + Needs a Key Signature as second parameter""" + if (pitch, keysig) in cache_toScale: + return cache_toScale[(pitch, keysig)] + else: + workingcopy = list(keysig.keysigList) + workingcopy.sort() # sort first. + mod = workingcopy[tonalDistanceFromC[pitch]] # tonalDistanceFromC has the same syntax as the keysig step/list position. mod becomes the (step, value) tuplet + value = toWhite[pitch] + mod[1] + cache_toScale[(pitch, keysig)] = value + return value + +def diffToKey(pitch, keysig): + """Return if a note is natural, sharp, flat etc. + Same syntax as Key Signature: + -20 double flat, 0 natural, +20 d-sharp.""" + return pitch - toScale(pitch, keysig) + +#Ordered pitches in fifths. +#To calculate real and correct intervals you need the pillar of fifth with 35 steps for each of the 31 realistic notenames (and 4 unrealistic ones)""" +pillarOfFifth = [ + #pillarOfFifth.index(260) -> 11 + 150, #feses 0 + 0, #ceses 1 + 200, #geses 2 + 50, #deses 3 + 250, #aeses 4 + 100, #eeses 5 + 300, #beses 6 + 160, #fes 7 + 10, #ces 8 + 210, #ges 9 + 60, #des 10 + 260, #aes 11 + 110, #ees 12 + 310, #bes 13 + 170, #f 14 + 20, #c 15 + 220, #g 16 + 70, #d 17 + 270, #a 18 + 120, #e 19 + 320, #b 20 + 180, #fis 21 + 30, #cis 22 + 230, #gis 23 + 80, #dis 24 + 280, #ais 25 + 130, #eis 26 + 330, #bis 27 + 190, #fisis 28 + 40, #cisis 29 + 240, #gisis 30 + 90, #disis 31 + 290, #aisis 32 + 140, #eisis 33 + 340, #bisis 34 + ] + + +def midiPitchLimiter(pitch, transpose): + if pitch + transpose < 0: + logger.warning(f"Tranpose lead to a note below midi value 0: {pitch}. Limiting to 0. Please fix manually") + return 0 + elif pitch + transpose > 127: + logger.warning(f"Tranpose lead to a note above midi value 127: {pitch}. Limiting to 127. Please fix manually") + return 127 + else: + return pitch + transpose + +def midiChannelLimiter(value): + """makes sure that a midi channel is in range 0-15""" + if value > 15: + logger.warning("Midi Channel bigger 15 detected: {}. Limiting to 15. Please fix manually".format(value)) + return 15 + elif value <0: + logger.warning("Midi Channel smaller 0 detected: {}. Limiting to 0. Please fix manually".format(value)) + return 0 + else: + return value + + +#The table to convert internal pitches into lilypond and back. +#0 - A tone humans cannot hear anymore. +#1420 - "Middle" c' +#2130 - Soprano-Singers high C +#3150 - Goes beyond the range of a modern piano +#+inf.0 - A rest + +#<10 is reserved for microtones, in the future. +#+10 One accidental up jumps over to the next note after cisis +#+50 One diatonic step, preserve accidentals +#+350 One Octave +ly2pitch = { + "ceses,,," : 00, + "ces,,," : 10, + "c,,," : 20, + "cis,,," : 30, + "cisis,,," : 40, + "deses,,," : 50, + "des,,," : 60, + "d,,," : 70, + "dis,,," : 80, + "disis,,," : 90, + "eeses,,," : 100, + "ees,,," : 110, + "e,,," : 120, + "eis,,," : 130, + "eisis,,," : 140, + "feses,,," : 150, + "fes,,," : 160, + "f,,," : 170, + "fis,,," : 180, + "fisis,,," : 190, + "geses,,," : 200, + "ges,,," : 210, + "g,,," : 220, + "gis,,," : 230, + "gisis,,," : 240, + "aeses,,," : 250, + "aes,,," : 260, + "a,,," : 270, + "ais,,," : 280, + "aisis,,," : 290, + "beses,,," : 300, + "bes,,," : 310, + "b,,," : 320, + "bis,,," : 330, + "bisis,,," : 340, + "ceses,," : 350, + "ces,," : 360, + "c,," : 370, + "cis,," : 380, + "cisis,," : 390, + "deses,," : 400, + "des,," : 410, + "d,," : 420, + "dis,," : 430, + "disis,," : 440, + "eeses,," : 450, + "ees,," : 460, + "e,," : 470, + "eis,," : 480, + "eisis,," : 490, + "feses,," : 500, + "fes,," : 510, + "f,," : 520, + "fis,," : 530, + "fisis,," : 540, + "geses,," : 550, + "ges,," : 560, + "g,," : 570, + "gis,," : 580, + "gisis,," : 590, + "aeses,," : 600, + "aes,," : 610, + "a,," : 620, + "ais,," : 630, + "aisis,," : 640, + "beses,," : 650, + "bes,," : 660, + "b,," : 670, + "bis,," : 680, + "bisis,," : 690, + "ceses," : 700, + "ces," : 710, + "c," : 720, + "cis," : 730, + "cisis," : 740, + "deses," : 750, + "des," : 760, + "d," : 770, + "dis," : 780, + "disis," : 790, + "eeses," : 800, + "ees," : 810, + "e," : 820, + "eis," : 830, + "eisis," : 840, + "feses," : 850, + "fes," : 860, + "f," : 870, + "fis," : 880, + "fisis," : 890, + "geses," : 900, + "ges," : 910, + "g," : 920, + "gis," : 930, + "gisis," : 940, + "aeses," : 950, + "aes," : 960, + "a," : 970, + "ais," : 980, + "aisis," : 990, + "beses," : 1000, + "bes," : 1010, + "b," : 1020, + "bis," : 1030, + "bisis," : 1040, + "ceses" : 1050, + "ces" : 1060, + "c" : 1070, + "cis" : 1080, + "cisis" : 1090, + "deses" : 1100, + "des" : 1110, + "d" : 1120, + "dis" : 1130, + "disis" : 1140, + "eeses" : 1150, + "ees" : 1160, + "e" : 1170, + "eis" : 1180, + "eisis" : 1190, + "feses" : 1200, + "fes" : 1210, + "f" : 1220, + "fis" : 1230, + "fisis" : 1240, + "geses" : 1250, + "ges" : 1260, + "g" : 1270, + "gis" : 1280, + "gisis" : 1290, + "aeses" : 1300, + "aes" : 1310, + "a" : 1320, + "ais" : 1330, + "aisis" : 1340, + "beses" : 1350, + "bes" : 1360, + "b" : 1370, + "bis" : 1380, + "bisis" : 1390, + "ceses'" : 1400, + "ces'" : 1410, + "c'" : 1420, + "cis'" : 1430, + "cisis'" : 1440, + "deses'" : 1450, + "des'" : 1460, + "d'" : 1470, + "dis'" : 1480, + "disis'" : 1490, + "eeses'" : 1500, + "ees'" : 1510, + "e'" : 1520, + "eis'" : 1530, + "eisis'" : 1540, + "feses'" : 1550, + "fes'" : 1560, + "f'" : 1570, + "fis'" : 1580, + "fisis'" : 1590, + "geses'" : 1600, + "ges'" : 1610, + "g'" : 1620, + "gis'" : 1630, + "gisis'" : 1640, + "aeses'" : 1650, + "aes'" : 1660, + "a'" : 1670, + "ais'" : 1680, + "aisis'" : 1690, + "beses'" : 1700, + "bes'" : 1710, + "b'" : 1720, + "bis'" : 1730, + "bisis'" : 1740, + "ceses''" : 1750, + "ces''" : 1760, + "c''" : 1770, + "cis''" : 1780, + "cisis''" : 1790, + "deses''" : 1800, + "des''" : 1810, + "d''" : 1820, + "dis''" : 1830, + "disis''" : 1840, + "eeses''" : 1850, + "ees''" : 1860, + "e''" : 1870, + "eis''" : 1880, + "eisis''" : 1890, + "feses''" : 1900, + "fes''" : 1910, + "f''" : 1920, + "fis''" : 1930, + "fisis''" : 1940, + "geses''" : 1950, + "ges''" : 1960, + "g''" : 1970, + "gis''" : 1980, + "gisis''" : 1990, + "aeses''" : 2000, + "aes''" : 2010, + "a''" : 2020, + "ais''" : 2030, + "aisis''" : 2040, + "beses''" : 2050, + "bes''" : 2060, + "b''" : 2070, + "bis''" : 2080, + "bisis''" : 2090, + "ceses'''" : 2100, + "ces'''" : 2110, + "c'''" : 2120, + "cis'''" : 2130, + "cisis'''" : 2140, + "deses'''" : 2150, + "des'''" : 2160, + "d'''" : 2170, + "dis'''" : 2180, + "disis'''" : 2190, + "eeses'''" : 2200, + "ees'''" : 2210, + "e'''" : 2220, + "eis'''" : 2230, + "eisis'''" : 2240, + "feses'''" : 2250, + "fes'''" : 2260, + "f'''" : 2270, + "fis'''" : 2280, + "fisis'''" : 2290, + "geses'''" : 2300, + "ges'''" : 2310, + "g'''" : 2320, + "gis'''" : 2330, + "gisis'''" : 2340, + "aeses'''" : 2350, + "aes'''" : 2360, + "a'''" : 2370, + "ais'''" : 2380, + "aisis'''" : 2390, + "beses'''" : 2400, + "bes'''" : 2410, + "b'''" : 2420, + "bis'''" : 2430, + "bisis'''" : 2440, + "ceses''''" : 2450, + "ces''''" : 2460, + "c''''" : 2470, + "cis''''" : 2480, + "cisis''''" : 2490, + "deses''''" : 2500, + "des''''" : 2510, + "d''''" : 2520, + "dis''''" : 2530, + "disis''''" : 2540, + "eeses''''" : 2550, + "ees''''" : 2560, + "e''''" : 2570, + "eis''''" : 2580, + "eisis''''" : 2590, + "feses''''" : 2600, + "fes''''" : 2610, + "f''''" : 2620, + "fis''''" : 2630, + "fisis''''" : 2640, + "geses''''" : 2650, + "ges''''" : 2660, + "g''''" : 2670, + "gis''''" : 2680, + "gisis''''" : 2690, + "aeses''''" : 2700, + "aes''''" : 2710, + "a''''" : 2720, + "ais''''" : 2730, + "aisis''''" : 2740, + "beses''''" : 2750, + "bes''''" : 2760, + "b''''" : 2770, + "bis''''" : 2780, + "bisis''''" : 2790, + "ceses'''''" : 2800, + "ces'''''" : 2810, + "c'''''" : 2820, + "cis'''''" : 2830, + "cisis'''''" : 2840, + "deses'''''" : 2850, + "des'''''" : 2860, + "d'''''" : 2870, + "dis'''''" : 2880, + "disis'''''" : 2890, + "eeses'''''" : 2900, + "ees'''''" : 2910, + "e'''''" : 2920, + "eis'''''" : 2930, + "eisis'''''" : 2940, + "feses'''''" : 2950, + "fes'''''" : 2960, + "f'''''" : 2970, + "fis'''''" : 2980, + "fisis'''''" : 2990, + "geses'''''" : 3000, + "ges'''''" : 3010, + "g'''''" : 3020, + "gis'''''" : 3030, + "gisis'''''" : 3040, + "aeses'''''" : 3050, + "aes'''''" : 3060, + "a'''''" : 3070, + "ais'''''" : 3080, + "aisis'''''" : 3090, + "beses'''''" : 3100, + "bes'''''" : 3110, + "b'''''" : 3120, + "bis'''''" : 3130, + "bisis'''''" : 3140, + #"r" : float('inf'), a rest is not a pitch + } + +sortedNoteNameList = [ + "ceses,,," , + "ces,,," , + "c,,," , + "cis,,," , + "cisis,,," , + "deses,,," , + "des,,," , + "d,,," , + "dis,,," , + "disis,,," , + "eeses,,," , + "ees,,," , + "e,,," , + "eis,,," , + "eisis,,," , + "feses,,," , + "fes,,," , + "f,,," , + "fis,,," , + "fisis,,," , + "geses,,," , + "ges,,," , + "g,,," , + "gis,,," , + "gisis,,," , + "aeses,,," , + "aes,,," , + "a,,," , + "ais,,," , + "aisis,,," , + "beses,,," , + "bes,,," , + "b,,," , + "bis,,," , + "bisis,,," , + "ceses,," , + "ces,," , + "c,," , + "cis,," , + "cisis,," , + "deses,," , + "des,," , + "d,," , + "dis,," , + "disis,," , + "eeses,," , + "ees,," , + "e,," , + "eis,," , + "eisis,," , + "feses,," , + "fes,," , + "f,," , + "fis,," , + "fisis,," , + "geses,," , + "ges,," , + "g,," , + "gis,," , + "gisis,," , + "aeses,," , + "aes,," , + "a,," , + "ais,," , + "aisis,," , + "beses,," , + "bes,," , + "b,," , + "bis,," , + "bisis,," , + "ceses," , + "ces," , + "c," , + "cis," , + "cisis," , + "deses," , + "des," , + "d," , + "dis," , + "disis," , + "eeses," , + "ees," , + "e," , + "eis," , + "eisis," , + "feses," , + "fes," , + "f," , + "fis," , + "fisis," , + "geses," , + "ges," , + "g," , + "gis," , + "gisis," , + "aeses," , + "aes," , + "a," , + "ais," , + "aisis," , + "beses," , + "bes," , + "b," , + "bis," , + "bisis," , + "ceses" , + "ces" , + "c" , + "cis" , + "cisis" , + "deses" , + "des" , + "d" , + "dis" , + "disis" , + "eeses" , + "ees" , + "e" , + "eis" , + "eisis" , + "feses" , + "fes" , + "f" , + "fis" , + "fisis" , + "geses" , + "ges" , + "g" , + "gis" , + "gisis" , + "aeses" , + "aes" , + "a" , + "ais" , + "aisis" , + "beses" , + "bes" , + "b" , + "bis" , + "bisis" , + "ceses'" , + "ces'" , + "c'" , + "cis'" , + "cisis'" , + "deses'" , + "des'" , + "d'" , + "dis'" , + "disis'" , + "eeses'" , + "ees'" , + "e'" , + "eis'" , + "eisis'" , + "feses'" , + "fes'" , + "f'" , + "fis'" , + "fisis'" , + "geses'" , + "ges'" , + "g'" , + "gis'" , + "gisis'" , + "aeses'" , + "aes'" , + "a'" , + "ais'" , + "aisis'" , + "beses'" , + "bes'" , + "b'" , + "bis'" , + "bisis'" , + "ceses''" , + "ces''" , + "c''" , + "cis''" , + "cisis''" , + "deses''" , + "des''" , + "d''" , + "dis''" , + "disis''" , + "eeses''" , + "ees''" , + "e''" , + "eis''" , + "eisis''" , + "feses''" , + "fes''" , + "f''" , + "fis''" , + "fisis''" , + "geses''" , + "ges''" , + "g''" , + "gis''" , + "gisis''" , + "aeses''" , + "aes''" , + "a''" , + "ais''" , + "aisis''" , + "beses''" , + "bes''" , + "b''" , + "bis''" , + "bisis''" , + "ceses'''" , + "ces'''" , + "c'''" , + "cis'''" , + "cisis'''" , + "deses'''" , + "des'''" , + "d'''" , + "dis'''" , + "disis'''" , + "eeses'''" , + "ees'''" , + "e'''" , + "eis'''" , + "eisis'''" , + "feses'''" , + "fes'''" , + "f'''" , + "fis'''" , + "fisis'''" , + "geses'''" , + "ges'''" , + "g'''" , + "gis'''" , + "gisis'''" , + "aeses'''" , + "aes'''" , + "a'''" , + "ais'''" , + "aisis'''" , + "beses'''" , + "bes'''" , + "b'''" , + "bis'''" , + "bisis'''" , + "ceses''''" , + "ces''''" , + "c''''" , + "cis''''" , + "cisis''''" , + "deses''''" , + "des''''" , + "d''''" , + "dis''''" , + "disis''''" , + "eeses''''" , + "ees''''" , + "e''''" , + "eis''''" , + "eisis''''" , + "feses''''" , + "fes''''" , + "f''''" , + "fis''''" , + "fisis''''" , + "geses''''" , + "ges''''" , + "g''''" , + "gis''''" , + "gisis''''" , + "aeses''''" , + "aes''''" , + "a''''" , + "ais''''" , + "aisis''''" , + "beses''''" , + "bes''''" , + "b''''" , + "bis''''" , + "bisis''''" , + "ceses'''''" , + "ces'''''" , + "c'''''" , + "cis'''''" , + "cisis'''''" , + "deses'''''" , + "des'''''" , + "d'''''" , + "dis'''''" , + "disis'''''" , + "eeses'''''" , + "ees'''''" , + "e'''''" , + "eis'''''" , + "eisis'''''" , + "feses'''''" , + "fes'''''" , + "f'''''" , + "fis'''''" , + "fisis'''''" , + "geses'''''" , + "ges'''''" , + "g'''''" , + "gis'''''" , + "gisis'''''" , + "aeses'''''" , + "aes'''''" , + "a'''''" , + "ais'''''" , + "aisis'''''" , + "beses'''''" , + "bes'''''" , + "b'''''" , + "bis'''''" , + "bisis'''''" , + ] + +baseNotesToBaseNames = { + 20 : "C", + 70 : "D", + 120 : "E", + 170 : "F", + 220 : "G", + 270 : "A", + 320 : "B/H", + + 20-10 : "Ces", + 70-10 : "Des", + 120-10 : "Ees", + 170-10 : "Fes", + 220-10 : "Ges", + 270-10 : "Aes", + 320-10 : "Bes/Bb", + + 20+10 : "Cis", + 70+10 : "Dis", + 120+10 : "Eis", + 170+10 : "Fis", + 220+10 : "Gis", + 270+10 : "Ais", + 320+10 : "Bis/His", + } + +orderedBaseNotes = ["C", "D", "E", "F", "G", "A", "H/B"] + + +#Basic notes. For example to use as parameter for key-signatures in the API. +#P for Pitch +P_C = 20 +P_D = 70 +P_E = 120 +P_F = 170 +P_G = 220 +P_B = P_H = 270 + +#This is a funny historical coincidence. The s was used as sharp-sign before. Which lead to people pronouncing Cs "Cis". +P_Cs = 20 + 10 +P_Ds = 70 + 10 +P_Es = 120 + 10 +P_Fs = 170 + 10 +P_Gs = 220 + 10 +P_Bs = P_Hs = 270 + 10 + +P_Cb = 20 - 10 +P_Db = 70 - 10 +P_Eb = 120 - 10 +P_Fb = 170 - 10 +P_Gb = 220 - 10 +P_Bb = P_Hb = 270 - 10 + + +#Generate some tables and other cached dicts on startup +#These are mostly values that are called multiple thousand times per track for every callback +pitch2ly = dict((ly2pitch[k], k) for k in ly2pitch) + +cache_toScale = {} #filled dynamically +cache_fromMidi = {} #filled dynamically +toMidi = {} #filled for all pitches on startup, below +toWhite = {} #filled for all pitches on startup, below +tonalDistanceFromC = {} #filled for all pitches on startup, below +distanceInDiatonicStepsFrom1720 = {} #filled for all pitches on startup, below. Utilized by item.asDotOnLine + + +for pitch in ly2pitch.values(): + toWhite[pitch] = divmod(pitch, 50)[0] * 50 + 20 + octOffset = (octave(pitch) +1) * 12 #twelve tones per midi octave + toMidi[pitch] = octOffset + halfToneDistanceFromC(pitch) + tonalDistanceFromC[pitch] = divmod(plain(toWhite[pitch]), 50)[0] + +#second round for dependencies on finished pitchmath +for pitch in ly2pitch.values(): + distanceInDiatonicStepsFrom1720[pitch] = distanceInDiatonicSteps(1720, pitch) #offset from the middle line in treble clef h', which is 0. c'' is -1, a' is +1 + +#Lists of Notenames. Maps midi to different note systems +#These are simple fixed lists without root notes or key signatures. For a real conversion use pitch.fromMidi(root, keysig) +midi_notenames_english = [] +for _midipitch in range(128): + _octave, _pitch = divmod(_midipitch, 12) + midi_notenames_english.append("{}{}".format("C Db D Eb E F F# G Ab A Bb B".split()[_pitch] ,_octave-1)) + +midi_notenames_german = [] +for _midipitch in range(128): + _octave, _pitch = divmod(_midipitch, 12) + midi_notenames_german.append("{}{}".format("C Des D Es E F Fis G As A B H".split()[_pitch] ,_octave-1)) + +"""midi_notenames_lilypond = [] +for _midipitch in range(128): + _octave, _pitch = divmod(_midipitch, 12) + midi_notenames_lilypond.append("{}{}".format("C Des D Ees E F Fis G Aes A Bes B".split()[_pitch] ,_octave-1)) +""" + +#60 is C' +t = """ +C,,,, Des,,,, D,,,, Ees,,,, E,,,, F,,,, Fis,,,, G,,,, Aes,,,, A,,,, Bes,,,, B,,,, +C,,, Des,,, D,,, Ees,,, E,,, F,,, Fis,,, G,,, Aes,,, A,,, Bes,,, B,,, +C,, Des,, D,, Ees,, E,, F,, Fis,, G,, Aes,, A,, Bes,, B,, +C, Des, D, Ees, E, F, Fis, G, Aes, A, Bes, B, +C Des D Ees E F Fis G Aes A Bes B +C' Des' D' Ees' E' F' Fis' G' Aes' A' Bes' B' +C'' Des'' D'' Ees'' E'' F'' Fis'' G'' Aes'' A'' Bes'' B'' +C''' Des''' D''' Ees''' E''' F''' Fis''' G''' Aes''' A''' Bes''' B''' +C'''' Des'''' D'''' Ees'''' E'''' F'''' Fis'''' G'''' Aes'''' A'''' Bes'''' B'''' +C''''' Des''''' D''''' Ees''''' E''''' F''''' Fis''''' G''''' Aes''''' A''''' Bes''''' B''''' +C'''''' Des'''''' D'''''' Ees'''''' E'''''' F'''''' Fis'''''' G'''''' +""" +midi_notenames_lilypond = t.split() + + +midi_notenames_gm_drums = [str(i) for i in range(0,35)] + [ + "Acoustic BD", + "Bass Drum 1", + "Side Stick", + "Acoustic Snare", + "Hand Clap", + "Electric Snare", + "Low Floor Tom", + "Closed Hi Hat", + "High Floor Tom", + "Pedal Hi-Hat", + "Low Tom", + "Open Hi-Hat", + "Low-Mid Tom", + "Hi-Mid Tom", + "Crash Cymbal 1", + "High Tom", + "Ride Cymbal 1", + "Chinese Cymbal", + "Ride Bell", + "Tambourine", + "Splash Cymbal", + "Cowbell", + "Crash Cymbal 2", + "Vibraslap", + "Ride Cymbal 2", + "Hi Bongo", + "Low Bongo", + "Mute Hi Conga", + "Open Hi Conga", + "Low Conga", + "High Timbale", + "Low Timbale", + "High Agogo", + "Low Agogo", + "Cabasa", + "Maracas", + "Short Whistle", + "Long Whistle", + "Short Guiro", + "Long Guiro", + "Claves", + "Hi Wood Block", + "Low Wood Block", + "Mute Cuica", + "Open Cuica", + "Mute Triangle", + "Open Triangle", + ] + [str(i) for i in range(87,128)] + + +def _defaultSimpleNoteNames(): + return midi_notenames_english + +simpleNoteNames = defaultdict(_defaultSimpleNoteNames) +simpleNoteNames["German"] = midi_notenames_german +simpleNoteNames["English"] = midi_notenames_english +simpleNoteNames["Lilypond"] = midi_notenames_lilypond +simpleNoteNames["Drums GM"] = midi_notenames_gm_drums + + + diff --git a/template/engine/resources/LICENSE b/template/engine/resources/LICENSE new file mode 100644 index 0000000..7f73629 --- /dev/null +++ b/template/engine/resources/LICENSE @@ -0,0 +1 @@ +Licenses for 3rd party resources can be found in text files with the same name as the resource. diff --git a/template/engine/resources/metronome/metronome.sfz b/template/engine/resources/metronome/metronome.sfz new file mode 100644 index 0000000..3e80f70 --- /dev/null +++ b/template/engine/resources/metronome/metronome.sfz @@ -0,0 +1,2 @@ + key=77 sample=normal.wav count=1 //normal metronome tick + key=76 sample=stressed.wav count=1 //stressed metronome tick diff --git a/template/engine/resources/metronome/normal.wav b/template/engine/resources/metronome/normal.wav new file mode 100644 index 0000000..ae57da1 Binary files /dev/null and b/template/engine/resources/metronome/normal.wav differ diff --git a/template/engine/resources/metronome/stressed.wav b/template/engine/resources/metronome/stressed.wav new file mode 100644 index 0000000..6463e75 Binary files /dev/null and b/template/engine/resources/metronome/stressed.wav differ diff --git a/template/engine/sampler_sf2.py b/template/engine/sampler_sf2.py new file mode 100644 index 0000000..f6fd675 --- /dev/null +++ b/template/engine/sampler_sf2.py @@ -0,0 +1,373 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ). + +This application 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 . +""" + + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Python Standard Lib +import os.path + +#Third Party +from calfbox import cbox + +#Template Modules +from .data import Data +from .input_midi import MidiInput + +#Client Modules +from engine.config import * #imports METADATA + +class Sampler_sf2(Data): + """ + Channels are base 1 + Programs and banks are base 0 + + We cache the complete list of banks/programs in self.patchlist + but we always fetch the active patches (16 channels) live from calfbox through + self.activePatches() + """ + def __init__(self, parentSession, filePath, activePatches, ignoreProgramChanges, mixer:bool, defaultSoundfont=None): + super().__init__(parentSession) + + self._unlinkOnSave = [] + self.filePath = filePath + self.patchlist = {} #bank:{program:name} + self.buffer_ignoreProgramChanges = ignoreProgramChanges + + self.midiInput = MidiInput(session=parentSession, portName="all") + cbox.JackIO.Metadata.set_port_order(f"{cbox.JackIO.status().client_name}:all", 0) #first one. The other port order later start at 1 because we loop 1-based for channels. + + #Set libfluidsynth! to 16 output pairs. We prepared 32 jack ports in the session start. "soundfont" is our given name, in the line below. This is a prepared config which will be looked up by add_new_instrument + cbox.Config.set("instrument:soundfont", "engine", "fluidsynth") + cbox.Config.set("instrument:soundfont", "output_pairs", 16) #this is not the same as session.py cbox.Config.set("io", "outputs", METADATA["cboxOutputs"]). It is yet another layer and those two need connections + #Create the permanent instrument which loads changing sf2 + layer = self.midiInput.scene.add_instrument_layer("soundfont") + self.instrumentLayer = layer + self.instrument = layer.get_instrument() + self.loadSoundfont(filePath, defaultSoundfont) + + #Block incoming program and bank changes per midi. + #Save and load is done via the instance variable. + self.midiInput.scene.status().layers[0].set_ignore_program_changes(ignoreProgramChanges) + assert self._ignoreProgramChanges == ignoreProgramChanges + + #Active patches is a dynamic value. Load the saved ones here, but do not save their state locally + try: + self.setActivePatches(activePatches) + except: + self._correctActivePatches() + + #Create a dynamic pair of audio output ports and route all stereo pairs to our summing channel. + #This does _not_ need updating when loading another sf2 or changing instruments. + + assert not self.parentSession.standaloneMode is None + if self.parentSession.standaloneMode: + lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1 + rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2 + else: + lmixUuid = cbox.JackIO.create_audio_output('left_mix') + rmixUuid = cbox.JackIO.create_audio_output('right_mix') + + for i in range(16): + router = cbox.JackIO.create_audio_output_router(lmixUuid, rmixUuid) + router.set_gain(-3.0) + self.instrument.get_output_slot(i).rec_wet.attach(router) #output_slot is 0 based and means a pair + #slot 17 is an error. cbox tells us there is only [1, 16], good. + + #Set port order. It is already correct because alphanumerial order, but explicit is better than implicit + for channelNumber in range(1,33): + portname = f"{cbox.JackIO.status().client_name}:out_{channelNumber}" + try: + cbox.JackIO.Metadata.set_port_order(portname, channelNumber) + except Exception as e: #No Jack Meta Data + logger.error(e) + break + + #Also sort the mixing channels + try: + portname = f"{cbox.JackIO.status().client_name}:left_mix" + cbox.JackIO.Metadata.set_port_order(portname, 33) + portname = f"{cbox.JackIO.status().client_name}:right_mix" + cbox.JackIO.Metadata.set_port_order(portname, 34) + except Exception as e: #No Jack Meta Data + logger.error(e) + + #If demanded create 16 routing midi channels, basically a jack port -> midi channel merger. + if True: + for midiRouterPortNum in range(1,17): + portName = f"channel_{str(midiRouterPortNum).zfill(2)}" + router_cboxMidiPortUid = cbox.JackIO.create_midi_input(portName) + router_scene = cbox.Document.get_engine().new_scene() + router_scene.clear() + router_scene.set_enable_default_song_input(False) + #router_scene.add_instrument_layer("soundfont") #This creates multiple sf2 instances + router_midi_layer = router_scene.add_new_midi_layer(router_cboxMidiPortUid) #Create a midi layer for our input port. That layer supports manipulation like transpose or channel routing. + #cbox.JackIO.route_midi_input(router_cboxMidiPortUid, router_scene.uuid) + router_midi_layer.set_out_channel(midiRouterPortNum) + router_midi_layer.set_in_channel(midiRouterPortNum) + #router_midi_layer.set_enable(False) + + #cbox.JackIO.route_midi_input(router_cboxMidiPortUid, self.midiInput.scene.uuid) #routes directly to the instrument and ignores the layer options like channel forcing + #cbox.JackIO.route_midi_input(router_cboxMidiPortUid, self.midiInput.cboxMidiPortUid) #runs, but does nothing + #cbox.JackIO.route_midi_input(router_midi_layer, self.midiInput.cboxMidiPortUid) #does not run + router_midi_layer.set_external_output(self.midiInput.cboxMidiPortUid) + #router_midi_layer.set_external_output(self.midiInput.scene.uuid) + #Port order + fullPortname = f"{cbox.JackIO.status().client_name}:out_{channelNumber}" + try: + cbox.JackIO.Metadata.set_port_order(fullPortname, midiRouterPortNum) + except Exception as e: #No Jack Meta Data + pass #don't break here. + + @property + def _ignoreProgramChanges(self): + try: + result = self.midiInput.scene.status().layers[0].status().ignore_program_changes + self.buffer_ignoreProgramChanges = result + except IndexError: #No layers means no loaded instrument yet. e.g. program start or in between loading. + result = self.buffer_ignoreProgramChanges + return result + + def unlinkUnusedSoundfonts(self, stopSession=False): + if stopSession and self.parentSession.lastSavedData and not self.filePath == self.parentSession.lastSavedData["filePath"]: + #The user might want to discard the current unsaved setup (self.filePath) by closing without saving. + #self.filePath holds the current file, not the saved file. We override to not get the actual saved setting deleted on quit. + self._unlinkOnSave.append(self.filePath) + self.filePath = self.parentSession.lastSavedData["filePath"] + + for filePath in self._unlinkOnSave: + if not filePath == self.filePath and os.path.exists(filePath): + os.unlink(filePath) + self._unlinkOnSave = [] + + + def _convertPatches(self, dictionary:dict): + """Returns a dict of dicts: + bank:{program:name}""" + result = {} + for value, name in dictionary.items(): + program = value & 127 + bank = value >> 7 + if not bank in result: + result[bank] = {} + result[bank][program] = name + return result + + def loadSoundfont(self, filePath, defaultSoundfont=None): + """defaultSoundfont is a special case. The path is not saved""" + + logger.info(f"loading path: \"{filePath}\" defaultSoundfont parameter: {defaultSoundfont}") + + #Remove the old link, if present. We cannot unlink directly in loadSoundfont because it is quite possible that a user will try out another soundfont but decide not to save but close and reopen to get his old soundfont back. + if self.filePath and os.path.islink(self.filePath): + self._unlinkOnSave.append(self.filePath) + + #self.midiInput.scene.clear() do not call for the sf2 engine. Will kill our instrument. + + for stereoPair in range(16): #midi channels fluidsynth output . 0-15 + #print (stereoPair) + slot = self.instrument.get_output_slot(stereoPair) #slot is 0 based + slot.set_output(stereoPair+1) #output is 1 based. setOutput routes to cbox jack ports, which are the same order. + + self.patchlist = {} + try: + if defaultSoundfont: + self.instrument.engine.load_soundfont(defaultSoundfont) + self.filePath = "" #redundant, but not wrong. + else: + self.instrument.engine.load_soundfont(filePath) + self.filePath = filePath + self.patchlist = self._convertPatches( self.instrument.engine.get_patches() ) + self._correctActivePatches() + return True, "" + except Exception as e: #throws a general Exception if not a good file + #TODO: An error here leads to an undefined program state with no soundfont loaded. Either restore the old state perfectly (but why?) or go into a defined "nothing loaded" state. + if os.path.exists(filePath) and os.path.islink(filePath): + os.unlink(filePath) #the file was maybe already linked. undo + return False, str(e) + + def _correctActivePatches(self): + """Check if the current choice of bank and program actually exists in the soundfont + and fall back to an existing value (or error). + We keep no internal state of active patches but always ask cbox. Therefore we directly + change cbox here and now.""" + if not self.patchlist: + self.patchlist = self._convertPatches( self.instrument.engine.get_patches() ) #try again + if not self.patchlist: raise ValueError("No programs in this sound font") + + for channel, (value, name) in self.instrument.engine.status().patch.items(): #channel is base 1 + program = value & 127 + bank = value >> 7 + if bank in self.patchlist and program in self.patchlist[bank]: + continue + else: + #just pick one at random + for youchoose_bank in self.patchlist.keys(): + for youchoose_program, youchoose_name in self.patchlist[youchoose_bank].items(): + youchoose_bank, youchoose_program, youchoose_name + logger.info(METADATA["name"] + f": Channel {channel} Old bank/program not possible in new soundfont. Switching to first available program.") + self.setPatch(channel, youchoose_bank, youchoose_program) + break #inner loop. one instrument is enough. + + + + def activePatches(self): + """Returns a dict of 16 tuples: + channel(count from 1):(int bank, int program, str name) + + "Acoustic Piano" is the default name, since that is fluidsynth behaviour. + + Takes the current bank into consideration + """ + result = {} + for channel, (value, name) in self.instrument.engine.status().patch.items(): + program = value & 127 + bank = value >> 7 + result[channel] = (bank,program,name) #the name doesn't matter for the program at all, it is only to get a human readable save file. + return result + + def setActivePatches(self, activePatchesDict): + """used for loading. + receives an activePatches dict and sets all banks and programs accordingly""" + assert self.patchlist, self.patchlist + for channel, (bank, program, name) in activePatchesDict.items(): #the name doesn't matter for the program at all, it is only to get a human readable save file. + self.setPatch(channel, bank, program) + + def setPatch(self, channel, bank, program): + """An input error happens not often, but can happen if the saved data mismatches the + soundfont. This happens on manual save file change or soundfont change (version update?) + between program runs. + We assume self.patchlist is up to date.""" + #After changing the bank we need to check if the program still exists. + #If not fall back to an existing program + if not program in self.patchlist[bank]: + for i in range(128): #0-127 + if i in self.patchlist[bank]: + program = i + break + else: + return ValueError(f"No Program in Bank {bank}") + + newValue = program | (bank<<7) + self.instrument.engine.set_patch(channel, newValue) #call to cbox + + def updateChannelAudioJackMetadaPrettyname(self, channel:int): + """Audio output channel pairs 1-16""" + bank, program, name = self.activePatches()[channel] #activePatches returns a dict, not a list. channel is a 1-based key, not a 0-based index. + + chanL = channel*2-1 #channel is 1-based. [1]*2-1 = 1. + chanR = channel*2 + + portnameL = f"{cbox.JackIO.status().client_name}:out_{chanL}" + portnameR = f"{cbox.JackIO.status().client_name}:out_{chanR}" + + #Use the instrument name as port name: 03-L:Violin + try: + cbox.JackIO.Metadata.set_pretty_name(portnameL, f"{str(channel).zfill(2)}-L : {name}") + cbox.JackIO.Metadata.set_pretty_name(portnameR, f"{str(channel).zfill(2)}-R : {name}") + except Exception as e: #No Jack Meta Data + logger.error(e) + + def updateChannelMidiInJackMetadaPrettyname(self, channel:int): + """Midi in single channels 1-16""" + bank, program, name = self.activePatches()[channel] #activePatches returns a dict, not a list. channel is a 1-based key, not a 0-based index. + + portName = f"channel_{str(channel).zfill(2)}" + fullPortname = f"{cbox.JackIO.status().client_name}:{portName}" + + #Use the instrument name as port name: 03-L:Violin + try: + cbox.JackIO.Metadata.set_pretty_name(fullPortname, f"{str(channel).zfill(2)} : {name}") + except Exception as e: #No Jack Meta Data + logger.error(e) + + + def updateAllChannelJackMetadaPrettyname(self): + """Add this to a callback. It can't be run on program startup because sampler_sf2 is + initiated before cbox creates its audio ports. + + It is advised to use this in a controlled manner. There is no internal check if + instruments changed and subsequent renaming. Multiple changes in a row are common, + therefore the place to update is in the API, where the new state is also sent to the UI. + """ + for channel in range(1,17): + self.updateChannelAudioJackMetadaPrettyname(channel) + self.updateChannelMidiInJackMetadaPrettyname(channel) + + """ + for channel, (bank, program, name) in self.activePatches().items(): + chanL = channel*2-1 #channel is 1-based. [1]*2-1 = 1. + chanR = channel*2 + + portnameL = f"{cbox.JackIO.status().client_name}:out_{chanL}" + portnameR = f"{cbox.JackIO.status().client_name}:out_{chanR}" + + #Use the instrument name as port name: 03-L:Violin + cbox.JackIO.Metadata.set_pretty_name(portnameL, f"{str(channel).zfill(2)}-L : {name}") + cbox.JackIO.Metadata.set_pretty_name(portnameR, f"{str(channel).zfill(2)}-R : {name}") + """ + + #Save / Load / Export + def serialize(self)->dict: + self.unlinkUnusedSoundfonts() #We do not want them to be in the save file. + return { + "filePath" : self.filePath, + "activePatches" : self.activePatches(), + "ignoreProgramChanges" : self.midiInput.scene.status().layers[0].status().ignore_program_changes, #includes bank changes + } + + @classmethod + def instanceFromSerializedData(cls, parentSession, serializedData): + """The entry function to create a sampler state from saved data. It is called by the session. + + This functions triggers a tree of other createInstanceFromSerializedData which finally + returns Sampler_sf2, which gets saved in the session. + + The serializedData is already converted to primitive python types from json, + but nothing more. Here we create the actual objects.""" + self = cls(parentSession=parentSession, + #Other parameters or set loaded data different from standard init after creation + filePath=serializedData["filePath"], + activePatches=serializedData["activePatches"], + ignoreProgramChanges=serializedData["ignoreProgramChanges"], + ) + return self + + def exportChannel(self, channel:int): + bank, program, name = self.activePatches()[channel] #activePatches returns a dict, not a list. channel is a 1-based key, not a 0-based index. + return { + "channel" : channel, + "bank": bank, + "program": program, + "name": name, + } + + def export(self)->dict: + prettyName = os.path.basename(self.filePath) + if prettyName.endswith(".sf2"): + prettyName = prettyName[:-4] + return { + "filePath":self.filePath, + "patchlist":self.patchlist, + "activePatches":self.activePatches(), + "name": prettyName.title(), + } diff --git a/template/engine/sequencer.py b/template/engine/sequencer.py new file mode 100644 index 0000000..a68af4f --- /dev/null +++ b/template/engine/sequencer.py @@ -0,0 +1,705 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Standard Library +from typing import List, Dict, Tuple, Iterable + +#Third Party Modules +from calfbox import cbox + +#Template Modules +from .data import Data +from .metronome import Metronome +from .duration import traditionalNumberToBaseDuration, MAXIMUM_TICK_DURATION + +#Client Modules +from engine.config import * #includes METADATA only. No other environmental setup is executed. +class Score(Data): + """Manages and holds tracks + Has a mutable list of Track instances. This is the official order. Rearranging happens here. + Order ist reflected in JACK through metadata. UIs should adopt it as well. + + Score.TrackClass needs to be injected with your Track class. + + Score.TrackClass needs to have a SequencerInterface of type SequencerInterface + + self.tracks holds only active tracks. Which means tracks that produce sound + That does not mean that they are visible or editable for the user. + Does NOT hold deleted tracks in the undo storage. You need to hold these tracks in memory + yourself before calling score.delete. For example Laborejo registers the Track instance + in our history module which keeps the instance alive. + + Special Tracks do not need to be created here. E.g. a metronome can be just a track. + + """ + TrackClass = None + + def __init__(self, parentSession): + assert Score.TrackClass + super().__init__(parentSession) + self.tracks = [] #see docstring + self.tempoMap = TempoMap(parentData = self) + self._template_processAfterInit() + self._tracksFailedLookup = [] + + def _template_processAfterInit(self): #needs a different name because there is an inherited class with the same method. + """Call this after either init or instanceFromSerializedData""" + if METADATA["metronome"]: + self.metronome = Metronome(parentData=self) #Purely dynamic structure. No save/load. No undo/redo + + #Whole Score / Song + + def buildSongDuration(self, startEndTuple=None): + """Set playback length for the entire score or a loop. + Why is start the end-tick of the song? + Starting from 0 would create an actual loop from the start to end. + We want the song to play only once. + The cbox way of doing that is to set the loop range to zero at the end of the track. + Zero length is stop. + """ + if startEndTuple is None: + longestTrackDuration = max(track.sequencerInterface.cachedDuration for track in self.tracks) + start = longestTrackDuration + end = longestTrackDuration + else: + start, end = startEndTuple + cbox.Document.get_song().set_loop(start, end) + + + #Tracks + def addTrack(self, name:str=""): + """Create and add a new track. Not an existing one""" + track = Score.TrackClass(parentData=self, name=name) + assert track.sequencerInterface + self.tracks.append(track) + return track + + def deleteTrack(self, track): + track.sequencerInterface.prepareForDeletion() + self.tracks.remove(track) + return track #for undo + + def updateJackMetadataSorting(self): + """Add this to you "tracksChanged" or "numberOfTracksChanged" callback. + Tell cbox to reorder the tracks by metadata. Deleted ports are automatically removed by JACK. + + It is advised to use this in a controlled manner. There is no Score-internal check if + self.tracks changed and subsequent sorting. Multiple track changes in a row are common, + therefore the place to update jack order is in the API, where the new track order is also + sent to the UI. + + We also check if the track is 'deactivated' by probing track.cboxMidiOutUuid. + Patroneo uses prepareForDeletion to deactive the tracks standalone track but keeps the + interface around for later use. + """ + order = {portName:index for index, portName in enumerate(track.sequencerInterface.cboxPortName() for track in self.tracks if track.sequencerInterface.cboxMidiOutUuid)} + + try: + cbox.JackIO.Metadata.set_all_port_order(order) + except Exception as e: #No Jack Meta Data or Error with ports. + logger.error(e) + + + def trackById(self, trackId:int): + """Returns a track or None, if not found""" + for track in self.tracks: + if trackId == id(track): + return track + else: + #Previously this crashed with a ValueError. However, after a rare bug that a gui widget focussed out because a track was deleted and then tried to send its value to the engine we realize that this lookup can gracefully return None. + #Nothing will break: Functions that are not aware yet, that None is an option will crash when they try to access None as a track object. For this case we present the following logger error: + if not trackId in self._tracksFailedLookup: + logger.error(f"Track {trackId} not found. Current Tracks: {[id(tr) for tr in self.tracks]}") + self._tracksFailedLookup.append(trackId) #prevent multiple error messages for the same track in a row. + return None + #raise ValueError(f"Track {trackId} not found. Current Tracks: {[id(tr) for tr in self.tracks]}") + + + + #Save / Load / Export + def serialize(self)->dict: + return { + "tracks" : [track.serialize() for track in self.tracks], + "tempoMap" : self.tempoMap.serialize(), + } + + @classmethod + def instanceFromSerializedData(cls, parentSession, serializedData): + """The entry function to create a score from saved data. It is called by the session. + + This functions triggers a tree of other createInstanceFromSerializedData which finally + return the score, which gets saved in the session. + + The serializedData is already converted to primitive python types from json, + but nothing more. Here we create the actual objects.""" + self = cls.__new__(cls) + Score.copyFromSerializedData(parentSession, serializedData, self) + return self + + @staticmethod + def copyFromSerializedData(parentSession, serializedData, childObject): + """ + childObject is a Score or similar. + + Because this is an actual parent class we can't use instanceFromSerializedData in a child + without actually creating an object. Long story short, use this to generate the data and + use it in your child class. If the Data class is used standalone it still can be used.""" + childObject.parentSession = parentSession + loadedTracks=[] + for trackSrzData in serializedData["tracks"]: + track = Score.TrackClass.instanceFromSerializedData(parentData=childObject, serializedData=trackSrzData) + loadedTracks.append(track) + + childObject.tracks=loadedTracks + childObject.tempoMap=TempoMap.instanceFromSerializedData(parentData=childObject, serializedData=serializedData["tempoMap"]) + childObject._template_processAfterInit() + + def export(self)->dict: + return { + "numberOfTracks" : len(self.tracks), + #"duration" : self. + } + + + +class _Interface(object): + #no load or save. Do that in the child classes. + + def __init__(self, parentTrack, name=None): + self.parentTrack = parentTrack + self.parentData = parentTrack.parentData + self._name = self._isNameAvailable(name) if name else str(id(self)) + self._enabled = True + self._processAfterInit() + + def _processAfterInit(self): + self._cachedPatterns = [] #makes undo after delete possible + self.calfboxTrack = cbox.Document.get_song().add_track() + self.calfboxTrack.set_name(self.name) #only cosmetic and cbox internal. Useful for debugging, not used in jack. + + #Caches and other Non-Saved attributes + self.cachedDuration = 0 #used by parentData.buildSongDuration to calculate the overall length of the song by checking all tracks. + + + @property + def name(self): + return self._name + + @property + def enabled(self)->bool: + return self._enabled + + def _isNameAvailable(self, name:str): + """Check if the name is free. If not increment""" + name = ''.join(ch for ch in name if ch.isalnum() or ch in (" ", "_", "-")) #sanitize + name = " ".join(name.split()) #remove double spaces + while name in [tr.sequencerInterface.name for tr in self.parentData.tracks]: + beforeLastChar = name[-2] + lastChar = name[-1] + if beforeLastChar==" " and lastChar.isalnum() and lastChar not in ("9", "z", "Z"): + #Pattern is "Trackname A" or "Trackname 1" which can be incremented. + name = name[:-1] + chr(ord(name[-1]) +1) + else: + name = name + " A" + return name + + def _updatePlayback(self): + self.parentData.buildSongDuration() + cbox.Document.get_song().update_playback() + + def setTrack(self, blobs:Iterable): #(bytes-blob, position, length) + """Converts an Iterable of (bytes-blob, position, length) to cbox patterns, clips and adds + them to an empty track, which replaces the current one. + + Simplest version is to send one blob at position 0 with its length.""" + #self.calfboxTrack.delete() #cbox clear data, not python structure + #self.calfboxTrack = cbox.Document.get_song().add_track() + #self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) + + self.calfboxTrack.clear_clips() + self._cachedPatterns = [] #makes undo after delete possible + pos = 0 + for blob, pos, leng in blobs: + if leng > 0: + pat = cbox.Document.get_song().pattern_from_blob(blob, leng) + t = (pat, pos, leng) + self._cachedPatterns.append(t) + + length = 0 + for pattern, position, length in self._cachedPatterns: + if length > 0: + self.calfboxTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern. + + + self.cachedDuration = pos + length #use the last set values + self._updatePlayback() + + def insertEmptyClip(self): + """Convenience function to make recording into an empty song possible. + Will be removed by self.setTrack.""" + blob = bytes() + #We do not need any content #blob += cbox.Pattern.serialize_event(0, 0x80, 60, 64) # note off + pattern = cbox.Document.get_song().pattern_from_blob(blob, MAXIMUM_TICK_DURATION) #blog, length + self.calfboxTrack.add_clip(0, 0, MAXIMUM_TICK_DURATION, pattern) #pos, offset, length, pattern. + self.cachedDuration = MAXIMUM_TICK_DURATION + self._updatePlayback() + +class _Subtrack(object): + """Generates its own midi data and caches the resulting track but does not have a name + nor its own jack midi port. Instead it is attached to an SequencerInterface. + + It is SequencerInterface because that has a jack midi port, and not Interface itself. + + Only used by SequencerInterface internally. Creation is done by its methods. + This is not a child class because a top level class' code is easier to read. + + Intended usecase is to add CC messages as one subtrack per CC number (e.g. CC7 = Volume). + + Of course you could just put CCs together with notes in the main Interface + and not use SubTracks. But where is the fun in that?""" + + def __init__(self, parentSequencerInterface): + self._cachedPatterns = [] #makes undo after delete possible + self.parentSequencerInterface = parentSequencerInterface + self.calfboxSubTrack = cbox.Document.get_song().add_track() + self.calfboxSubTrack.set_external_output(parentSequencerInterface.cboxMidiOutUuid) + + def prepareForDeletion(self): + self.calfboxSubTrack.delete() #in place self deletion. + self.calfboxSubTrack = None + cbox.Document.get_song().update_playback() + + def recreateThroughUndo(self): + assert self.calfboxSubTrack is None, self.calfboxSubTrack + self.calfboxSubTrack = cbox.Document.get_song().add_track() + for pattern, position, length in self._cachedPatterns: + self.calfboxSubTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern. + cbox.Document.get_song().update_playback() + + def setSubtrack(self, blobs:Iterable): #(bytes-blob, position, length) + """Does not add to the parents cached duration. Therefore it will not send data beyond + its parent track length, except if another track pushes the overall duration beyond.""" + self.calfboxSubTrack.clear_clips() + self._cachedPatterns = [] #makes undo after delete possible + pos = 0 + for blob, pos, leng in blobs: + if leng > 0: + pat = cbox.Document.get_song().pattern_from_blob(blob, leng) + t = (pat, pos, leng) + self._cachedPatterns.append(t) + + length = 0 + for pattern, position, length in self._cachedPatterns: + if length > 0: + self.calfboxSubTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern. + + cbox.Document.get_song().update_playback() + + +class SequencerInterface(_Interface): #Basically the midi part of a track. + """A tracks name is the same as the jack midi-out ports name. + + The main purpose of the child class is to manage its musical data and regulary + fill self.calfboxTrack with musical data: + + Create one ore more patterns, distribute them into clips, add clips to the cboxtrack. + + buffer = bytes() + buffer += cbox.Pattern.serialize_event(startTick, 0x90, pitch, velocity) # note on + buffer += cbox.Pattern.serialize_event(endTick-1, 0x80, pitch, velocity) # note off #-1 ticks to create a small logical gap. Does not affect next note on. + pattern = cbox.Document.get_song().pattern_from_blob(buffer, oneMeasureInTicks) + self.calfboxTrack.add_clip(index*oneMeasureInTicks, 0, oneMeasureInTicks, pattern) #pos, pattern-internal offset, length, pattern. + + Use caches to optimize performance. This is mandatory! + self.cachedDuration = the maximum track length. Used to determine the song playback duration. + + """ + + def _processAfterInit(self): + #Create midi out and cbox track + logger.info(f"Creating empty SequencerInterface instance for {self._name}") + super()._processAfterInit() + self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self._name) + self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) + cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) + + self._subtracks = {} #arbitrary key: _Subtrack(). This is not in Interface itself because Subtracks assume a jack midi out. + self.enable(self._enabled) + + + def enable(self, enabled): + """This is "mute", more or less. It only disables the note parts, not CCs or other subtracks. + This means if you switch this on again during playback you will have the correct context.""" + if enabled: + #self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) #Old version. Does not prevent hanging notes. + self.calfboxTrack.set_mute(0) + else: + #self.calfboxTrack.set_external_output("") #Old version. Does not prevent hanging notes. + self.calfboxTrack.set_mute(1) + + self._enabled = bool(enabled) + cbox.Document.get_song().update_playback() + + @_Interface.name.setter + def name(self, value): + if not value in (track.sequencerInterface.name for track in self.parentData.tracks): + self._name = self._isNameAvailable(value) + if self.cboxMidiOutUuid: #we could be deactivated + cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) + + def cboxPortName(self)->str: + """Return the complete jack portname: OurName:PortName""" + portname = cbox.JackIO.status().client_name + ":" + self.name + return portname + + def prepareForDeletion(self): + """Called by score right before this track gets deleted. + + This does not mean the track is gone. It can be recovered by + undo. That is why we bother setting calfboxTrack to None + again. + """ + if not self.calfboxTrack: #maybe non-template part deactivated it, like Patroneo groups + return + + try: + portlist = cbox.JackIO.get_connected_ports(self.cboxPortName()) + except: #port not found. + portlist = [] + + self._beforeDeleteThisJackMidiWasConnectedTo = portlist + + self.calfboxTrack.set_external_output("") + cbox.JackIO.delete_midi_output(self.cboxMidiOutUuid) + self.calfboxTrack.delete() #in place self deletion. + self.calfboxTrack = None + self.cboxMidiOutUuid = None + #we leave cachedDuration untouched + self._updatePlayback() + + def recreateThroughUndo(self): + """Brings this track back from the dead, in-place. + Assumes this track instance was not in the score but + somewhere in memory. self.prepareForDeletion() was called + in the past which deleted the midi output but not the cbox-midi + data it generated and held""" + #Recreate Calfbox Midi Data + assert self.calfboxTrack is None, self.calfboxTrack + self.calfboxTrack = cbox.Document.get_song().add_track() + self.calfboxTrack.set_name(self.name) #only cosmetic and cbox internal. Useful for debugging, not used in jack. + for pattern, position, length in self._cachedPatterns: + self.calfboxTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern. + #self.cachedDuration is still valid + + #Create MIDI and reconnect Jack + self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self.name) + cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) + self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) + for port in self._beforeDeleteThisJackMidiWasConnectedTo: + try: + cbox.JackIO.port_connect(self.cboxPortName(), port) + except: #external connected synth is maybe gone. Prevent crash. + logger.warning(f"Previously external connection {port} is gone. Can't connect anymore." ) + + #Make it official + self._updatePlayback() + + def setSubtrack(self, key, blobs:Iterable): #(bytes-blob, position, length) + """Creates a new subtrack if key is unknown + Forward data to the real function + + Simplest version is to send one blob at position 0 with its length""" + if not key in self._subtracks: + self._subtracks[key] = _Subtrack(parentSequencerInterface=self) + + assert self._subtracks[key], key + assert isinstance(self._subtracks[key], _Subtrack), type(self._subtracks[key]) + + self._subtracks[key].setSubtrack(blobs) + + def deleteSubtrack(self, key): + """Remove a subtrack. + Return for a potential undo""" + self._subtracks[key].prepareForDeletion() + toDelete = self._subtracks[key] + del self._subtracks[key] + return toDelete + + + #Save / Load / Export + def serialize(self)->dict: + """Generate Data to save as json""" + return { + "name" : self.name, + "enabled" : self._enabled, + } + + @classmethod + def instanceFromSerializedData(cls, parentTrack, serializedData): + self = cls.__new__(cls) + self._name = serializedData["name"] + self._enabled = serializedData["enabled"] + + self.parentTrack = parentTrack + self.parentData = parentTrack.parentData + self._processAfterInit() + return self + + def export(self)->dict: + return { + "id" : id(self), + "name" : self.name, + "index" : self.parentData.tracks.index(self.parentTrack) if self.parentTrack in self.parentData.tracks else None , #could be a special track, like the metronome + "cboxPortName" : self.cboxPortName(), + "cboxMidiOutUuid" : self.cboxMidiOutUuid, + "enabled" : self._enabled, + } + + +class SfzInstrumentSequencerInterface(_Interface): + """Like a midi output, only routes to an internal instrument. + + This is not a pure sfz sampler, but rather a track that ends in an instrument instead of a + jack midi output.""" + + def __init__(self, parentTrack, name:str, absoluteSfzPath:str): + + super().__init__(parentTrack, name) #includes processAfterInit + self.scene = cbox.Document.get_engine().new_scene() + self.scene.clear() + self.scene.add_new_instrument_layer(name, "sampler") #"sampler" is the cbox sfz engine + self.scene.status().layers[0].get_instrument().engine.load_patch_from_string(0, "", "", "") #fill with null instruments + self.scene.set_enable_default_song_input(True) + self.instrumentLayer = self.scene.status().layers[0].get_instrument() + + self.scene.status().layers[0].set_ignore_program_changes(1) #TODO: ignore different channels. We only want one channel per scene/instrument/port. + + newProgramNumber = 1 + program = self.instrumentLayer.engine.load_patch_from_file(newProgramNumber, absoluteSfzPath, name) + self.instrumentLayer.engine.set_patch(10, newProgramNumber) #from 1. 10 is the channel #TODO: we want this to be on all channels. + + #TODO: Metronome is not compatible with current cbox. we need to route midi data from our cbox track explicitely to self.scene, which is not possible right now. + self.calfboxTrack.set_external_output("") + + #Metadata + portnameL = f"{cbox.JackIO.status().client_name}:out_1" + portnameR = f"{cbox.JackIO.status().client_name}:out_2" + cbox.JackIO.Metadata.set_pretty_name(portnameL, name.title() + "-L") + cbox.JackIO.Metadata.set_pretty_name(portnameR, name.title() + "-R") + + def enable(self, enabled): + if enabled: + self.scene.status().layers[0].set_enable(True) + else: + self.scene.status().layers[0].set_enable(False) + self._enabled = bool(enabled) #this is redundant in the SfzInstrument, but the normal midi outs need this. So we stick to the convention. + cbox.Document.get_song().update_playback() + + @property + def enabled(self)->bool: + return self._enabled + +class TempoMap(object): + """ + This is a singleton instance in Score. Don't subclass. + + Main data structure is self._tempoMap = {positionInTicks:(bpmAsFloat, timesigUpper, timesigLower)} + + The tempo map is only active if the whole program is JACK Transport Master (via Cbox). + If not we simply follow jack sync. + + All values are floats. + + TempoMap itself handles this global switch if you set isTransportMaster=True (it is a property) + + For simplicity reasons the tempo map only deals with quarter notes per minute internally. + There are functions to convert to and from a number of other tempo formats. + + There are three recommended ways to change the tempo map: + + 1) setTempoMap completely replaces the tempo map with a new supplied one + + 2) If you want just one tempo use the convenience function setQuarterNotesPerMinute. + This will override and delete(!) the current tempo map. + You can retrieve it with getQuarterNotePerMinute. + + 3) set isTransportMaster will trigger a rebuild of the tempo map as a side effect. It does not + change the existing tempo map. Flipping the transport master back will reenable the old tempo Map + + If you want to incrementally change the tempo map, which is really not necessary because + changing it completely is a very cheap operation, you can edit the dict _tempoMap directly. + + In case you have a complex tempo management yourself, like Laborejo, use it to setTempoMap and + then don't worry about save and load. Treat it as a cache that conveniently restores the last + setting after program startup. """ + + + + def __init__(self, parentData): + logger.info("Creating empty TempoMap instance") + self.parentData = parentData + self._tempoMap = {0:(120.0, 4, 4)} # 4/4, 120bpm. will not be used on startup, but is needed if transportMaster is switched on + self._isTransportMaster = False + self._processAfterInit() + assert not cbox.Document.get_song().status().mtis + + def _processAfterInit(self): + self.factor = 1.0 # not saved + self.isTransportMaster = self._isTransportMaster #already triggers cbox settings through @setter. + self._sanitize() + + def _updatePlayback(self): + """A wrapper that not only calls update playback but forces JACK to call its BBT callback, + so it gets the new tempo info even without transport running. + + That is a bit of a hack, but it works without disturbing anything too much.""" + cbox.Document.get_song().update_playback() + pos = cbox.Transport.status().pos #can be None on program start + if self.isTransportMaster and not cbox.Transport.status().playing: #pos can be 0 + if pos is None: + #Yes, we destroy the current playback position. But we ARE timebase master, so that is fine. + cbox.Transport.seek_samples(0) + else: #default case + cbox.Transport.seek_samples(pos) + + + @property + def isTransportMaster(self) -> bool: + return self._isTransportMaster + + @isTransportMaster.setter + def isTransportMaster(self, value:bool): + logger.info(f"Jack Transport Master status: {value}") + self._isTransportMaster = value + if value: + self._sendToCbox() #reactivate existing tempo map + cbox.JackIO.external_tempo(False) + cbox.JackIO.transport_mode(master = True, conditional = False) #conditional = only attempt to become a master (will fail if there is one already) + else: + self._clearCboxTempoMap() #clear cbox map but don't touch our own data. + cbox.JackIO.external_tempo(True) + try: + cbox.JackIO.transport_mode(master = False) + except Exception: #"Not a current timebase master" + pass + + + self._updatePlayback() + + def _sanitize(self): + """Inplace modification of self.tempoMap. Remove zeros and convert to float values. """ + self._tempoMap = {int(key):(float(value), timesigNum, timesigDenom) for key, (value, timesigNum, timesigDenom) in self._tempoMap.items() if value > 0.0} + #Don't use the following. Empty tempo maps are allowed, especially in jack transport slave mode. #Instead set a default tempo 120 on init explicitly + #if not self._tempoMap: + #logger.warning("Found invalid tempo map. Forcing to 120 bpm. Please correct manually") + #self._tempoMap = {0, 120.0} + + def _clearCboxTempoMap(self): + """Remove all cbox tempo values by iterating over all of them and set them to None, which is + the secret cbox handshake to delete a tempo change on a specific position. + + Keep our own local data intact.""" + song = cbox.Document.get_song() + for mti in song.status().mtis: #Creates a new temporary list with newly created objects, safe to iterate and delete. + song.delete_mti(mti.pos) + self._updatePlayback() + assert not song.status().mtis, song.status().mtis + + def _sendToCbox(self): + """Send to cbox""" + assert self.isTransportMaster + assert self._tempoMap + song = cbox.Document.get_song() + for pos, (value, timesigNum, timesigDenom) in self._tempoMap.items(): + song.set_mti(pos=pos, tempo=value*self.factor, timesig_denom=timesigDenom , timesig_num=timesigNum) #Tempo changes are fine to happen on the same tick as note on. + #song.set_mti(pos=pos, tempo=value * self.factor) #Tempo changes are fine to happen on the same tick as note on. + self._updatePlayback() + + def setTempoMap(self, tempoMap:dict): + """All-in-one function for outside access""" + if self._tempoMap != tempoMap: + self._tempoMap = tempoMap + self._sanitize() + if self.isTransportMaster: #if not the data will be used later. + self._clearCboxTempoMap() #keeps our own data so it can be send again. + self._sendToCbox() + + def setFactor(self, factor:float): + """Factor is from 1, not from the current one.""" + self.factor = round(factor, 4) + self._sanitize() + self._clearCboxTempoMap() #keeps our own data so it can be send again. + self._sendToCbox() #uses the factor + + def setQuarterNotesPerMinute(self, quarterNotesPerMinute:float): + """Simple tempo setter. Overrides all other tempo data. + Works in tandem with self.setTimeSignature""" + currentValue, timesigNum, timesigDenom = self._tempoMap[0] + self.setTempoMap({0:(quarterNotesPerMinute, timesigNum, timesigDenom)}) + + def setTimeSignature(self, timesigNum:int, timesigDenom:int): + """Simple traditional timesig setter. Overrides all other timesig data. + Works in tandem with self.setTimeSignature. + """ + assert timesigNum > 0, timesigNum + #assert timesigDenom in traditionalNumberToBaseDuration, (timesigDenom, traditionalNumberToBaseDuration) For Laborejo this makes sense, but Patroneo has a fallback option with irregular timesigs: 8 steps in groups of 3 to make a quarter is valid. Results in "8/12" + currentValue, OLD_timesigNum, OLD_timesigDenom = self._tempoMap[0] + self.setTempoMap({0:(currentValue, timesigNum, timesigDenom)}) + + def getQuarterNotesPerMinute(self)->float: + """This assumes there is only one tempo point""" + if self.isTransportMaster: + assert len(self._tempoMap) == 1, len(self._tempoMap) + assert 0 in self._tempoMap, self._tempoMap + return self._tempoMap[0][0] #second [0] is the tuple (tempo, timesig, timesig) + else: + logger.info("Requested Quarter Notes per Minute, but we are not transport master") + return None + + #Save / Load / Export + + def serialize(self)->dict: + """Generate Data to save as json""" + return { + "isTransportMaster" : self.isTransportMaster, + "tempoMap" : self._tempoMap, + } + + @classmethod + def instanceFromSerializedData(cls, parentData, serializedData): + logger.info("Loading TempoMap from saved file") + self = cls.__new__(cls) + self.parentData = parentData + self._tempoMap = serializedData["tempoMap"] #json saves dict-keys as strings. We revert back in sanitize() + self._isTransportMaster = serializedData["isTransportMaster"] + self._processAfterInit() + return self + + def export(self)->dict: + return { + "id" : id(self), + "isTransportMaster" : self.isTransportMaster, + "tempoMap" : self._tempoMap, + "mtis" : cbox.Document.get_song().status().mtis, + } diff --git a/template/engine/session.py b/template/engine/session.py new file mode 100644 index 0000000..4da4803 --- /dev/null +++ b/template/engine/session.py @@ -0,0 +1,185 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +#Python Standard Library +import os +import os.path +import json +import atexit +from sys import exit as sysexit + +#Third Party Modules +from calfbox import cbox + +#Our Template Modules +from .history import History +from .duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, MAXIMUM_TICK_DURATION + +#User Data +from engine.config import * +from engine.main import Data + +class Session(object): + + def __init__(self): + self.sessionPrefix = "" #set in nsm_openOrNewCallback. If we want to save and load resources they need to be in the session dir. We never load from outside, the scheme is always "import first, load local file" + self.absoluteJsonFilePath = "" #set in nsm_openOrNewCallback. Makes it possible to save a file manually. Even in script mode or debug mode. + self.guiWasSavedAsNSMVisible = False #set by nsm_openOrNewCallback, but without a save file we start with an initially hidden GUI + self.nsmClient = None #We get it from api.startEngine which gets it from the GUI. nsmClient.reactToMessage is added to the global event loop there. + self.history = History() #Undo and Redo. Works through the api but is saved in the session. Not saved in the save file. + self.guiSharedDataToSave = {} #the gui can write its values here directly to get them saved and restored on startup. We opt not to use the Qt config save to keep everything in one file. + self.recordingEnabled = False #MidiInput callbacks can use this to prevent/allow data creation. Handled via api callback. Saved. + self.eventLoop = None # added in api.startEngine + self.data = None #nsm_openOrNewCallback + self.standaloneMode = None #fake NSM single server for LaborejoSoftwareSuite programs or not. Set in nsm_openOrNewCallback + + def addSessionPrefix(self, jsonDataAsString:str): + """During load the current session prefix gets added. Turning pseudo-relative paths into + absolute paths. + + This is meant for imported resources, e.g. a soundfont, which need to be handled as abolute + paths while the program is running (e.g. for fluidsynth). It leaves our engine functions + cleaner because they do not know if they are under session management or not. + + It assumes that the session dir does not change during runtime, which is a reasonable + assumption.""" + assert type(jsonDataAsString) is str, type(jsonDataAsString) + assert self.sessionPrefix, self.sessionPrefix + return jsonDataAsString.replace("", self.sessionPrefix) + + def removeSessionPrefix(self, jsonDataAsString:str): + """During saving the current session prefix gets removed from all absolute paths leaving + pseudo-relative paths "" behind. + + This is meant for imported resources, e.g. a soundfont, which need to be handled as abolute + paths while the program is running (e.g. for fluidsynth). It leaves our engine functions + cleaner because they do not know if they are under session management or not. + + It assumes that the session dir does not change during runtime, which is a reasonable + assumption.""" + assert type(jsonDataAsString) is str, type(jsonDataAsString) + return jsonDataAsString.replace(self.sessionPrefix, "") + + def nsm_openOrNewCallback(self, ourPath, sessionName, ourClientNameUnderNSM): + logger.info("New/Open session") + cbox.init_engine("") + + #Most set config must be called before start audio. Set audio outputs seems to be an exception. + cbox.Config.set("io", "client_name", ourClientNameUnderNSM) + cbox.Config.set("io", "enable_common_midi_input", 0) #remove the default "catch all" midi input + cbox.Config.set("io", "outputs", METADATA["cboxOutputs"]) + + #A workaround to have the program adopt to JACK transport position. It already tries but there is no song yet, so it is song-duration=0 and setting transport is not possible beyond song duration. + #We fake a song length which will shortly be set to the real size. + cbox.Document.get_song().set_loop(MAXIMUM_TICK_DURATION, MAXIMUM_TICK_DURATION) + cbox.Document.get_song().update_playback() + + #Now we can start audio with a fake song length + cbox.start_audio() + atexit.register(self.stopSession) #this will handle all python exceptions, but not segfaults of C modules. + + cbox.do_cmd("/master/set_ppqn_factor", None, [D4]) #quarter note has how many ticks? needs to be in a list. + + self.sessionPrefix = ourPath #if we want to save and load resources they need to be in the session dir. We never load from outside, the scheme is always "import first, load local file" + self.absoluteJsonFilePath = os.path.join(ourPath, "save." + METADATA["shortName"] + ".json") + + self.standaloneMode = sessionName == "NOT-A-SESSION" + + try: + self.data = self.openFromJson(self.absoluteJsonFilePath) + except FileNotFoundError: + self.data = None #This makes debugging output nicer. If we init Data() here all errors will be presented as follow-up error "while handling exception FileNotFoundError". + except (NotADirectoryError, PermissionError) as e: + self.data = None + logger.error("Will not load or save because: " + e.__repr__()) + if not self.data: + self.data = Data(parentSession = self) + logger.info("New/Open session complete") + + def openFromJson(self, absoluteJsonFilePath): + logger.info("Loading file start") + with open(absoluteJsonFilePath, "r", encoding="utf-8") as f: + try: + text = self.addSessionPrefix(f.read()) + result = json.loads(text) + except Exception as error: + logger.error(error) + + if result and "version" in result and "origin" in result and result["origin"] == METADATA["url"]: + if METADATA["version"] >= result["version"]: #Achtung. This only works because Python can compare Strings this way! + self.guiWasSavedAsNSMVisible = result["guiWasSavedAsNSMVisible"] + if "recordingEnabled" in result: #introduced in april 2020 + self.recordingEnabled = result["recordingEnabled"] + self.guiSharedDataToSave = result["guiSharedDataToSave"] + assert type(self.guiSharedDataToSave) is dict, self.guiSharedDataToSave + logger.info("Loading file complete") + return Data.instanceFromSerializedData(parentSession=self, serializedData=result) + else: + logger.error(f"""{absoluteJsonFilePath} was saved with {result["version"]} but we need {METADATA["version"]}""") + sysexit() + else: + logger.error(f"""Error. {absoluteJsonFilePath} not loaded. Not a sane {METADATA["name"]} file in json format""") + sysexit() + + + def nsm_saveCallback(self, ourPath, sessionName, ourClientNameUnderNSM): + #not neccessary. NSMClient does that for us. self.nsmClient.announceSaveStatus(True) + try: + if not os.path.exists(ourPath): + os.makedirs(ourPath) + except Exception as e: + logger.error("Will not load or save because: " + e.__repr__()) + + result = self.data.serialize() + result["origin"] = METADATA["url"] + result["version"] = METADATA["version"] + result["recordingEnabled"] = self.recordingEnabled + result["guiWasSavedAsNSMVisible"] = self.nsmClient.isVisible + result["guiSharedDataToSave"] = self.guiSharedDataToSave + #result["savedOn"] = datetime.now().isoformat() #this is inconvenient for git commits. Even a save without no changes will trigger a git diff. + jsonData = json.dumps(result, indent=2) + jsonData = self.removeSessionPrefix(jsonData) + + #if result and jsonData: #make a test here to make sure that the data is not wiped out or corrupted + try: + with open(self.absoluteJsonFilePath, "w", encoding="utf-8") as f: + f.write(jsonData) + except Exception as e: + logger.error("Will not load or save because: " + e.__repr__()) + + logger.info("Saving file complete") + return self.absoluteJsonFilePath + + def stopSession(self): + """This got registered with atexit in the nsm new or open callback above. + will handle all python exceptions, but not segfaults of C modules. """ + logger.info("Starting Quit through @atexit, session.stopSession") + self.eventLoop.stop() + logger.info("@atexit: Event loop stopped") + #Don't do that. We are just a client. + #cbox.Transport.stop() + #logger.info("@atexit: Calfbox Transport stopped ") + cbox.stop_audio() + logger.info("@atexit: Calfbox Audio stopped ") + cbox.shutdown_engine() + logger.info("@atexit: Calfbox Engine shutdown ") diff --git a/template/gitignore.template b/template/gitignore.template new file mode 100644 index 0000000..02c56ef --- /dev/null +++ b/template/gitignore.template @@ -0,0 +1,131 @@ +# Byte-compiled / optimized / DLL files +*__pycache__ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + + +#build process +*.bin +*.build +build/ +Makefile +compiledprefix.py +sitepackages +template/calfbox/.deps +template/calfbox/build +template/calfbox/autom4te.cache +template/calfbox/Makefile.in +template/calfbox/aclocal.m4 +template/calfbox/compile +template/calfbox/config.h +template/calfbox/config.h.in +template/calfbox/config.h.in~ +template/calfbox/config.status +template/calfbox/configure +template/calfbox/depcomp +template/calfbox/install-sh +template/calfbox/ltmain.sh +template/calfbox/missing +template/calfbox/stamp-h1 + + diff --git a/template/helper.py b/template/helper.py new file mode 100644 index 0000000..cc273b5 --- /dev/null +++ b/template/helper.py @@ -0,0 +1,104 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +Most of this file is trivial code and does not reach the "Schaffenshöhe" for Urheberrecht. +If it is: + +This is practically intended as my Public Domain software. +However, there is no such thing as public domain in Germany. +Therefore: + +Released under WTFPL License + +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. +You can redistribute it and/or modify it under the +terms of the Do What The Fuck You Want To Public License, Version 2, +as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. +""" + +def nothing(*args, **kwargs): + pass + +from functools import lru_cache #https://docs.python.org/3.4/library/functools.html#functools.lru_cache +cache_unlimited = lru_cache(maxsize=None) +#use as @cache_unlimited decorator + +import itertools +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + +def onlyOne(iterable): + true_found = False + for v in iterable: + if v: + # a True was found! + if true_found: + # found too many True's + return False + else: + # found the first True + true_found = True + # found zero or one True value + return true_found + +class EndlessGenerator(object): + """used to turn a single + parameter into an endless list of paramters. + + DO NOT for loop over this object. + It will never stop. Use it with next(instance)""" + def __init__(self, value): + self.value = value + + def __iter__(self): + return self + + def __next__(self): + return self.value + +def flatList(lst): + """Take a nested list of any depth and make it a flat, plain + list with only one level""" + for elem in lst: + if type(elem) in (list, tuple): + for i in flatList(elem): + yield i + else: + yield elem + +def compress(input, inputLowest, inputHighest, outputLowest, outputHighest): + return (input-inputLowest) / (inputHighest-inputLowest) * (outputHighest-outputLowest) + outputLowest + +def listToUniqueKeepOrder(seq): + seen = set() + seen_add = seen.add + return [x for x in seq if not (x in seen or seen_add(x))] + + +def padhexa(self, s:str): + """Like zfill, but for hex number strings""" + return '0x' + s[2:].zfill(2) + + +import inspect +from pprint import pprint +def whoCalled(): + """If you want to use this only once, better use breakpoint()""" + pprint (inspect.stack()) + print() + + +def provokecrash(): + """Obviously for testing""" + import ctypes + p = ctypes.pointer(ctypes.c_char.from_address(5)) + p[0] = b'x' + + diff --git a/template/launcher.template b/template/launcher.template new file mode 100755 index 0000000..16141bd --- /dev/null +++ b/template/launcher.template @@ -0,0 +1,18 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +#Symlink this to root dir as the program name without extension + +import os +import sys + +#print (__file__) #this reports the name of the symlink. +os.chdir(os.path.dirname(__file__)) +sys.path.insert(0, os.getcwd()) + +from template import start #Executes various start up checks and sets up our environment likes search paths + +from qtgui import mainwindow #which in turn imports the engine and starts the engine +with start.profiler(): + mainwindow.MainWindow().qtApp.exec_() + #Program is over. Code here does not get executed. Quit is done via NSM in mainWindow._nsmQuit diff --git a/template/main.py.template b/template/main.py.template new file mode 100644 index 0000000..6eccb19 --- /dev/null +++ b/template/main.py.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# file: __main__.py + +""" +Symlink this file to root __main__.py + +This file is an alternativ to the python launcher ./$(PROGRAM) when using zipapp. +Any code changes there must be manually recplicated here. + +We do NOT need to change the working dir here, as in the direct launcher. +python zipapp uses indeed zip and will resolve symlinks to real files, thus this will become +a real file in the right place, resulting in the right cwd. +""" + +if __name__ == '__main__': + from template import start #Executes various start up checks and sets up our environment likes search paths + from qtgui import mainwindow #which in turn imports the engine and starts the engine + with start.profiler(): + mainwindow.MainWindow().qtApp.exec_() + #Program is over. Code here does not get executed. Quit is done via NSM in mainWindow._nsmQuit + diff --git a/template/qtgui/__init__.py b/template/qtgui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/template/qtgui/about.py b/template/qtgui/about.py new file mode 100644 index 0000000..b37931e --- /dev/null +++ b/template/qtgui/about.py @@ -0,0 +1,173 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +#Standard Lib +from random import choice + +#System Wide Modules +from PyQt5 import QtCore, QtWidgets, QtGui + +#Template Moduiles +from .designer.about import Ui_TemplateAbout + +#Client Modules +from engine.config import * #imports METADATA +from qtgui.resources import * #Has the logo +import engine.api as api #Already loaded, will not change anything + + + +class About(QtWidgets.QDialog): + """A help window with useful tips. + Also the nagscreen for Donations. + + The didYouKnow sentences stay here to be edited and not in engine/constants.py? + They are GUI only and need to be translated + + The About dialog depends on a set key guiSharedDataToSave["showAboutDialog"]. That is usually + created in the mainWindow for a new instance, never saved, and intial value depends on + METADATA["showAboutDialogFirstStart"] + """ + + didYouKnow = [] #Will be filled by the non-template part of the program + + def __init__(self, mainWindow): + #super(DidYouKnow, self).__init__(parent, QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.CustomizeWindowHint|QtCore.Qt.X11BypassWindowManagerHint) + #super(DidYouKnow, self).__init__(parent, QtCore.Qt.FramelessWindowHint) + + super().__init__(parent=mainWindow) + self.setModal(True) #block until closed + self.ui = Ui_TemplateAbout() + self.ui.setupUi(self) + self.mainWindow = mainWindow + + self.ui.didyouknowLabel.setText(choice(self.tricks())) + self.index = self.tricks().index(self.ui.didyouknowLabel.text()) + + self.ui.numberSlider.setMaximum(len(self.tricks())-1) + self.ui.numberSlider.setValue(self.index) + + self.ui.numberLabel.setText("Tip " + str(self.index+1) + "/" + str(len(self.tricks()))) + + self.ui.numberSlider.valueChanged.connect(self.moveSlider) + + copyright = f"""{METADATA["author"]} ({METADATA["year"]})""" + name = f"""{METADATA["name"]} ver. {METADATA["version"]}""" + aboutText = "\n".join([name, copyright, METADATA["url"]]) + self.ui.aboutLabel.setText(aboutText) + + #Image: 300x151 + aboutLogoPixmap = QtGui.QPixmap(":aboutlogo.png") + #pixmap_scaled = aboutLogoPixmap.scaled(self.ui.goldenratioLabel.size(), QtCore.Qt.KeepAspectRatio) + #self.ui.goldenratioLabel.setPixmap(pixmap_scaled) + self.ui.goldenratioLabel.setPixmap(aboutLogoPixmap) + + """ + #We don't want the user to get bombarded with information on the first start. + #Only show us the second time. + if not "showAboutDialog" in api.session.guiSharedDataToSave: + self.ui.showOnStartup.hide() + api.session.guiSharedDataToSave["showAboutDialog"] = True + """ + + self.ui.showOnStartup.stateChanged.connect(self.saveStartupState) + + """ + QAbstractSlider.SliderNoAction 0 + QAbstractSlider.SliderSingleStepAdd 1 + QAbstractSlider.SliderSingleStepSub 2 + QAbstractSlider.SliderPageStepAdd 3 + QAbstractSlider.SliderPageStepSub 4 + QAbstractSlider.SliderToMinimum 5 + QAbstractSlider.SliderToMaximum 6 + QAbstractSlider.SliderMove 7 + """ + self.shortcut("right", lambda: self.ui.numberSlider.triggerAction(1)) + self.shortcut("left", lambda: self.ui.numberSlider.triggerAction(2)) + self.shortcut("up", lambda: self.ui.numberSlider.triggerAction(1)) + self.shortcut("down", lambda: self.ui.numberSlider.triggerAction(2)) + + + self.ui.numberSlider.wheelEvent = self.mouseWheelEventCustom #Deactivate the normal number slider wheel event + self.wheelEvent = self.mouseWheelEventCustom #Use a window wide one that is easier to control + + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + self.ui.showOnStartup.setChecked(settings.value("showAboutDialog", type=bool)) + + self.ui.numberSlider.setFocus(True) + + + def tricks(self): + """For some reason translations do not work if saved as class variable. + A getter function works""" + + return About.didYouKnow + [ + #Make the first three words matter! + #Do not start them all with "You can..." or "...that you can", in response to the Did you know? title. + QtCore.QCoreApplication.translate("TemplateAbout", "Help can be found through the Laborejo Community"), + QtCore.QCoreApplication.translate("TemplateAbout", "Temporary Shortcuts can be created by hovering the mouse cursor over a menu entry (with no shortcut) and pressing a numpad key"), + QtCore.QCoreApplication.translate("TemplateAbout", "Closing the program with the [X] icon or shortcuts like Alt+F4 will only hide the window. This state will be saved, so you don't need to see the window each time you load your session."), + ] + + def reject(self): + self.hide() + + def showEvent(self, event): + """The qt main loop is slow. We can't show the about dialog, or any other sub-dialog, + right after mainWindow.show() becauer the event loop is not ready and the screen positions + will be wrong. + + Instead we wait that the showEvent actually arrives. + And even then we wait a few ms, to be on the safe side. That was actually needed on the + devs system. + + Also the dialog is sometimes not closed but hidden. + Also we had reports of not correctly parented and centered dialogs. + + Make sure everything is where it should be""" + parentCenter = self.mainWindow.geometry().center() + aboutCenter = self.geometry().center() + self.move(parentCenter - aboutCenter) + event.accept() + + def mouseWheelEventCustom(self, event): + event.accept() + if event.angleDelta().y() > 0: + self.ui.numberSlider.triggerAction(1) + else: + self.ui.numberSlider.triggerAction(2) + + def shortcut(self, key, function): + act = QtWidgets.QAction('') + act.setShortcut(key) + act.triggered.connect(function) + self.addAction(act) + + def saveStartupState(self): + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + settings.setValue("showAboutDialog", bool(self.ui.showOnStartup.checkState())) + + def moveSlider(self): + nowIdx = self.ui.numberSlider.value() + self.ui.didyouknowLabel.setText(self.tricks()[nowIdx]) + self.ui.numberLabel.setText("Trick " + str(nowIdx+1) + "/" + str(len(self.tricks()))) diff --git a/template/qtgui/chooseSessionDirectory.py b/template/qtgui/chooseSessionDirectory.py new file mode 100644 index 0000000..2099c21 --- /dev/null +++ b/template/qtgui/chooseSessionDirectory.py @@ -0,0 +1,120 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Standard Lib +from tempfile import gettempdir +from pathlib import Path +import os.path +from os import makedirs + +#System Wide Modules +from PyQt5 import QtCore, QtWidgets, QtGui + +#Template Moduiles +from .designer.chooseSessionDirectory import Ui_TemplateChooseSessionDirectory +from .resources import * #has the translation + +#Client Modules +from engine.config import * #imports METADATA +from qtgui.resources import * #Has the logo + + +class ChooseSessionDirectory(QtWidgets.QDialog): + def __init__(self, qtApp): + + language = QtCore.QLocale().languageToString(QtCore.QLocale().language()) + logger.info("{}: Language set to {}".format(METADATA["name"], language)) + if language in METADATA["supportedLanguages"]: + templateTranslator = QtCore.QTranslator() + templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL + qtApp.installTranslator(templateTranslator) + + super().__init__() #no parent, this is the top level window at this time. + self.qtApp = qtApp + self.setModal(True) #block until closed + self.ui = Ui_TemplateChooseSessionDirectory() + self.ui.setupUi(self) + + copyright = f"""{METADATA["author"]} ({METADATA["year"]})""" + name = f"""{METADATA["name"]} ver. {METADATA["version"]}""" + aboutText = "\n".join([name, copyright ,METADATA["url"]]) + self.ui.aboutLabel.setText(aboutText) + + #Image: 300x151 + aboutLogoPixmap = QtGui.QPixmap(":aboutlogo.png") + pixmap_scaled = aboutLogoPixmap.scaled(self.ui.goldenratioLabel.size(), QtCore.Qt.KeepAspectRatio) + self.ui.goldenratioLabel.setPixmap(pixmap_scaled) + + message = QtCore.QCoreApplication.translate("TemplateChooseSessionDirectory", "Please choose a directory for your session files. It is recommended to start through Agordejo/New Session Manager instead.") + self.ui.label.setText(message) + + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + self.recentDirList = [] + if settings.contains("recentDirectoriesWithouNSM"): + self.recentDirList = settings.value("recentDirectoriesWithouNSM", type=list)[-5:] + #remember self.recentDirList for later saving + self.ui.pathComboBox.insertItems(0, reversed(self.recentDirList)) + else: + self.ui.pathComboBox.setCurrentText(gettempdir()) + + self.ui.buttonBox.accepted.connect(self.accept) + self.ui.buttonBox.rejected.connect(self.reject) + + self.ui.openFileDialogButton.setText("") + self.ui.openFileDialogButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogOpenButton"))) + self.ui.openFileDialogButton.clicked.connect(self.requestPathFromDialog) + self.exec() + + + def requestPathFromDialog(self): + if self.ui.pathComboBox.currentText() == gettempdir(): + startPath = str(Path.home()) + else: + startPath = self.ui.pathComboBox.currentText() + dirname = QtWidgets.QFileDialog.getExistingDirectory(self, QtCore.QCoreApplication.translate("TemplateChooseSessionDirectory", "Choose Session Directory"), startPath) + + if dirname: + self.ui.pathComboBox.setCurrentText(dirname) + + def accept(self): + self.path = self.ui.pathComboBox.currentText() #easy abstraction so that the caller does not need to know our widget name + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if not os.path.exists(self.path): + try: + makedirs(self.path) + except: + pass #file saving error logging is handled later + + #There is no guarantee that the dir really exists. but at this point the user is on its own. + #It is allowed to use /dev/null after all + + if not self.path in self.recentDirList: + self.recentDirList.append(self.path) + settings.setValue("recentDirectoriesWithouNSM", self.recentDirList) + super().accept() + + def reject(self): + self.path = None + super().reject() + diff --git a/template/qtgui/constantsAndConfigs.py b/template/qtgui/constantsAndConfigs.py new file mode 100644 index 0000000..9fdd66a --- /dev/null +++ b/template/qtgui/constantsAndConfigs.py @@ -0,0 +1,170 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +Laborejo2 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Third Party Modules +from PyQt5 import QtWidgets, QtCore, QtGui + +#Template Modules +from .resources import * + +#Client Modules +import engine.api as api + + +class ConstantsAndConfigs(object): + """Handles shared widgets like the Grid and ScoreView with Zoom and Stretch""" + + def __init__(self): + self.ticksToPixelRatio = None #Must be set by the client ConstantsAndConfigs! + self.gridRhythm = api.D4 #Default value is quarter. The QSettings save and reload this on a per-file basis. + self.gridOpacity = 0.1 #this is only the default value. Once changed a QSetting will be used instead. + self.followPlayhead = True #camera follows the playhead during playback. + self.duringPlayback = False #set by a callback. + self.velocityToPixelRatio = 2 #2.0 is the default. 128 pixels maximum. + + self._snapToGrid = True # For CC point and Tempo point placements + self.snapToGridCallbacks = [] # see ccViewCallbacks + + self.zoomFactor = 1 # #Initial Zoom Level. Also hardcoded into scoreView.zoomNull. Lower means bigger items. + self.maximumZoomOut = 0.25 + + #fonts which are used in the QGraphicsScenes. They are special because they don't scale with the DPI. + self.fontDB = QtGui.QFontDatabase() + #fid = self.fontDB.addApplicationFont("gui/resources/freesans.ttf") + #if fid == -1: + # raise ValueError("Freesans.ttf not loaded ") + + fid = self.fontDB.addApplicationFont(":template/euterpe.ttf") + if fid == -1: + raise ValueError("euterpe.ttf not loaded. Make sure resources were generated") + + #For easier programWide access move these to constantsAndConfigs + #constantsAndConfigs.theFont = self.fontDB.font("FreeSans", "", 13) + #constantsAndConfigs.theFont.setPixelSize(13) #It is very important to set the pixel size before setting the font to the text + self.musicFont = self.fontDB.font("Euterpe", "", 13) + self.musicFont.setPixelSize(13) #It is very important to set the pixel size before setting the font to the text + + self.dynamics = ["f", "ff", "p", "pp", "mf", "mp", "tacet"] + + self.prettyRhythms = [ #list because we need item order + (api.DL, "0.25 Longa"), + (api.DB, "0.5 Brevis"), + (api.D1, "1 Whole"), + (api.D2, "2 Half"), + (api.D4, "4 Quarter"), + (api.D8, "8 Eigth"), + (api.D16, "16 Sixteenth"), + (api.D32, "32 Thirthy-Second"), + (api.D64, "64 Sixty-Fourth"), + (api.D128, "128 Hundred Twenty-Eighth "), + (api.D256, "256 Two-hundred Fifty-Sixth"), + ] + self.prettyRhythmsStrings = [v for k,v in self.prettyRhythms] + self.prettyRhythmsValues = [k for k,v in self.prettyRhythms] + + self.prettyExtendedRhythms = [ #list because we need item order + (api.DL, "0.25 Longa"), + (api.DB, "0.5 Brevis"), + (api.D1, "1 Whole"), + (api.D2, "2 Half "), + (api.D4, "4 Quarter"), + (api.D8, "8 Eigth"), + (api.D16, "16 Sixteenth"), + (api.D32, "32 Thirthy-Second"), + (api.D64, "64 Sixty-Fourth"), + (api.D128, "128 Hundred Twenty-Eighth"), + + (1.5*api.DL, "Dotted 0.25 Longa"), + (1.5*api.DB, "Dotted 0.5 Brevis"), + (1.5*api.D1, "Dotted 1 Whole"), + (1.5*api.D2, "Dotted 2 Half"), + (1.5*api.D4, "Dotted 4 Quarter"), + (1.5*api.D8, "Dotted 8 Eigth "), + (1.5*api.D16, "Dotted 16 Sixteenth"), + (1.5*api.D32, "Dotted 32 Thirthy-Second"), + (1.5*api.D64, "Dotted 64 Sixty-Fourth "), + (1.5*api.D128, "Dotted 128 Hundred Twenty-Eighth "), + + ] + self.prettyExtendedRhythmsStrings = [v for k,v in self.prettyExtendedRhythms] + self.prettyExtendedRhythmsValues = [k for k,v in self.prettyExtendedRhythms] + + #use with constantsAndConfigs.musicFont + self.realNoteDisplay = { + api.DB : "𝅜", + api.D1 : "𝅝", + api.D2 : "𝅗𝅥", + api.D4 : "𝅘𝅥", + api.D8 : "𝅘𝅥𝅮", + api.D16 : "𝅘𝅥𝅯", + api.D32 : "𝅘𝅥𝅰", + api.D64 : "𝅘𝅥𝅱", + api.D128 : "𝅘𝅥𝅲", + + 1.5 * api.DB : "𝅜𝅭", #dotted DB + 1.5 * api.D1 : "𝅝𝅭", #dotted D1 + 1.5 * api.D2 : "𝅗𝅥𝅭", #dotted D2 + 1.5 * api.D4 : "𝅘𝅥𝅭", #dotted D4 + 1.5 * api.D8 : "𝅘𝅥𝅮𝅭", #dotted D8 + 1.5 * api.D16 : "𝅘𝅥𝅯𝅭", #dotted D16 + 1.5 * api.D32 : "𝅘𝅥𝅰𝅭", #dotted D32 + 1.5 * api.D64 : "𝅘𝅥𝅱𝅭", #dotted D64 + 1.5 * api.D128 : "𝅘𝅥𝅲𝅭", #dotted D128 + + 2.25 * api.DB : "𝅜𝅭", #double dotted DB + 2.25 * api.D1 : "𝅝𝅭", #double dotted D1 + 2.25 * api.D2 : "𝅗𝅥𝅭", #double dotted D2 + 2.25 * api.D4 : "𝅘𝅥𝅭", #double dotted D4 + 2.25 * api.D8 : "𝅘𝅥𝅮𝅭", #double dotted D8 + 2.25 * api.D16 : "𝅘𝅥𝅯𝅭", #double dotted D16 + 2.25 * api.D32 : "𝅘𝅥𝅰𝅭", #double dotted D32 + 2.25 * api.D64 : "𝅘𝅥𝅱𝅭", #double dotted D64 + 2.25 * api.D128 : "𝅘𝅥𝅲𝅭", #double dotted D128 + } + + + self.commonNotes = [ #list because we need item order + (api.D1, "𝅝"), #D1 + (api.D2 , "𝅗𝅥"), #D2 + (api.D4 , "𝅘𝅥"), #D4 + (api.D8 , "𝅘𝅥𝅮"), #D8 + + (1.5 * api.D1 , "𝅝𝅭"), #dotted D1 + (1.5 * api.D2 , "𝅗𝅥𝅭"), #dotted D2 + (1.5 * api.D4 , "𝅘𝅥𝅭"), #dotted D4 + (1.5 * api.D8 , "𝅘𝅥𝅮𝅭"), #dotted D8 + ] + + self.realNotesStrings = [v for k,v in self.commonNotes] + self.realNotesValues = [k for k,v in self.commonNotes] + + @property + def snapToGrid(self): + return self._snapToGrid + + @snapToGrid.setter + def snapToGrid(self, value): + self._snapToGrid = value + for func in self.snapToGridCallbacks: + func() diff --git a/template/qtgui/debugScript.py b/template/qtgui/debugScript.py new file mode 100644 index 0000000..0cb6064 --- /dev/null +++ b/template/qtgui/debugScript.py @@ -0,0 +1,72 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +import os.path +#from code import InteractiveInterpreter +import engine.api as api + +class DebugScriptRunner(object): + """We do not provide our own text editor in the GUI. Instead the user must edit debugscript.py + in our NSM Session directory, save and then self.run""" + + def __init__(self, apilocals): + super().__init__() + self.apilocals = apilocals + self.absoluteScriptFilePath = None + + def trueInit(self, nsmClient): + assert nsmClient + self.nsmClient = nsmClient + self.absoluteScriptFilePath = os.path.join(self.nsmClient.ourPath, "debugscript.py") + + def _createEmptyDebugScript(self): + assert self.absoluteScriptFilePath + logger.info(f"{self.nsmClient.ourClientNameUnderNSM}: Script file not found. Initializing: {self.absoluteScriptFilePath}") + text = ("""#! /usr/bin/env python3""" + "\n" + """# -*- coding: utf-8 -*-""" + "\n" + "#This file is in the Qt-MainWindow (-> self) scope.\n#You can also call api commands which will trigger a GUI callback. Like api.undo(), api.redo() etc." + "\n" + """print ("Hello World")""" + "\n" + ) + + with open(self.absoluteScriptFilePath, "w", encoding="utf-8") as f: + f.write(text) + + def run(self): + if not os.path.exists(self.absoluteScriptFilePath): + if not os.path.exists(self.nsmClient.ourPath): + os.makedirs(self.nsmClient.ourPath) + self._createEmptyDebugScript() + + try: + exec(compile(open(self.absoluteScriptFilePath).read(), filename=self.absoluteScriptFilePath, mode="exec"), globals(), self.apilocals) + except Exception as e: + print (e) + + #with open(self.absoluteScriptFilePath, "r", encoding="utf-8") as f: + # source = f.read() + #self.runsource(source=source, filename=self.absoluteScriptFilePath) diff --git a/template/qtgui/designer/about.py b/template/qtgui/designer/about.py new file mode 100644 index 0000000..462dfad --- /dev/null +++ b/template/qtgui/designer/about.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'about.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_TemplateAbout(object): + def setupUi(self, TemplateAbout): + TemplateAbout.setObjectName("TemplateAbout") + TemplateAbout.resize(374, 436) + TemplateAbout.setMinimumSize(QtCore.QSize(374, 436)) + TemplateAbout.setMaximumSize(QtCore.QSize(374, 436)) + self.verticalLayout = QtWidgets.QVBoxLayout(TemplateAbout) + self.verticalLayout.setObjectName("verticalLayout") + self.widget = QtWidgets.QWidget(TemplateAbout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.widget.setMinimumSize(QtCore.QSize(0, 151)) + self.widget.setMaximumSize(QtCore.QSize(16777215, 151)) + self.widget.setObjectName("widget") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.goldenratioLabel = QtWidgets.QLabel(self.widget) + self.goldenratioLabel.setMinimumSize(QtCore.QSize(300, 151)) + self.goldenratioLabel.setMaximumSize(QtCore.QSize(300, 151)) + self.goldenratioLabel.setText("") + self.goldenratioLabel.setAlignment(QtCore.Qt.AlignCenter) + self.goldenratioLabel.setObjectName("goldenratioLabel") + self.horizontalLayout_3.addWidget(self.goldenratioLabel) + self.verticalLayout.addWidget(self.widget) + self.layoutWidget = QtWidgets.QWidget(TemplateAbout) + self.layoutWidget.setObjectName("layoutWidget") + self.formLayout = QtWidgets.QFormLayout(self.layoutWidget) + self.formLayout.setContentsMargins(-1, -1, -1, 0) + self.formLayout.setObjectName("formLayout") + self.frame_2 = QtWidgets.QFrame(self.layoutWidget) + self.frame_2.setMinimumSize(QtCore.QSize(0, 0)) + self.frame_2.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Sunken) + self.frame_2.setLineWidth(0) + self.frame_2.setObjectName("frame_2") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_2) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.numberLabel = QtWidgets.QLabel(self.frame_2) + self.numberLabel.setText("Trick 0/10") + self.numberLabel.setObjectName("numberLabel") + self.horizontalLayout.addWidget(self.numberLabel) + self.numberSlider = QtWidgets.QSlider(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.numberSlider.sizePolicy().hasHeightForWidth()) + self.numberSlider.setSizePolicy(sizePolicy) + self.numberSlider.setFocusPolicy(QtCore.Qt.ClickFocus) + self.numberSlider.setOrientation(QtCore.Qt.Horizontal) + self.numberSlider.setObjectName("numberSlider") + self.horizontalLayout.addWidget(self.numberSlider) + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.frame_2) + self.groupBox = QtWidgets.QGroupBox(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) + self.groupBox.setSizePolicy(sizePolicy) + self.groupBox.setMinimumSize(QtCore.QSize(0, 150)) + self.groupBox.setMaximumSize(QtCore.QSize(16777215, 150)) + self.groupBox.setObjectName("groupBox") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.didyouknowLabel = QtWidgets.QLabel(self.groupBox) + self.didyouknowLabel.setText("This is an example text. If it gets too long you get line breaks.") + self.didyouknowLabel.setWordWrap(True) + self.didyouknowLabel.setOpenExternalLinks(True) + self.didyouknowLabel.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.didyouknowLabel.setObjectName("didyouknowLabel") + self.verticalLayout_2.addWidget(self.didyouknowLabel) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem) + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.groupBox) + self.verticalLayout.addWidget(self.layoutWidget) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setContentsMargins(-1, 0, -1, -1) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.showOnStartup = QtWidgets.QCheckBox(TemplateAbout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.showOnStartup.sizePolicy().hasHeightForWidth()) + self.showOnStartup.setSizePolicy(sizePolicy) + self.showOnStartup.setFocusPolicy(QtCore.Qt.ClickFocus) + self.showOnStartup.setChecked(True) + self.showOnStartup.setObjectName("showOnStartup") + self.horizontalLayout_2.addWidget(self.showOnStartup) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem1) + self.closeButton = QtWidgets.QDialogButtonBox(TemplateAbout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.closeButton.sizePolicy().hasHeightForWidth()) + self.closeButton.setSizePolicy(sizePolicy) + self.closeButton.setFocusPolicy(QtCore.Qt.WheelFocus) + self.closeButton.setOrientation(QtCore.Qt.Horizontal) + self.closeButton.setStandardButtons(QtWidgets.QDialogButtonBox.Close) + self.closeButton.setCenterButtons(False) + self.closeButton.setObjectName("closeButton") + self.horizontalLayout_2.addWidget(self.closeButton) + self.verticalLayout.addLayout(self.horizontalLayout_2) + self.widget_2 = QtWidgets.QWidget(TemplateAbout) + self.widget_2.setObjectName("widget_2") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_2) + self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_4.setSpacing(0) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.aboutLabel = QtWidgets.QLabel(self.widget_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.aboutLabel.sizePolicy().hasHeightForWidth()) + self.aboutLabel.setSizePolicy(sizePolicy) + self.aboutLabel.setText("Laborejo URL and other Data will be displayed here") + self.aboutLabel.setTextFormat(QtCore.Qt.AutoText) + self.aboutLabel.setAlignment(QtCore.Qt.AlignCenter) + self.aboutLabel.setWordWrap(True) + self.aboutLabel.setOpenExternalLinks(True) + self.aboutLabel.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.aboutLabel.setObjectName("aboutLabel") + self.horizontalLayout_4.addWidget(self.aboutLabel) + self.verticalLayout.addWidget(self.widget_2) + + self.retranslateUi(TemplateAbout) + self.closeButton.accepted.connect(TemplateAbout.accept) + self.closeButton.rejected.connect(TemplateAbout.reject) + QtCore.QMetaObject.connectSlotsByName(TemplateAbout) + + def retranslateUi(self, TemplateAbout): + _translate = QtCore.QCoreApplication.translate + TemplateAbout.setWindowTitle(_translate("TemplateAbout", "About")) + self.groupBox.setTitle(_translate("TemplateAbout", "Did you know...?")) + self.showOnStartup.setText(_translate("TemplateAbout", "show on startup")) + diff --git a/template/qtgui/designer/about.ui b/template/qtgui/designer/about.ui new file mode 100644 index 0000000..76bbde2 --- /dev/null +++ b/template/qtgui/designer/about.ui @@ -0,0 +1,355 @@ + + + TemplateAbout + + + + 0 + 0 + 374 + 436 + + + + + 374 + 436 + + + + + 374 + 436 + + + + About + + + + + + + 0 + 0 + + + + + 0 + 151 + + + + + 16777215 + 151 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 300 + 151 + + + + + 300 + 151 + + + + + + + Qt::AlignCenter + + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Sunken + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Trick 0/10 + + + + + + + + 0 + 0 + + + + Qt::ClickFocus + + + Qt::Horizontal + + + + + + + + + + + 0 + 0 + + + + + 0 + 150 + + + + + 16777215 + 150 + + + + Did you know...? + + + + + + This is an example text. If it gets too long you get line breaks. + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + Qt::ClickFocus + + + show on startup + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Qt::WheelFocus + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + false + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Laborejo URL and other Data will be displayed here + + + Qt::AutoText + + + Qt::AlignCenter + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + closeButton + accepted() + TemplateAbout + accept() + + + 248 + 254 + + + 157 + 274 + + + + + closeButton + rejected() + TemplateAbout + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/template/qtgui/designer/chooseSessionDirectory.py b/template/qtgui/designer/chooseSessionDirectory.py new file mode 100644 index 0000000..fc786e5 --- /dev/null +++ b/template/qtgui/designer/chooseSessionDirectory.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'chooseSessionDirectory.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_TemplateChooseSessionDirectory(object): + def setupUi(self, TemplateChooseSessionDirectory): + TemplateChooseSessionDirectory.setObjectName("TemplateChooseSessionDirectory") + TemplateChooseSessionDirectory.resize(374, 436) + TemplateChooseSessionDirectory.setMinimumSize(QtCore.QSize(374, 436)) + TemplateChooseSessionDirectory.setMaximumSize(QtCore.QSize(374, 436)) + self.verticalLayout = QtWidgets.QVBoxLayout(TemplateChooseSessionDirectory) + self.verticalLayout.setObjectName("verticalLayout") + self.widget = QtWidgets.QWidget(TemplateChooseSessionDirectory) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.widget.setMinimumSize(QtCore.QSize(0, 151)) + self.widget.setMaximumSize(QtCore.QSize(16777215, 151)) + self.widget.setObjectName("widget") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.goldenratioLabel = QtWidgets.QLabel(self.widget) + self.goldenratioLabel.setMinimumSize(QtCore.QSize(300, 151)) + self.goldenratioLabel.setMaximumSize(QtCore.QSize(300, 151)) + self.goldenratioLabel.setText("") + self.goldenratioLabel.setAlignment(QtCore.Qt.AlignCenter) + self.goldenratioLabel.setObjectName("goldenratioLabel") + self.horizontalLayout_3.addWidget(self.goldenratioLabel) + self.verticalLayout.addWidget(self.widget) + self.layoutWidget = QtWidgets.QWidget(TemplateChooseSessionDirectory) + self.layoutWidget.setObjectName("layoutWidget") + self.formLayout = QtWidgets.QFormLayout(self.layoutWidget) + self.formLayout.setContentsMargins(-1, -1, -1, 0) + self.formLayout.setObjectName("formLayout") + self.label = QtWidgets.QLabel(self.layoutWidget) + self.label.setWordWrap(True) + self.label.setObjectName("label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.label) + self.frame_2 = QtWidgets.QFrame(self.layoutWidget) + self.frame_2.setMinimumSize(QtCore.QSize(0, 0)) + self.frame_2.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Sunken) + self.frame_2.setLineWidth(0) + self.frame_2.setObjectName("frame_2") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_2) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.frame_2) + self.verticalLayout.addWidget(self.layoutWidget) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.pathComboBox = QtWidgets.QComboBox(TemplateChooseSessionDirectory) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.pathComboBox.sizePolicy().hasHeightForWidth()) + self.pathComboBox.setSizePolicy(sizePolicy) + self.pathComboBox.setEditable(True) + self.pathComboBox.setObjectName("pathComboBox") + self.horizontalLayout_2.addWidget(self.pathComboBox) + self.openFileDialogButton = QtWidgets.QPushButton(TemplateChooseSessionDirectory) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.openFileDialogButton.sizePolicy().hasHeightForWidth()) + self.openFileDialogButton.setSizePolicy(sizePolicy) + self.openFileDialogButton.setFlat(False) + self.openFileDialogButton.setObjectName("openFileDialogButton") + self.horizontalLayout_2.addWidget(self.openFileDialogButton) + self.verticalLayout.addLayout(self.horizontalLayout_2) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.buttonBox = QtWidgets.QDialogButtonBox(TemplateChooseSessionDirectory) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + self.widget_2 = QtWidgets.QWidget(TemplateChooseSessionDirectory) + self.widget_2.setObjectName("widget_2") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_2) + self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_4.setSpacing(0) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.aboutLabel = QtWidgets.QLabel(self.widget_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.aboutLabel.sizePolicy().hasHeightForWidth()) + self.aboutLabel.setSizePolicy(sizePolicy) + self.aboutLabel.setText("Laborejo URL and other Data will be displayed here") + self.aboutLabel.setTextFormat(QtCore.Qt.AutoText) + self.aboutLabel.setAlignment(QtCore.Qt.AlignCenter) + self.aboutLabel.setWordWrap(True) + self.aboutLabel.setOpenExternalLinks(True) + self.aboutLabel.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.aboutLabel.setObjectName("aboutLabel") + self.horizontalLayout_4.addWidget(self.aboutLabel) + self.verticalLayout.addWidget(self.widget_2) + + self.retranslateUi(TemplateChooseSessionDirectory) + QtCore.QMetaObject.connectSlotsByName(TemplateChooseSessionDirectory) + + def retranslateUi(self, TemplateChooseSessionDirectory): + _translate = QtCore.QCoreApplication.translate + TemplateChooseSessionDirectory.setWindowTitle(_translate("TemplateChooseSessionDirectory", "Choose Session Directory")) + self.label.setText(_translate("TemplateChooseSessionDirectory", "Here will be a text that we need a directory to save to. The program name will be included so it needs to be set programatically.")) + self.openFileDialogButton.setText(_translate("TemplateChooseSessionDirectory", "Choose Directory")) + + diff --git a/template/qtgui/designer/chooseSessionDirectory.ui b/template/qtgui/designer/chooseSessionDirectory.ui new file mode 100644 index 0000000..18c06fc --- /dev/null +++ b/template/qtgui/designer/chooseSessionDirectory.ui @@ -0,0 +1,248 @@ + + + TemplateChooseSessionDirectory + + + + 0 + 0 + 374 + 436 + + + + + 374 + 436 + + + + + 374 + 436 + + + + Choose Session Directory + + + + + + + 0 + 0 + + + + + 0 + 151 + + + + + 16777215 + 151 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 300 + 151 + + + + + 300 + 151 + + + + + + + Qt::AlignCenter + + + + + + + + + + + 0 + + + + + Here will be a text that we need a directory to save to. The program name will be included so it needs to be set programatically. + + + true + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Sunken + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + Choose Directory + + + false + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Laborejo URL and other Data will be displayed here + + + Qt::AutoText + + + Qt::AlignCenter + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + diff --git a/template/qtgui/designer/usermanual.py b/template/qtgui/designer/usermanual.py new file mode 100644 index 0000000..53aaae9 --- /dev/null +++ b/template/qtgui/designer/usermanual.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'usermanual.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_TemplateUserManual(object): + def setupUi(self, TemplateUserManual): + TemplateUserManual.setObjectName("TemplateUserManual") + TemplateUserManual.resize(776, 733) + self.verticalLayout = QtWidgets.QVBoxLayout(TemplateUserManual) + self.verticalLayout.setObjectName("verticalLayout") + self.widget = QtWidgets.QWidget(TemplateUserManual) + self.widget.setObjectName("widget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.home = QtWidgets.QPushButton(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.home.sizePolicy().hasHeightForWidth()) + self.home.setSizePolicy(sizePolicy) + self.home.setFlat(False) + self.home.setObjectName("home") + self.horizontalLayout.addWidget(self.home) + self.back = QtWidgets.QPushButton(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.back.sizePolicy().hasHeightForWidth()) + self.back.setSizePolicy(sizePolicy) + self.back.setShortcut("Backspace") + self.back.setObjectName("back") + self.horizontalLayout.addWidget(self.back) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.verticalLayout.addWidget(self.widget) + self.textBrowser = QtWidgets.QTextBrowser(TemplateUserManual) + self.textBrowser.setObjectName("textBrowser") + self.verticalLayout.addWidget(self.textBrowser) + + self.retranslateUi(TemplateUserManual) + QtCore.QMetaObject.connectSlotsByName(TemplateUserManual) + + def retranslateUi(self, TemplateUserManual): + _translate = QtCore.QCoreApplication.translate + TemplateUserManual.setWindowTitle(_translate("TemplateUserManual", "Form")) + self.home.setText(_translate("TemplateUserManual", "Home")) + self.back.setText(_translate("TemplateUserManual", "Back")) + diff --git a/template/qtgui/designer/usermanual.ui b/template/qtgui/designer/usermanual.ui new file mode 100644 index 0000000..0f73f76 --- /dev/null +++ b/template/qtgui/designer/usermanual.ui @@ -0,0 +1,75 @@ + + + TemplateUserManual + + + + 0 + 0 + 776 + 733 + + + + Form + + + + + + + + + + 0 + 0 + + + + Home + + + false + + + + + + + + 0 + 0 + + + + Back + + + Backspace + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + diff --git a/template/qtgui/eventloop.py b/template/qtgui/eventloop.py new file mode 100644 index 0000000..10ab4cd --- /dev/null +++ b/template/qtgui/eventloop.py @@ -0,0 +1,90 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +from PyQt5 import QtCore + + +class EventLoop(object): + + def __init__(self): + """The loop for all things GUI and controlling the GUI (e.g. by a control midi in port) + + directConnect is 0ms (see below) + fastConnect 20ms + slowConnect 100ms + verySlowConnect 200ms + + 0 ms means "if there is time". 10ms-20ms is smooth. 100ms is still ok. + Influences everything. Control Midi In Latency, playback cursor scrolling smoothnes etc. + + But not realtime. This is not the realtime loop. Converting midi into instrument sounds + or playing back sequenced midi data is not handled by this loop at all. + + Creating a non-qt class for the loop is an abstraction layer that enables the engine to + work without modification for non-gui situations. In this case it will use its own loop, + like python async etc. + + A qt event loop needs the qt-app started. Otherwise it will not run. + We init the event loop outside of main but call start from the mainWindow. + """ + self.fastLoop = QtCore.QTimer() + self.slowLoop = QtCore.QTimer() + self.verySlowLoop = QtCore.QTimer() + + def fastConnect(self, function): + self.fastLoop.timeout.connect(function) + + def slowConnect(self, function): + self.slowLoop.timeout.connect(function) + + def verySlowConnect(self, function): + self.verySlowLoop.timeout.connect(function) + + def fastDisconnect(self, function): + """The function must be the exact instance that was registered""" + self.fastLoop.timeout.disconnect(function) + + def slowDisconnect(self, function): + """The function must be the exact instance that was registered""" + self.slowLoop.timeout.disconnect(function) + + def verySlowDisconnect(self, function): + """The function must be the exact instance that was registered""" + self.verySlowLoop.timeout.disconnect(function) + + def start(self): + """The event loop MUST be started after the Qt Application instance creation""" + logger.info("Starting fast qt event loop") + self.fastLoop.start(20) + logger.info("Starting slow qt event loop") + self.slowLoop.start(100) + logger.info("Starting very slow qt event loop") + self.verySlowLoop.start(200) + + def stop(self): + logger.info("Stopping fast qt event loop") + self.fastLoop.stop() + logger.info("Stopping slow qt event loop") + self.slowLoop.stop() + logger.info("Stopping very slow qt event loop") + self.verySlowLoop.stop() diff --git a/template/qtgui/helper.py b/template/qtgui/helper.py new file mode 100644 index 0000000..410c1e1 --- /dev/null +++ b/template/qtgui/helper.py @@ -0,0 +1,339 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of Laborejo ( https://www.laborejo.org ) + +Laborejo 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +from PyQt5 import QtCore, QtGui, QtWidgets + +from hashlib import md5 + + +def iconFromString(st, size=128): + px = QtGui.QPixmap(size,size) + color = stringToColor(st) + px.fill(color) + return QtGui.QIcon(px) + +def stringToColor(st): + """Convert a string to QColor. Same string, same color + Is used for group coloring""" + if st: + c = md5(st.encode()).hexdigest() + return QtGui.QColor(int(c[0:9],16) % 255, int(c[10:19],16) % 255, int(c[20:29],16)% 255, 255) + else: + return QtGui.QColor(255,255,255,255) #Return White + +def invertColor(qcolor): + r = 255 - qcolor.red() + g = 255 - qcolor.green() + b = 255 - qcolor.blue() + return QtGui.QColor(r, g, b, qcolor.alpha()) + +def removeInstancesFromScene(qtGraphicsClass): + """"Remove all instances of a qt class that implements .instances=[] from the QGraphicsScene. + Don't use for items or anything in the notation view. This is used by the likes of the conductor + only since they exist only once and gets redrawn completely each time.""" + for instance in qtGraphicsClass.instances: + instance.setParentItem(None) + instance.scene().removeWhenIdle(instance) + qtGraphicsClass.instances = [] + +def callContextMenu(listOfLabelsAndFunctions): + menu = QtWidgets.QMenu() + + for text, function in listOfLabelsAndFunctions: + if text == "separator": + menu.addSeparator() + else: + a = QtWidgets.QAction(text, menu) + menu.addAction(a) + a.triggered.connect(function) + + pos = QtGui.QCursor.pos() + pos.setY(pos.y() + 5) + menu.exec_(pos) + +def stretchLine(qGraphicsLineItem, factor): + line = qGraphicsLineItem.line() + line.setLength(line.length() * factor) + qGraphicsLineItem.setLine(line) + +def stretchRect(qGraphicsRectItem, factor): + r = qGraphicsRectItem.rect() + r.setRight(r.right() * factor) + qGraphicsRectItem.setRect(r) + +class QHLine(QtWidgets.QFrame): + def __init__(self): + super().__init__() + self.setFrameShape(QtWidgets.QFrame.HLine) + self.setFrameShadow(QtWidgets.QFrame.Sunken) + +def setPaletteAndFont(qtApp): + """Set our programs color + This is in its own function because it is a annoying to scroll by that in init. + http://doc.qt.io/qt-5/qpalette.html""" + fPalBlue = QtGui.QPalette() + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Window, QtGui.QColor(32, 35, 39)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Window, QtGui.QColor(37, 40, 45)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Window, QtGui.QColor(37, 40, 45)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(89, 95, 104)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.WindowText, QtGui.QColor(223, 237, 255)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, QtGui.QColor(223, 237, 255)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Base, QtGui.QColor(48, 53, 60)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, QtGui.QColor(55, 61, 69)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base, QtGui.QColor(55, 61, 69)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, QtGui.QColor(60, 64, 67)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, QtGui.QColor(69, 73, 77)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, QtGui.QColor(69, 73, 77)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, QtGui.QColor(182, 193, 208)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, QtGui.QColor(182, 193, 208)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, QtGui.QColor(182, 193, 208)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, QtGui.QColor(42, 44, 48)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, QtGui.QColor(42, 44, 48)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, QtGui.QColor(42, 44, 48)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(96, 103, 113)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, QtGui.QColor(210, 222, 240)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Text, QtGui.QColor(210, 222, 240)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Button, QtGui.QColor(51, 55, 62)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Button, QtGui.QColor(59, 63, 71)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Button, QtGui.QColor(59, 63, 71)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(98, 104, 114)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, QtGui.QColor(210, 222, 240)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, QtGui.QColor(210, 222, 240)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, QtGui.QColor(255, 255, 255)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.BrightText, QtGui.QColor(255, 255, 255)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, QtGui.QColor(255, 255, 255)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Light, QtGui.QColor(59, 64, 72)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Light, QtGui.QColor(63, 68, 76)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Light, QtGui.QColor(63, 68, 76)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, QtGui.QColor(48, 52, 59)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Midlight, QtGui.QColor(51, 56, 63)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, QtGui.QColor(51, 56, 63)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, QtGui.QColor(18, 19, 22)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Dark, QtGui.QColor(20, 22, 25)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, QtGui.QColor(20, 22, 25)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, QtGui.QColor(28, 30, 34)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Mid, QtGui.QColor(32, 35, 39)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, QtGui.QColor(32, 35, 39)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, QtGui.QColor(13, 14, 16)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Shadow, QtGui.QColor(15, 16, 18)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, QtGui.QColor(15, 16, 18)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, QtGui.QColor(32, 35, 39)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Highlight, QtGui.QColor(14, 14, 17)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight, QtGui.QColor(27, 28, 33)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, QtGui.QColor(89, 95, 104)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText, QtGui.QColor(217, 234, 253)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText, QtGui.QColor(223, 237, 255)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Link, QtGui.QColor(79, 100, 118)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.Link, QtGui.QColor(156, 212, 255)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Link, QtGui.QColor(156, 212, 255)) + + fPalBlue.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.LinkVisited, QtGui.QColor(51, 74, 118)) + fPalBlue.setColor(QtGui.QPalette.Active, QtGui.QPalette.LinkVisited, QtGui.QColor(64, 128, 255)) + fPalBlue.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.LinkVisited, QtGui.QColor(64, 128, 255)) + + qtApp.setPalette(fPalBlue) + + font = QtGui.QFont("DejaVu Sans", 10) + font.setPixelSize(12) + qtApp.setFont(font) + return fPalBlue + +class ToggleSwitch(QtWidgets.QAbstractButton): + """From https://stackoverflow.com/questions/14780517/toggle-switch-in-qt/38102598 + + Usage: + + # Thumb size < track size (Gitlab style) + s1 = ToggleSwitch() + s1.toggled.connect(lambda c: print('toggled', c)) + s1.clicked.connect(lambda c: print('clicked', c)) + s1.pressed.connect(lambda: print('pressed')) + s1.released.connect(lambda: print('released')) + + s2 = ToggleSwitch() + s2.setEnabled(False) + + # Thumb size > track size (Android style) + s3 = ToggleSwitch(thumb_radius=11, track_radius=8) + s4 = ToggleSwitch(thumb_radius=11, track_radius=8) + s4.setEnabled(False) + """ + + + def __init__(self, parent=None, track_radius=10, thumb_radius=8): + super().__init__(parent=parent) + self.setCheckable(True) + self.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + + self._track_radius = track_radius + self._thumb_radius = thumb_radius + + self._margin = max(0, self._thumb_radius - self._track_radius) + self._base_offset = max(self._thumb_radius, self._track_radius) + self._end_offset = { + True: lambda: self.width() - self._base_offset, + False: lambda: self._base_offset, + } + self._offset = self._base_offset + + palette = self.palette() + if self._thumb_radius > self._track_radius: + self._track_color = { + True: palette.highlight(), + False: palette.dark(), + } + self._thumb_color = { + True: palette.highlight(), + False: palette.light(), + } + self._text_color = { + True: palette.highlightedText().color(), + False: palette.dark().color(), + } + self._thumb_text = { + True: '', + False: '', + } + self._track_opacity = 0.5 + else: + self._thumb_color = { + True: palette.highlightedText(), + False: palette.light(), + } + self._track_color = { + True: palette.highlight(), + False: palette.dark(), + } + self._text_color = { + True: palette.highlight().color(), + False: palette.dark().color(), + } + self._thumb_text = { + True: '✔', + False: '✕', + } + self._track_opacity = 1 + + @QtCore.pyqtProperty(int) + def offset(self): + return self._offset + + @offset.setter + def offset(self, value): + self._offset = value + self.update() + + def sizeHint(self): # pylint: disable=invalid-name + return QtCore.QSize( + 4 * self._track_radius + 2 * self._margin, + 2 * self._track_radius + 2 * self._margin, + ) + + def setChecked(self, checked): + super().setChecked(checked) + self.offset = self._end_offset[checked]() + + def resizeEvent(self, event): + super().resizeEvent(event) + self.offset = self._end_offset[self.isChecked()]() + + def paintEvent(self, event): # pylint: disable=invalid-name, unused-argument + p = QtGui.QPainter(self) + p.setRenderHint(QtGui.QPainter.Antialiasing, True) + p.setPen(QtCore.Qt.NoPen) + track_opacity = self._track_opacity + thumb_opacity = 1.0 + text_opacity = 1.0 + if self.isEnabled(): + track_brush = self._track_color[self.isChecked()] + thumb_brush = self._thumb_color[self.isChecked()] + text_color = self._text_color[self.isChecked()] + else: + track_opacity *= 0.8 + track_brush = self.palette().shadow() + thumb_brush = self.palette().mid() + text_color = self.palette().shadow().color() + + p.setBrush(track_brush) + p.setOpacity(track_opacity) + p.drawRoundedRect( + self._margin, + self._margin, + self.width() - 2 * self._margin, + self.height() - 2 * self._margin, + self._track_radius, + self._track_radius, + ) + p.setBrush(thumb_brush) + p.setOpacity(thumb_opacity) + p.drawEllipse( + self.offset - self._thumb_radius, + self._base_offset - self._thumb_radius, + 2 * self._thumb_radius, + 2 * self._thumb_radius, + ) + p.setPen(text_color) + p.setOpacity(text_opacity) + font = p.font() + font.setPixelSize(1.5 * self._thumb_radius) + p.setFont(font) + p.drawText( + QtCore.QRectF( + self.offset - self._thumb_radius, + self._base_offset - self._thumb_radius, + 2 * self._thumb_radius, + 2 * self._thumb_radius, + ), + QtCore.Qt.AlignCenter, + self._thumb_text[self.isChecked()], + ) + + def mouseReleaseEvent(self, event): # pylint: disable=invalid-name + super().mouseReleaseEvent(event) + if event.button() == QtCore.Qt.LeftButton: + anim = QtCore.QPropertyAnimation(self, b'offset', self) + anim.setDuration(120) + anim.setStartValue(self.offset) + anim.setEndValue(self._end_offset[self.isChecked()]()) + anim.start() + + def enterEvent(self, event): # pylint: disable=invalid-name + self.setCursor(QtCore.Qt.PointingHandCursor) + super().enterEvent(event) diff --git a/template/qtgui/mainwindow.py b/template/qtgui/mainwindow.py new file mode 100644 index 0000000..4e9ee7a --- /dev/null +++ b/template/qtgui/mainwindow.py @@ -0,0 +1,383 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +#Standard Library +import os +import os.path +from sys import argv as sysargv +from sys import exit as sysexit + +#Third Party +from PyQt5 import QtCore, QtGui, QtWidgets +logger.info(f"PyQt Version: {QtCore.PYQT_VERSION_STR}") +#from PyQt5 import QtOpenGL + +#Template Modules +from .nsmclient import NSMClient +from .usermanual import UserManual +from .debugScript import DebugScriptRunner +from .menu import Menu +from .resources import * +from .about import About +from .helper import setPaletteAndFont +from .eventloop import EventLoop +from template.start import PATHS, qtApp + +#Client modules +from engine.config import * #imports METADATA +import engine.api as api #This loads the engine and starts a session. +from qtgui.designer.mainwindow import Ui_MainWindow #The MainWindow designer file is loaded from the CLIENT side +from qtgui.resources import * +from qtgui.constantsAndConfigs import constantsAndConfigs + + +api.session.eventLoop = EventLoop() +#QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_DontUseNativeMenuBar) #Force a real menu bar. Qt on wayland will not display it otherwise. + + +#Setup the translator before classes are set up. Otherwise we can't use non-template translation. +#to test use LANGUAGE=de_DE.UTF-8 . not LANG= +#Language variants like de_AT.UTF-8 will be detected automatically and will result in Qt language detection as "German" +language = QtCore.QLocale().languageToString(QtCore.QLocale().language()) +logger.info("{}: Language set to {}".format(METADATA["name"], language)) +if language in METADATA["supportedLanguages"]: + templateTranslator = QtCore.QTranslator() + templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL + qtApp.installTranslator(templateTranslator) + + otherTranslator = QtCore.QTranslator() + otherTranslator.load(METADATA["supportedLanguages"][language], ":translations") #colon to make it a resource URL + qtApp.installTranslator(otherTranslator) + +else: + """silently fall back to English by doing nothing""" + + +class MainWindow(QtWidgets.QMainWindow): + """Before the mainwindow class is even parsed all the engine imports are done. + As side effects they set up our session, callbacks and api. They are now waiting + for the start signal which will be send by NSM. Session Management simulates user actions, + so their place is in the (G)UI. Therefore the MainWindows role is to set up the nsm client.""" + + def __init__(self): + super().__init__() + self.qtApp = qtApp + self.qtApp.setWindowIcon(QtGui.QIcon(":icon.png")) #non-template part of the program + QtGui.QIcon.setThemeName("hicolor") #audio applications can be found here. We have no need for other icons. + logger.info("Init MainWindow") + + #Callbacks. Must be registered before startEngine. + api.callbacks.message.append(self.callback_message) + + #NSM Client + self.nsmClient = NSMClient(prettyName = METADATA["name"], #will raise an error and exit if this is not run from NSM + supportsSaveStatus = True, + saveCallback = api.session.nsm_saveCallback, + openOrNewCallback = api.session.nsm_openOrNewCallback, + exitProgramCallback = self._nsmQuit, + hideGUICallback = self.hideGUI, + showGUICallback = self.showGUI, + loggingLevel = logging.getLogger().level, + ) + + #Set up the user interface from Designer and other widgets + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self.fPalBlue = setPaletteAndFont(self.qtApp) + self.userManual = UserManual(mainWindow=self) #starts hidden. menu shows, closeEvent hides. + + self.xFactor = 1 #keep track of the x stretch factor. + + self.setWindowTitle(self.nsmClient.ourClientNameUnderNSM) + self.qtApp.setApplicationName(self.nsmClient.ourClientNameUnderNSM) + self.qtApp.setApplicationDisplayName(self.nsmClient.ourClientNameUnderNSM) + self.qtApp.setOrganizationName("Laborejo Software Suite") + self.qtApp.setOrganizationDomain("laborejo.org") + self.qtApp.setApplicationVersion(METADATA["version"]) + self.setAcceptDrops(True) + + self.debugScriptRunner = DebugScriptRunner(apilocals=locals()) #needs to have trueInit called after the session and nsm was set up. Which happens in startEngine. + self.debugScriptRunner.trueInit(nsmClient=self.nsmClient) + + #Show the About Dialog the first time the program starts up. + #This is the initial state user/system wide and not a saved in NSM nor bound to the NSM ID (like window position) + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if not settings.contains("showAboutDialog"): + settings.setValue("showAboutDialog", METADATA["showAboutDialogFirstStart"]) + + self.about = About(mainWindow=self) #This does not show, it only creates. Showing is decided in self.start + self.ui.menubar.setNativeMenuBar(False) #Force a real menu bar. Qt on wayland will not display it otherwise. + self.menu = Menu(mainWindow=self) #needs the about dialog, save file and the api.session ready. + + self.installEventFilter(self) #Bottom of the file. Works around the hover numpad bug that is in Qt for years. + + self.initiGuiSharedDataToSave() + + def start(self): + + api.session.eventLoop.start() #The event loop must be started after the qt app + api.session.eventLoop.fastConnect(self.nsmClient.reactToMessage) + api.startEngine(self.nsmClient) #Load the file, start the eventLoop. Triggers all the callbacks that makes us draw. + + if api.session.guiWasSavedAsNSMVisible: + self.showGUI() + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("showAboutDialog") and settings.value("showAboutDialog", type=bool): + QtCore.QTimer.singleShot(100, self.about.show) #Qt Event loop is not ready at that point. We need to wait for the paint event. This is not to stall for time: Using the event loop guarantees that it exists + elif not self.nsmClient.sessionName == "NOT-A-SESSION": #standalone mode + self.ui.actionQuit.setShortcut("") + #TODO: this is a hack until we figure out how to cleanly handle hide vs quite from outside the application + self.hideGUI() + + self._zoom() #enable zoom factor loaded from save file + + + #def event(self, event): + # print (event.type()) + # return super().event(event) + + def callback_message(self, title, text): + QtWidgets.QMessageBox.warning(self, title ,text) + + def dragEnterEvent(self, event): + """Needs self.setAcceptDrops(True) in init""" + if event.mimeData().hasUrls(): + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """Needs self.setAcceptDrops(True) in init. + + Having that function in the mainWindow will not make drops available for subwindows + like About or UserManual. """ + for url in event.mimeData().urls(): + filePath = url.toLocalFile() + #Decide here if you want only files, only directories, both etc. + if os.path.isfile(filePath) and filePath.lower().endswith(".sf2"): + #linkedPath = self.nsmClient.importResource(filePath) + print ("received drop") + + + + def storeWindowSettings(self): + """Window state is not saved in the real save file. That would lead to portability problems + between computers, like different screens and resolutions. + + For convenience that means we just use the damned qt settings and save wherever qt wants. + + We don't use the NSM id, session share their placement. + + bottom line: get a tiling window manager. + """ + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + settings.setValue("geometry", self.saveGeometry()) + settings.setValue("windowState", self.saveState()) + + def restoreWindowSettings(self): + """opposite of storeWindowSettings. Read there.""" + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("geometry") and settings.contains("windowState"): + self.restoreGeometry(settings.value("geometry")) + self.restoreState(settings.value("windowState")) + + + def initiGuiSharedDataToSave(self): + """Called by init""" + + if not "last_export_dir" in api.session.guiSharedDataToSave: + api.session.guiSharedDataToSave["last_export_dir"] = os.path.expanduser("~") + + if "grid_opacity" in api.session.guiSharedDataToSave: + constantsAndConfigs.gridOpacity = float(api.session.guiSharedDataToSave["grid_opacity"]) + + if "grid_rhythm" in api.session.guiSharedDataToSave: + #setting this is enough. When the grid gets created it fetches the constantsAndConfigs value. + #Set only in submenus.GridRhytmEdit + constantsAndConfigs.gridRhythm = int(api.session.guiSharedDataToSave["grid_rhythm"]) + + #Stretch + if "ticks_to_pixel_ratio" in api.session.guiSharedDataToSave: + #setting this is enough. Drawing on startup uses the constantsAndConfigs value. + #Set only in ScoreView._stretchXCoordinates + constantsAndConfigs.ticksToPixelRatio = float(api.session.guiSharedDataToSave["ticks_to_pixel_ratio"]) + + if "zoom_factor" in api.session.guiSharedDataToSave: + #setting this is enough. Drawing on startup uses the constantsAndConfigs value. + #Set only in ScoreView._zoom + constantsAndConfigs.zoomFactor = float(api.session.guiSharedDataToSave["zoom_factor"]) + + #Zoom and Stretch + + def wheelEvent(self, ev): + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + ev.accept() + if ev.angleDelta().y() > 0: + self.zoomIn() + else: + self.zoomOut() + + + elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier: + ev.accept() + if ev.angleDelta().y() > 0: + self.widen() + else: + self.shrinken() + + else: + super().wheelEvent(ev) #send to the items + + def _zoom(self): + api.session.guiSharedDataToSave["zoom_factor"] = constantsAndConfigs.zoomFactor + #Send to client mainWindow + self.zoom(constantsAndConfigs.zoomFactor) + + def zoom(self, scaleFactor:float): + raise NotImplementedError("Reimplement this in your program. Can do nothing, if you want. See template/qtgui/mainwindow.py for example implementation") + #self.scoreView.resetTransform() + #self.scoreView.scale(scaleFactor, scaleFactor) + + def stretchXCoordinates(self, factor): + raise NotImplementedError("Reimplement this in your program. Can do nothing, if you want. See template/qtgui/mainwindow.py for example implementation") + #self.scoreView.stretchXCoordinates(factor) + #self.scoreView.centerOnCursor() + + def zoomIn(self): + constantsAndConfigs.zoomFactor = round(constantsAndConfigs.zoomFactor + 0.25, 2) + if constantsAndConfigs.zoomFactor > 2.5: + constantsAndConfigs.zoomFactor = 2.5 + self._zoom() + return True + + def zoomOut(self): + constantsAndConfigs.zoomFactor = round(constantsAndConfigs.zoomFactor - 0.25, 2) + if constantsAndConfigs.zoomFactor < constantsAndConfigs.maximumZoomOut: + constantsAndConfigs.zoomFactor = constantsAndConfigs.maximumZoomOut + self._zoom() + return True + + def zoomNull(self): + constantsAndConfigs.zoomFactor = 1 + self._zoom() + + def _stretchXCoordinates(self, factor): + """Reposition the items on the X axis. + Call goes through all parents/children, starting from here. + + The parent sets the X coordinates of its children. + Then the parent calls the childs _stretchXCoordinates() method if the child has children + itself. For example a rectangleItem has a position which is set by the parent. But the + rectangleItem has a right border which needs to be adjusted as well. This right border is + treated as child of the rectItem, handled by rectItem._stretchXCoordinates(factor). + """ + if self.xFactor * factor < 0.015: + return + self.xFactor *= factor + constantsAndConfigs.ticksToPixelRatio /= factor + api.session.guiSharedDataToSave["ticks_to_pixel_ratio"] = constantsAndConfigs.ticksToPixelRatio + self.stretchXCoordinates(factor) + return True + + + def widen(self): + #self._stretchXCoordinates(1*1.2) #leads to rounding errors + self._stretchXCoordinates(2) + + def shrinken(self): + #self._stretchXCoordinates(1/1.2) #leads to rounding errors + self._stretchXCoordinates(0.5) + + #Close and exit + + def _nsmQuit(self, ourPath, sessionName, ourClientNameUnderNSM): + logger.info("Qt main window received NSM exit callback. Calling pythons system exit. ") + self.storeWindowSettings() + + #api.stopEngine() #will be called trough sessions atexit + #self.qtApp.quit() #does not work. This will fail and pynsmclient2 will send SIGKILL + sysexit() #works, NSM cleanly detects a quit. Triggers the session atexit condition + logger.error("Code executed after sysexit. This message should not have been visible.") + #Code here never gets executed. + + def closeEvent(self, event): + """This is the manual close event, not the NSM Message. + Ignore. We use it to send the GUI into hiding.""" + event.ignore() + self.hideGUI() + + def hideGUI(self): + self.storeWindowSettings() + self.hide() + self.nsmClient.announceGuiVisibility(False) + + def showGUI(self): + self.restoreWindowSettings() + self.show() + self.nsmClient.announceGuiVisibility(True) + + def eventFilter(self, obj, event): + """Qt has a known but unresolved bug (for many years now) + that shortcuts with the numpad don't trigger. This has little + chance of ever getting fixed, despite getting reported multiple + times. + + Try to uninstall this filter (self.__init__) after a new qt release + and check if it works without. It should (it did already in the past) + but so far no luck... + + What do we do? + + We intercept every key and see if it was with a numpad modifier. + If yes we trigger the menu action ourselves and discard the event + + We also need to separate the action of assigning a hover shortcut + to a menu and actually triggering it. Since we are in the template + part of the program we can't assume there is a main widget, + as in Patroneo and Laborejos case. + + The obj is always MainWindow, so we can't detect if the menu was open or not. + self.qtApp.focusWidget() is also NOT the menu. + + The menu is only triggered AFTER the filter. But we need to + detect if a menu is open while we are running the filter. + self.menu.ui.menubar.activeAction() + + For each key press/release we receive 3 keypress events, not + counting autoRepeats. event.type() which is different from type(event) + returns 51 (press), 6(release), 7(accept) for the three. This is key press, release and event accept(?). + + This event filter somehow does not differentiate between numpad + on or numpad off. There maybe is another qt check for that. + """ + if (not self.menu.ui.menubar.activeAction()) and event.type() == 51 and type(event) is QtGui.QKeyEvent and event.modifiers() == QtCore.Qt.KeypadModifier and event.text() and event.text() in "0123456789": + action = self.menu.hoverShortcutDict[event.text()] + if action: + action.trigger() + event.accept() #despite what Qt docs say it is not enough to return True to "eat" the event. + return True + else: + #No keypad shortcut. Just use the normal handling. + return super().eventFilter(obj, event) diff --git a/template/qtgui/menu.py b/template/qtgui/menu.py new file mode 100644 index 0000000..bf6af9e --- /dev/null +++ b/template/qtgui/menu.py @@ -0,0 +1,399 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Standard Library + +#Third Party +from PyQt5 import QtCore, QtGui, QtWidgets + + +#Template Modules +from .about import About +from .resources import * + +#User Modules +import engine.api as api +from engine.config import * #imports METADATA + +class Menu(object): + + def __init__(self, mainWindow): + """We can receive a dict of extra menu actions, which also override our hardcoded ones""" + + self.mainWindow = mainWindow + self.ui = mainWindow.ui #just shorter to write. + + #Hover Shortcuts + self.hoverShortcutDict = HoverActionDictionary(self.mainWindow) #for temporary hover shortcuts. key=actual key, value=QAction + self.lastHoverAction = None #the last time the mouse hovered over a menu it was this QAction. This gets never empty again, but works because we only check it during menu KeyPressEvent + def _rememberHover(action): self.lastHoverAction = action + self.ui.menubar.hovered.connect(_rememberHover) + self._originalMenuKeyPressEvent = self.ui.menubar.keyPressEvent + self.ui.menubar.keyPressEvent = self.menuKeyPressInterceptor + + #Order Matters. First is inserted first + self._setupFileMenu() + self._setupEditMenu() + self._setupHelpMenu() + self._setupDebugMenu() #Setup the provided debug menu, keep it separate from actual code + + #Set the label of undo and redo to show the info the api provides + api.callbacks.historyChanged.append(self.renameUndoRedoByHistory) + + def renameUndoRedoByHistory(self, undoList, redoList): + undoString = QtCore.QCoreApplication.translate("TemplateMainWindow", "Undo") + redoString = QtCore.QCoreApplication.translate("TemplateMainWindow", "Redo") + + if undoList: + undoInfo = QtCore.QCoreApplication.translate("NOOPengineHistory", undoList[-1]) + self.ui.actionUndo.setText(undoString +": {}".format(undoInfo)) + self.ui.actionUndo.setEnabled(True) + else: + self.ui.actionUndo.setText(undoString) + self.ui.actionUndo.setEnabled(False) + + if redoList: + redoInfo = QtCore.QCoreApplication.translate("NOOPengineHistory", redoList[-1]) + self.ui.actionRedo.setText(redoString + ": {}".format(redoInfo)) + self.ui.actionRedo.setEnabled(True) + else: + self.ui.actionRedo.setText(redoString) + self.ui.actionRedo.setEnabled(False) + + def _setupHelpMenu(self): + self.addSubmenu("menuHelp", QtCore.QCoreApplication.translate("TemplateMainWindow", "Help")) + self.addMenuEntry("menuHelp", "actionUser_Manual", QtCore.QCoreApplication.translate("TemplateMainWindow", "User Manual"), connectedFunction=self.mainWindow.userManual.show) + self.addSeparator("menuHelp") + self.addMenuEntry("menuHelp", "actionAbout", QtCore.QCoreApplication.translate("TemplateMainWindow", "About and Tips"), connectedFunction=self.mainWindow.about.show) + + def _setupEditMenu(self): + self.addSubmenu("menuEdit", QtCore.QCoreApplication.translate("TemplateMainWindow", "Edit")) + self.addMenuEntry("menuEdit", "actionUndo", QtCore.QCoreApplication.translate("TemplateMainWindow", "Undo"), connectedFunction=api.undo, shortcut="Ctrl+Z") + self.addMenuEntry("menuEdit", "actionRedo", QtCore.QCoreApplication.translate("TemplateMainWindow", "Redo"), connectedFunction=api.redo, shortcut="Ctrl+Shift+Z") + + def _setupFileMenu(self): + self.addSubmenu("menuFile", QtCore.QCoreApplication.translate("TemplateMainWindow", "File")) + self.addMenuEntry("menuFile", "actionSave", QtCore.QCoreApplication.translate("TemplateMainWindow", "Save"), connectedFunction=api.save, shortcut="Ctrl+S") + self.addMenuEntry("menuFile", "actionQuit", QtCore.QCoreApplication.translate("TemplateMainWindow", "Quit (without saving)"), connectedFunction=self.mainWindow.nsmClient.serverSendExitToSelf, shortcut="Ctrl+Q") + + def _setupDebugMenu(self): + """Debug entries are not translated""" + def guiCallback_menuDebugVisibility(state): + self.ui.menuDebug.menuAction().setVisible(state) + api.session.guiSharedDataToSave["actionToggle_Debug_Menu_Visibility"] = state + + self.addSubmenu("menuDebug", "Debug") + self.addMenuEntry("menuDebug", "actionToggle_Debug_Menu_Visibility", "Toggle Debug Menu Visibility", connectedFunction=guiCallback_menuDebugVisibility, shortcut="Ctrl+Alt+Shift+D") + self.ui.actionToggle_Debug_Menu_Visibility.setCheckable(True) + self.ui.actionToggle_Debug_Menu_Visibility.setEnabled(True) + + self.addMenuEntry("menuDebug", "actionRun_Script_File", "Run Script File", connectedFunction=self.mainWindow.debugScriptRunner.run) #runs the file debugscript.py in our session dir. + self.addMenuEntry("menuDebug", "actionEmpty_Menu_Entry", "Empty Menu Entry") + deleteSessionText = "Delete our session data and exit: {}".format(self.mainWindow.nsmClient.ourPath) #Include the to-be-deleted path into the menu text. Better verbose than sorry. + self.addMenuEntry("menuDebug", "actionReset_Own_Session_Data", deleteSessionText, connectedFunction=self.mainWindow.nsmClient.debugResetDataAndExit) + + #Now that the actions are connected we can create the initial setup + if not "actionToggle_Debug_Menu_Visibility" in api.session.guiSharedDataToSave: + api.session.guiSharedDataToSave["actionToggle_Debug_Menu_Visibility"] = False + state = api.session.guiSharedDataToSave["actionToggle_Debug_Menu_Visibility"] + self.ui.actionToggle_Debug_Menu_Visibility.setChecked(state) #does not trigger the connected action... + self.ui.menuDebug.menuAction().setVisible(state) #...therefore we hide manually. + + def menuKeyPressInterceptor(self, event): + """ + Triggered when a menu is open and mouse hovers over a menu. + + Menu Entries without their own shortcut can be temporarily assigned one of the ten + number keypad keys as shortcuts. + + If the numpad key is already in use it will be rerouted (freed first). + """ + strings = set(("Num+1", "Num+2", "Num+3", "Num+4", "Num+5", "Num+6", "Num+7", "Num+8", "Num+9", "Num+0")) + if self.lastHoverAction and (not self.lastHoverAction.menu()) and event.modifiers() == QtCore.Qt.KeypadModifier and ((not self.lastHoverAction.shortcut().toString()) or self.lastHoverAction.shortcut().toString() in strings): + key = event.text() #just number 0-9 #event.key() is 49, 50... + self.hoverShortcutDict[key] = self.lastHoverAction + event.accept() + else: + self._originalMenuKeyPressEvent(event) + + + def addSubmenu(self, submenuActionAsString, text): + """ + Returns the submenu. + + + submenuActionAsString is something like menuHelp + + It is not possible to create nested submenus yet. + text must already be translated. + + If the menu already exists (decided by submenuActionAsString, not by text) it will be + returned instead.""" + + try: + return getattr(self.ui, submenuActionAsString) + except AttributeError: + pass + + setattr(self.ui, submenuActionAsString, QtWidgets.QMenu(self.ui.menubar)) + menu = getattr(self.ui, submenuActionAsString) + menu.setObjectName(submenuActionAsString) + menu.menuAction().setObjectName(submenuActionAsString) + menu.setTitle(text) + self.ui.menubar.addAction(menu.menuAction()) + return menu + + def getSubmenuOrder(self): + """Returns a list of strings with submenu names, compatible with orderSubmenus""" + #text() shows the translated string. We wan't the attribute name like actionEdit + #action.objectName() gives us the name, but only if set. QtDesigner does not set. + #action.menu().objectName() works. + listOfStrings = [] + for a in self.ui.menubar.actions(): + if a.menu(): + listOfStrings.append(a.menu().objectName()) + else: #separator + listOfStrings.append(None) + return listOfStrings + + def getTemplateSubmenus(self): + """Returns a list of strings with submenu names, compatible with orderSubmenus. + Only contains the ones set in the template. + Not in order.""" + #Hardcoded. + return ["menuFile", "menuEdit", "menuHelp", "menuDebug"] + + def getClientSubmenus(self): + """opposite of getTemplateSubmenus. + In order""" + allMenus = self.getSubmenuOrder() + templateMenus = self.getTemplateSubmenus() + return [st for st in allMenus if st not in templateMenus] + + def orderSubmenus(self, listOfStrings): + """sort after listOfStrings. + + List of strings can contain None to insert a separator at this position. + + It is expected that you give the complete list of menus.""" + self.ui.menubar.clear() + for submenuActionAsString in listOfStrings: + if submenuActionAsString: + menu = getattr(self.ui, submenuActionAsString) + self.ui.menubar.addAction(menu.menuAction()) + else: + self.ui.menubar.addSeparator() + + def hideSubmenu(self, submenu:str): + menuAction = getattr(self.ui, submenu).menuAction() + menuAction.setVisible(False) + + def removeSubmenu(self, submenuAction:str): + menuAction = getattr(self.ui, submenu).menuAction() + raise NotImplementedError #TODO + + def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False): + """ + parameterstrings must already be translated. + + Returns the QAction + + Add a new menu entry to an existing submenu. + If you want a new submenu use the create submenu function first. + + submenu is the actual menu action name, like "menuHelp". You need to know the variable names. + + actionAsString is the unique action name including the word "action", text is the display name. Text must be already translated, + actionAsString must never be translated. + + shortcut is an optional string like "Ctrl+Shift+Z" + + If the action already exists (decided by submenuActionAsString, not by text) it will be + returned instead. + """ + + try: + return getattr(self.ui, actionAsString) + except AttributeError: + pass + + setattr(self.ui, actionAsString, QtWidgets.QAction(self.mainWindow)) + action = getattr(self.ui, actionAsString) + + action.setObjectName(actionAsString) + action.setText(text) #Text must already be translated + action.setToolTip(tooltip) #Text must already be translated + if iconResource: + assert QtCore.QFile.exists(iconResource) + ico = QtGui.QIcon(iconResource) + action.setIcon(ico) + if checkable: + action.setCheckable(True) + + self.connectMenuEntry(actionAsString, connectedFunction, shortcut) + + submenu = getattr(self.ui, submenu) + submenu.addAction(action) + + return action + + def hideMenuEntry(self, menuAction:str): + action = getattr(self.ui, menuAction) + action.setVisible(False) + + def removeMenuEntry(self, menuAction:str): + raise NotImplementedError #TODO + + def addSeparator(self, submenu): + submenu = getattr(self.ui, submenu) + submenu.addSeparator() + + def connectMenuEntry(self, actionAsString, connectedFunction, shortcut=""): + """ + Returns the submenu. + Disconnects the old action first + + shortcut is an optional string like "Ctrl+Shift+Z" + + You can give None for connectedFunction. That makes automatisation in the program easier. + However it is possible to set a shortcut without a connectedFunction. + Also this helps to disconnect a menu entry of all its function. + """ + + action = getattr(self.ui, actionAsString) + try: + action.triggered.disconnect() + except TypeError: + #Action has no connected function on triggered-signal. + pass + + if connectedFunction: + action.triggered.connect(connectedFunction) + + if shortcut: + action.setShortcut(shortcut) + + return action + + def disable(self): + """This is meant for a short temporary disabling.""" + + for a in self.mainWindow.findChildren(QtWidgets.QAction): + if not a.menu(): + a.setEnabled(False) + + def enable(self): + for a in self.mainWindow.findChildren(QtWidgets.QAction): + a.setEnabled(True) + + self.renameUndoRedoByHistory(*api.getUndoLists()) + + + +class HoverActionDictionary(dict): + """Key = number from 0-9 as string + Value = QAction + + Singleton. + """ + + def __init__(self, mainWindow, *args): + self.mainWindow = mainWindow + + if not "hoverActionDictionary" in api.session.guiSharedDataToSave: + # key = action.text() , value = key sequence as text 'Num3+' key. Yes, the plus is in the string + # Basically both values and keys should be unique. + api.session.guiSharedDataToSave["hoverActionDictionary"] = {} + + dict.__init__(self, args) + + self.qShortcuts = { + "1" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_1, + "2" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_2, + "3" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_3, + "4" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_4, + "5" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_5, + "6" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_6, + "7" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_7, + "8" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_8, + "9" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_9, + "0" : QtCore.Qt.KeypadModifier+QtCore.Qt.Key_0, + } + + dict.__setitem__(self, "0", None) + dict.__setitem__(self, "1", None) + dict.__setitem__(self, "2", None) + dict.__setitem__(self, "3", None) + dict.__setitem__(self, "4", None) + dict.__setitem__(self, "5", None) + dict.__setitem__(self, "6", None) + dict.__setitem__(self, "7", None) + dict.__setitem__(self, "8", None) + dict.__setitem__(self, "9", None) + """ + for i in range(10): + emptyAction = QtWidgets.QAction("empty{}".format(i)) + self.mainWindow.addAction(emptyAction) #no actions without a widget + emptyAction.triggered.connect(api.nothing) + emptyAction.setShortcut(QtGui.QKeySequence(self.qShortcuts[str(i)])) + dict.__setitem__(self, str(i), emptyAction) + """ + + #Load the hover shortcuts from last session. + #We assume the shortcuts are clean and there is no mismatch or ambiguity. + if "hoverActionDictionary" in api.session.guiSharedDataToSave: # key = action.text() , value = key sequence as text 'Num3+' key. Yes, the plus is in the string + for action in self.mainWindow.findChildren(QtWidgets.QAction): #search the complete menu + #TODO: if the menu label changes, maybe through translation, this will break. Non-critical, but annoying. Find a way to find unique ids. + if action.text() in api.session.guiSharedDataToSave["hoverActionDictionary"]: #A saved action was found + keySequenceAsString = api.session.guiSharedDataToSave["hoverActionDictionary"][action.text()] + justTheNumberAsString = keySequenceAsString[-1 ] #-1 is a number as string + action.setShortcut(QtGui.QKeySequence(self.qShortcuts[justTheNumberAsString])) + dict.__setitem__(self, justTheNumberAsString, action) #does not trigger our own set item! + + def __setitem__(self, key, menuAction): + """key is a string of a single number 0-9""" + assert key in "0 1 2 3 4 5 6 7 8 9".split() + + try: + self[key].setShortcut(QtGui.QKeySequence()) #reset the action that previously had that shortcut + except AttributeError: + pass + + #Remove old entry from save dict if shortcut already in there. shortcut is the value, so we have to do it by iteration + for k,v in api.session.guiSharedDataToSave["hoverActionDictionary"].items(): + if v == "Num+"+key: + del api.session.guiSharedDataToSave["hoverActionDictionary"][k] + break + assert not key in api.session.guiSharedDataToSave["hoverActionDictionary"].values() + + try: + dict.__setitem__(self, menuAction.shortcut().toString()[-1], None) #-1 is a number as string + except IndexError: + pass + + menuAction.setShortcut(QtGui.QKeySequence()) #reset the new action to remove existing shortcuts + menuAction.setShortcut(QtGui.QKeySequence(self.qShortcuts[key])) + api.session.guiSharedDataToSave["hoverActionDictionary"][menuAction.text()] = "Num+"+key + dict.__setitem__(self, key, menuAction) + diff --git a/template/qtgui/nsmclient.py b/template/qtgui/nsmclient.py new file mode 100644 index 0000000..fac7cad --- /dev/null +++ b/template/qtgui/nsmclient.py @@ -0,0 +1,714 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +PyNSMClient - A New Session Manager Client-Library in one file. + +The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ +New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org +With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) + +MIT License + +Copyright (c) since 2014: Laborejo Software Suite , All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import logging; +logger = None #filled by init with prettyName + +import struct +import socket +from os import getenv, getpid, kill +import os +import os.path +import shutil +from uuid import uuid4 +from sys import argv +from signal import signal, SIGTERM, SIGINT, SIGKILL #react to exit signals to close the client gracefully. Or kill if the client fails to do so. +from urllib.parse import urlparse + +class _IncomingMessage(object): + """Representation of a parsed datagram representing an OSC message. + + An OSC message consists of an OSC Address Pattern followed by an OSC + Type Tag String followed by zero or more OSC Arguments. + """ + + def __init__(self, dgram): + #NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains. + #Therefore we can strip the bundle prefix and handle it as normal message. + if b"#bundle" in dgram: + bundlePrefix, singleMessage = dgram.split(b"/", maxsplit=1) + dgram = b"/" + singleMessage # / eaten by split + self.isBroadcast = True + else: + self.isBroadcast = False + self.LENGTH = 4 #32 bit + self._dgram = dgram + self._parameters = [] + self.parse_datagram() + + + def get_int(self, dgram, start_index): + """Get a 32-bit big-endian two's complement integer from the datagram. + + Args: + dgram: A datagram packet. + start_index: An index where the integer starts in the datagram. + + Returns: + A tuple containing the integer and the new end index. + + Raises: + ValueError if the datagram could not be parsed. + """ + try: + if len(dgram[start_index:]) < self.LENGTH: + raise ValueError('Datagram is too short') + return ( + struct.unpack('>i', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) + except (struct.error, TypeError) as e: + raise ValueError('Could not parse datagram %s' % e) + + def get_string(self, dgram, start_index): + """Get a python string from the datagram, starting at pos start_index. + + We receive always the full string, but handle only the part from the start_index internally. + In the end return the offset so it can be added to the index for the next parameter. + Each subsequent call handles less of the same string, starting further to the right. + + According to the specifications, a string is: + "A sequence of non-null ASCII characters followed by a null, + followed by 0-3 additional null characters to make the total number + of bits a multiple of 32". + + Args: + dgram: A datagram packet. + start_index: An index where the string starts in the datagram. + + Returns: + A tuple containing the string and the new end index. + + Raises: + ValueError if the datagram could not be parsed. + """ + #First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00. + if dgram[start_index:].startswith(b"\x00\x00\x00\x00"): + return "", start_index + 4 + + #Otherwise we have a non-empty string that must follow the rules of the docstring. + + offset = 0 + try: + while dgram[start_index + offset] != 0: + offset += 1 + if offset == 0: + raise ValueError('OSC string cannot begin with a null byte: %s' % dgram[start_index:]) + # Align to a byte word. + if (offset) % self.LENGTH == 0: + offset += self.LENGTH + else: + offset += (-offset % self.LENGTH) + # Python slices do not raise an IndexError past the last index, + # do it ourselves. + if offset > len(dgram[start_index:]): + raise ValueError('Datagram is too short') + data_str = dgram[start_index:start_index + offset] + return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset + except IndexError as ie: + raise ValueError('Could not parse datagram %s' % ie) + except TypeError as te: + raise ValueError('Could not parse datagram %s' % te) + + def get_float(self, dgram, start_index): + """Get a 32-bit big-endian IEEE 754 floating point number from the datagram. + + Args: + dgram: A datagram packet. + start_index: An index where the float starts in the datagram. + + Returns: + A tuple containing the float and the new end index. + + Raises: + ValueError if the datagram could not be parsed. + """ + try: + return (struct.unpack('>f', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) + except (struct.error, TypeError) as e: + raise ValueError('Could not parse datagram %s' % e) + + def parse_datagram(self): + try: + self._address_regexp, index = self.get_string(self._dgram, 0) + if not self._dgram[index:]: + # No params is legit, just return now. + return + + # Get the parameters types. + type_tag, index = self.get_string(self._dgram, index) + if type_tag.startswith(','): + type_tag = type_tag[1:] + + # Parse each parameter given its type. + for param in type_tag: + if param == "i": # Integer. + val, index = self.get_int(self._dgram, index) + elif param == "f": # Float. + val, index = self.get_float(self._dgram, index) + elif param == "s": # String. + val, index = self.get_string(self._dgram, index) + else: + logger.warning("Unhandled parameter type: {0}".format(param)) + continue + self._parameters.append(val) + except ValueError as pe: + #raise ValueError('Found incorrect datagram, ignoring it', pe) + # Raising an error is not ignoring it! + logger.warning("Found incorrect datagram, ignoring it. {}".format(pe)) + + @property + def oscpath(self): + """Returns the OSC address regular expression.""" + return self._address_regexp + + @staticmethod + def dgram_is_message(dgram): + """Returns whether this datagram starts as an OSC message.""" + return dgram.startswith(b'/') + + @property + def size(self): + """Returns the length of the datagram for this message.""" + return len(self._dgram) + + @property + def dgram(self): + """Returns the datagram from which this message was built.""" + return self._dgram + + @property + def params(self): + """Convenience method for list(self) to get the list of parameters.""" + return list(self) + + def __iter__(self): + """Returns an iterator over the parameters of this message.""" + return iter(self._parameters) + +class _OutgoingMessage(object): + def __init__(self, oscpath): + self.LENGTH = 4 #32 bit + self.oscpath = oscpath + self._args = [] + + def write_string(self, val): + dgram = val.encode('utf-8') + diff = self.LENGTH - (len(dgram) % self.LENGTH) + dgram += (b'\x00' * diff) + return dgram + + def write_int(self, val): + return struct.pack('>i', val) + + def write_float(self, val): + return struct.pack('>f', val) + + def add_arg(self, argument): + t = {str:"s", int:"i", float:"f"}[type(argument)] + self._args.append((t, argument)) + + def build(self): + dgram = b'' + + #OSC Path + dgram += self.write_string(self.oscpath) + + if not self._args: + dgram += self.write_string(',') + return dgram + + # Write the parameters. + arg_types = "".join([arg[0] for arg in self._args]) + dgram += self.write_string(',' + arg_types) + for arg_type, value in self._args: + f = {"s":self.write_string, "i":self.write_int, "f":self.write_float}[arg_type] + dgram += f(value) + return dgram + +class NSMNotRunningError(Exception): + """Error raised when environment variable $NSM_URL was not found.""" + +class NSMClient(object): + """The representation of the host programs as NSM sees it. + Technically consists of an udp server and a udp client. + + Does not run an event loop itself and depends on the host loop. + E.g. a Qt timer or just a simple while True: sleep(0.1) in Python.""" + def __init__(self, prettyName, supportsSaveStatus, saveCallback, openOrNewCallback, exitProgramCallback, hideGUICallback=None, showGUICallback=None, broadcastCallback=None, sessionIsLoadedCallback=None, loggingLevel = "info"): + + self.nsmOSCUrl = self.getNsmOSCUrl() #this fails and raises NSMNotRunningError if NSM is not available. Host programs can ignore it or exit their program. + + self.realClient = True + self.cachedSaveStatus = None #save status checks for this. + + global logger + logger = logging.getLogger(prettyName) + logger.info("import") + if loggingLevel == "info" or loggingLevel == 20: + logging.basicConfig(level=logging.INFO) #development + logger.info("Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!") #the NSM name is not ready yet so we just use the pretty name + elif loggingLevel == "error" or loggingLevel == 40: + logging.basicConfig(level=logging.ERROR) #production + else: + logging.warning("Unknown logging level: {}. Choose 'info' or 'error'".format(loggingLevel)) + logging.basicConfig(level=logging.INFO) #development + + #given parameters, + self.prettyName = prettyName #keep this consistent! Settle for one name. + self.supportsSaveStatus = supportsSaveStatus + self.saveCallback = saveCallback + self.exitProgramCallback = exitProgramCallback + self.openOrNewCallback = openOrNewCallback #The host needs to: Create a jack client with ourClientNameUnderNSM - Open the saved file and all its resources + self.broadcastCallback = broadcastCallback + self.hideGUICallback = hideGUICallback + self.showGUICallback = showGUICallback + self.sessionIsLoadedCallback = sessionIsLoadedCallback + + #Reactions get the raw _IncomingMessage OSC object + #A client can add to reactions. + self.reactions = { + "/nsm/client/save" : self._saveCallback, + "/nsm/client/show_optional_gui" : lambda msg: self.showGUICallback(), + "/nsm/client/hide_optional_gui" : lambda msg: self.hideGUICallback(), + "/nsm/client/session_is_loaded" : self._sessionIsLoadedCallback, + #Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument. + #broadcast is handled directly by the function because it has more parameters + } + #self.discardReactions = set(["/nsm/client/session_is_loaded"]) + self.discardReactions = set() + + #Networking and Init + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp + self.sock.bind(('', 0)) #pick a free port on localhost. + ip, port = self.sock.getsockname() + self.ourOscUrl = f"osc.udp://{ip}:{port}/" + + self.executableName = self.getExecutableName() + + #UNIX Signals. Used for quit. + signal(SIGTERM, self.sigtermHandler) #NSM sends only SIGTERM. #TODO: really? pynsm version 1 handled sigkill as well. + signal(SIGINT, self.sigtermHandler) + + #The following instance parameters are all set in announceOurselves + self.serverFeatures = None + self.sessionName = None + self.ourPath = None + self.ourClientNameUnderNSM = None + self.ourClientId = None # the "file extension" of ourClientNameUnderNSM + self.isVisible = None #set in announceGuiVisibility + self.saveStatus = True # true is clean. false means we need saving. + + self.announceOurselves() + + assert self.serverFeatures, self.serverFeatures + assert self.sessionName, self.sessionName + assert self.ourPath, self.ourPath + assert self.ourClientNameUnderNSM, self.ourClientNameUnderNSM + + self.sock.setblocking(False) #We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer. + #After this point the host must include self.reactToMessage in its event loop + + #We assume we are save at startup. + self.announceSaveStatus(isClean = True) + + logger.info("NSMClient client init complete. Going into listening mode.") + + + def reactToMessage(self): + """This is the main loop message. It is added to the clients event loop.""" + try: + data, addr = self.sock.recvfrom(4096) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096. + except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. + return None + + msg = _IncomingMessage(data) + if msg.oscpath in self.reactions: + self.reactions[msg.oscpath](msg) + elif msg.oscpath in self.discardReactions: + pass + elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/open", "Loaded."]: #NSM sends that all programs of the session were loaded. + logger.info ("Got /reply Loaded from NSM Server") + elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/save", "Saved."]: #NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually + logger.info ("Got /reply Saved from NSM Server") + elif msg.isBroadcast: + if self.broadcastCallback: + logger.info (f"Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}") + self.broadcastCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM, msg.oscpath, msg.params) + else: + logger.info (f"No callback for broadcast! Got messagePath {msg.oscpath} and listOfArguments {msg.params}") + elif msg.oscpath == "/error": + logger.warning("Got /error from NSM Server. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) + else: + logger.warning("Reaction not implemented:. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) + + + def send(self, path:str, listOfParameters:list, host=None, port=None): + """Send any osc message. Defaults to nsmd URL. + Will not wait for an answer but return None.""" + if host and port: + url = (host, port) + else: + url = self.nsmOSCUrl + msg = _OutgoingMessage(path) + for arg in listOfParameters: + msg.add_arg(arg) #type is auto-determined by outgoing message + self.sock.sendto(msg.build(), url) + + def getNsmOSCUrl(self): + """Return and save the nsm osc url or raise an error""" + nsmOSCUrl = getenv("NSM_URL") + if not nsmOSCUrl: + raise NSMNotRunningError("New-Session-Manager environment variable $NSM_URL not found.") + else: + #osc.udp://hostname:portnumber/ + o = urlparse(nsmOSCUrl) + #return o.hostname, o.port #this always make the hostname lowercase. usually it does not matter, but we got crash reports. Alternative: + return o.netloc.split(":")[0], o.port + + def getExecutableName(self): + """Finding the actual executable name can be a bit hard + in Python. NSM wants the real starting point, even if + it was a bash script. + """ + #TODO: I really don't know how to find out the name of the bash script + fullPath = argv[0] + assert os.path.dirname(fullPath) in os.environ["PATH"], (fullPath, os.path.dirname(fullPath), os.environ["PATH"]) #NSM requires the executable to be in the path. No excuses. This will never happen since the reference NSM server-GUI already checks for this. + + executableName = os.path.basename(fullPath) + assert not "/" in executableName, executableName #see above. + return executableName + + def announceOurselves(self): + """Say hello to NSM and tell it we are ready to receive + instructions + + /nsm/server/announce s:application_name s:capabilities s:executable_name i:api_version_major i:api_version_minor i:pid""" + + def buildClientFeaturesString(): + #:dirty:switch:progress: + result = [] + if self.supportsSaveStatus: + result.append("dirty") + if self.hideGUICallback and self.showGUICallback: + result.append("optional-gui") + if result: + return ":".join([""] + result + [""]) + else: + return "" + + logger.info("Sending our NSM-announce message") + + announce = _OutgoingMessage("/nsm/server/announce") + announce.add_arg(self.prettyName) #s:application_name + announce.add_arg(buildClientFeaturesString()) #s:capabilities + announce.add_arg(self.executableName) #s:executable_name + announce.add_arg(1) #i:api_version_major + announce.add_arg(2) #i:api_version_minor + announce.add_arg(int(getpid())) #i:pid + hostname, port = self.nsmOSCUrl + assert hostname, self.nsmOSCUrl + assert port, self.nsmOSCUrl + self.sock.sendto(announce.build(), self.nsmOSCUrl) + + #Wait for /reply (aka 'Howdy, what took you so long?) + data, addr = self.sock.recvfrom(1024) + msg = _IncomingMessage(data) + + if msg.oscpath == "/error": + originalMessage, errorCode, reason = msg.params + logger.error("Code {}: {}".format(errorCode, reason)) + quit() + + elif msg.oscpath == "/reply": + nsmAnnouncePath, welcomeMessage, managerName, self.serverFeatures = msg.params + assert nsmAnnouncePath == "/nsm/server/announce", nsmAnnouncePath + logger.info("Got /reply " + welcomeMessage) + + #Wait for /nsm/client/open + data, addr = self.sock.recvfrom(1024) + msg = _IncomingMessage(data) + assert msg.oscpath == "/nsm/client/open", msg.oscpath + self.ourPath, self.sessionName, self.ourClientNameUnderNSM = msg.params + self.ourClientId = os.path.splitext(self.ourClientNameUnderNSM)[1][1:] + logger.info("Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath)) + self.openOrNewCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #Host function to either load an existing session or create a new one. + logger.info("Our client should be done loading or creating the file {}".format(self.ourPath)) + replyToOpen = _OutgoingMessage("/reply") + replyToOpen.add_arg("/nsm/client/open") + replyToOpen.add_arg("{} is opened or created".format(self.prettyName)) + self.sock.sendto(replyToOpen.build(), self.nsmOSCUrl) + else: + raise ValueError("Unexpected message path after announce: {}".format((msg.oscpath, msg.params))) + + def announceGuiVisibility(self, isVisible): + message = "/nsm/client/gui_is_shown" if isVisible else "/nsm/client/gui_is_hidden" + self.isVisible = isVisible + guiVisibility = _OutgoingMessage(message) + logger.info("Telling NSM that our clients switched GUI visibility to: {}".format(message)) + self.sock.sendto(guiVisibility.build(), self.nsmOSCUrl) + + def announceSaveStatus(self, isClean): + """Only send to the NSM Server if there was really a change""" + if not self.supportsSaveStatus: + return + + if not isClean == self.cachedSaveStatus: + message = "/nsm/client/is_clean" if isClean else "/nsm/client/is_dirty" + self.cachedSaveStatus = isClean + saveStatus = _OutgoingMessage(message) + logger.info("Telling NSM that our clients save state is now: {}".format(message)) + self.sock.sendto(saveStatus.build(), self.nsmOSCUrl) + + def _saveCallback(self, msg): + logger.info("Telling our client to save as {}".format(self.ourPath)) + self.saveCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) + replyToSave = _OutgoingMessage("/reply") + replyToSave.add_arg("/nsm/client/save") + replyToSave.add_arg("{} saved".format(self.prettyName)) + self.sock.sendto(replyToSave.build(), self.nsmOSCUrl) + #it is assumed that after saving the state is clear + self.announceSaveStatus(isClean = True) + + + def _sessionIsLoadedCallback(self, msg): + if self.sessionIsLoadedCallback: + logger.info("Received 'Session is Loaded'. Our client supports it. Forwarding message...") + self.sessionIsLoadedCallback() + else: + logger.info("Received 'Session is Loaded'. Our client does not support it, which is the default. Discarding message...") + + def sigtermHandler(self, signal, frame): + """Wait for the user to quit the program + + The user function does not need to exit itself. + Just shutdown audio engines etc. + + It is possible, that the client does not implement quit + properly. In that case NSM protocol demands that we quit anyway. + No excuses. + + Achtung GDB! If you run your program with + gdb --args python foo.py + the Python signal handler will not work. This has nothing to do with this library. + """ + logger.info("Telling our client to quit.") + self.exitProgramCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) + #There is a chance that exitProgramCallback will hang and the program won't quit. However, this is broken design and bad programming. We COULD place a timeout here and just kill after 10s or so, but that would make quitting our responsibility and fixing a broken thing. + #If we reach this point we have reached the point of no return. Say goodbye. + logger.warning("Client did not quit on its own. Sending SIGKILL.") + kill(getpid(), SIGKILL) + logger.error("SIGKILL did nothing. Do it manually.") + + def debugResetDataAndExit(self): + """This is solely meant for debugging and testing. The user way of action should be to + remove the client from the session and add a new instance, which will get a different + NSM-ID. + Afterwards we perform a clean exit.""" + logger.warning("debugResetDataAndExit will now delete {} and then request an exit.".format(self.ourPath)) + if os.path.exists(self.ourPath): + if os.path.isfile(self.ourPath): + try: + os.remove(self.ourPath) + except Exception as e: + logger.info(e) + elif os.path.isdir(self.ourPath): + try: + shutil.rmtree(self.ourPath) + except Exception as e: + logger.info(e) + else: + logger.info("{} does not exist.".format(self.ourPath)) + self.serverSendExitToSelf() + + def serverSendExitToSelf(self): + """If you want a very strict client you can block any non-NSM quit-attempts, like ignoring a + qt closeEvent, and instead send the NSM Server a request to close this client. + This method is a shortcut to do just that. + """ + logger.info("Sending SIGTERM to ourselves to trigger the exit callback.") + #if "server-control" in self.serverFeatures: + # message = _OutgoingMessage("/nsm/server/stop") + # message.add_arg("{}".format(self.ourClientId)) + # self.sock.sendto(message.build(), self.nsmOSCUrl) + #else: + kill(getpid(), SIGTERM) #this calls the exit callback + + def serverSendSaveToSelf(self): + """Some clients want to offer a manual Save function, mostly for psychological reasons. + We offer a clean solution in calling this function which will trigger a round trip over the + NSM server so our client thinks it received a Save instruction. This leads to a clean + state with a good saveStatus and no required extra functionality in the client.""" + + logger.info("instructing the NSM-Server to send Save to ourselves.") + if "server-control" in self.serverFeatures: + #message = _OutgoingMessage("/nsm/server/save") # "Save All" Command. + message = _OutgoingMessage("/nsm/gui/client/save") + message.add_arg("{}".format(self.ourClientId)) + self.sock.sendto(message.build(), self.nsmOSCUrl) + else: + logger.warning("...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures)) + + def changeLabel(self, label:str): + """This function is implemented because it is provided by NSM. However, it does not much. + The message gets received but is not saved. + The official NSM GUI uses it but then does not save it. + We would have to send it every startup ourselves. + + This is fine for us as clients, but you need to provide a GUI field to enter that label.""" + logger.info("Telling the NSM-Server that our label is now " + label) + message = _OutgoingMessage("/nsm/client/label") + message.add_arg(label) #s:label + self.sock.sendto(message.build(), self.nsmOSCUrl) + + def broadcast(self, path:str, arguments:list): + """/nsm/server/broadcast s:path [arguments...] + We, as sender, will not receive the broadcast back. + + Broadcasts starting with /nsm are not allowed and will get discarded by the server + """ + if path.startswith("/nsm"): + logger.warning("Attempted broadbast starting with /nsm. Not allwoed") + else: + logger.info("Sending broadcast " + path + repr(arguments)) + message = _OutgoingMessage("/nsm/server/broadcast") + message.add_arg(path) + for arg in arguments: + message.add_arg(arg) #type autodetect + self.sock.sendto(message.build(), self.nsmOSCUrl) + + def importResource(self, filePath): + """aka. import into session + + ATTENTION! You will still receive an absolute path from this function. You need to make + sure yourself that this path will not be saved in your save file, but rather use a place- + holder that gets replaced by the actual session path each time. A good point is after + serialisation. search&replace for the session prefix ("ourPath") and replace it with a tag + e.g. . The opposite during load. + Only such a behaviour will make your session portable. + + Do not use the following pattern: An alternative that comes to mind is to only work with + relative paths and force your programs workdir to the session directory. Better work with + absolute paths internally . + + Symlinks given path into session dir and returns the linked path relative to the ourPath. + It can handles single files as well as whole directories. + + if filePath is already a symlink we do not follow it. os.path.realpath or os.readlink will + not be used. + + Multilayer links may indicate a users ordering system that depends on + abstractions. e.g. with mounted drives under different names which get symlinked to a + reliable path. + + Basically do not question the type of our input filePath. + + tar with the follow symlink option has os.path.realpath behaviour and therefore is able + to follow multiple levels of links anyway. + + A hardlink does not count as a link and will be detected and treated as real file. + + Cleaning up a session directory is either responsibility of the user + or of our client program. We do not provide any means to unlink or delete files from the + session directory. + """ + + #Even if the project was not saved yet now it is time to make our directory in the NSM dir. + if not os.path.exists(self.ourPath): + os.makedirs(self.ourPath) + + filePath = os.path.abspath(filePath) #includes normalisation + if not os.path.exists(self.ourPath):raise FileNotFoundError(self.ourPath) + if not os.path.isdir(self.ourPath): raise NotADirectoryError(self.ourPath) + if not os.access(self.ourPath, os.W_OK): raise PermissionError("not writable", self.ourPath) + + if not os.path.exists(filePath):raise FileNotFoundError(filePath) + if os.path.isdir(filePath): raise IsADirectoryError(filePath) + if not os.access(filePath, os.R_OK): raise PermissionError("not readable", filePath) + + filePathInOurSession = os.path.commonprefix([filePath, self.ourPath]) == self.ourPath + linkedPath = os.path.join(self.ourPath, os.path.basename(filePath)) + linkedPathAlreadyExists = os.path.exists(linkedPath) + + if not os.access(os.path.dirname(linkedPath), os.W_OK): raise PermissionError("not writable", os.path.dirname(linkedPath)) + + + if filePathInOurSession: + #loadResource from our session dir. Portable session, manually copied beforehand or just loading a link again. + linkedPath = filePath #we could return here, but we continue to get the tests below. + logger.info(f"tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ") + + elif linkedPathAlreadyExists and os.readlink(linkedPath) == filePath: + #the imported file already exists as link in our session dir. We do not link it again but simply report the existing link. + #We only check for the first target of the existing link and do not follow it through to a real file. + #This way all user abstractions and file structures will be honored. + linkedPath = linkedPath + logger.info(f"tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath} ") + + elif linkedPathAlreadyExists: + #A new file shall be imported but it would create a linked name which already exists in our session dir. + #Because we already checked for a new link to the same file above this means actually linking a different file so we need to differentiate with a unique name + firstpart, extension = os.path.splitext(linkedPath) + uniqueLinkedPath = firstpart + "." + uuid4().hex + extension + assert not os.path.exists(uniqueLinkedPath) + os.symlink(filePath, uniqueLinkedPath) + logger.info(self.ourClientNameUnderNSM + f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.") + linkedPath = uniqueLinkedPath + + else: #this is the "normal" case. External resources will be linked. + assert not os.path.exists(linkedPath) + os.symlink(filePath, linkedPath) + logger.info(f"imported external resource {filePath} as link {linkedPath}") + + assert os.path.exists(linkedPath), linkedPath + return linkedPath + +class NullClient(object): + """Use this as a drop-in replacement if your program has a mode without NSM but you don't want + to change the code itself. + This was originally written for programs that have a core-engine and normal mode of operations + is a GUI with NSM but they also support commandline-scripts and batch processing. + For these you don't want NSM.""" + + def __init__(self, *args, **kwargs): + self.realClient = False + self.ourClientNameUnderNSM = "NSM Null Client" + + def announceSaveStatus(self, *args): + pass + + def announceGuiVisibility(self, *args): + pass + + def reactToMessage(self): + pass + + def importResource(self): + return "" + + def serverSendExitToSelf(self): + quit() diff --git a/template/qtgui/nsmsingleserver.py b/template/qtgui/nsmsingleserver.py new file mode 100644 index 0000000..c31b1b6 --- /dev/null +++ b/template/qtgui/nsmsingleserver.py @@ -0,0 +1,119 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +import os, socket, asyncio +from signal import signal, SIGTERM, SIGUSR1 +from threading import Thread +from sys import argv + +from .nsmclient import _IncomingMessage, _OutgoingMessage + + + +class NSMProtocol(asyncio.DatagramProtocol): + directory = None + addr = None #a cache + + def __init__(self): + super().__init__() + def connection_made(self, transport): + self.transport = transport + def datagram_received(self, data, addr): + NSMProtocol.addr = addr + + msg = _IncomingMessage(data) + if msg.oscpath == "/nsm/server/announce": + application_name, capabilities, executable_name, api_version_major, api_version_minor, pid = msg.params + NSMProtocol.pid = pid + reply = _OutgoingMessage("/reply") + reply.add_arg("/nsm/server/announce") + reply.add_arg("Welcome!") + reply.add_arg("Fake Save Server") + reply.add_arg("server-control:") + self.send(reply, addr) + + #['/home/user/NSM Sessions/dev-example/QtCboxNsm Exämple ツ.nXDBM', 'dev-example', 'QtCboxNsm Exämple ツ.nXDBM'] + openMsg = reply = _OutgoingMessage("/nsm/client/open") + openMsg.add_arg(NSMProtocol.directory) + openMsg.add_arg("NOT-A-SESSION") + openMsg.add_arg(application_name) + self.send(openMsg, addr) + + self.send(_OutgoingMessage("/nsm/client/show_optional_gui"), addr) + + elif msg.oscpath == "/nsm/gui/client/save": + self.send(_OutgoingMessage("/nsm/client/save"), addr) + elif msg.oscpath == "/nsm/client/gui_is_hidden": + os.kill(NSMProtocol.pid, SIGTERM) + elif msg.oscpath == "/nsm/server/stop": + os.kill(NSMProtocol.pid, SIGTERM) + #else: + # print (msg.oscpath, msg.params) + + def send(self, message, addr): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(message.build(), addr) + + @staticmethod + def staticSave(*args): + NSMProtocol.send(None, _OutgoingMessage("/nsm/client/save"), NSMProtocol.addr) + + +def startSingleNSMServer(directory): + """Set all paths like NSM would receive them and nsmclient.py expects them.""" + + serverSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + serverSock.bind(('', 0)) # Bind to a free port provided by the host. + SERVER_PORT = serverSock.getsockname()[1] + NSMProtocol.directory = directory + serverSock.close() + + os.environ["NSM_URL"] = f"osc.udp://localhost:{SERVER_PORT}/" + executableName = os.path.basename(argv[0]) + executableDir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + ##assert os.path.exists(os.path.join(executableDir, executableName)) not valid anymore with zipapp. But it worked for years, so I guess the code is ok. + argv[0] = os.path.join(executableDir, executableName) #NSM speciality. nsmclient exlicitely checks for this + os.environ["PATH"] = os.environ["PATH"] + ":" + executableDir + #print (argv[0]) + #print (executableName) + #print (executableDir) + #print(os.environ["PATH"]) + #print(os.environ["NSM_URL"]) + + #loop = asyncio.get_event_loop() + #loop.create_task(asyncio.start_server(handle_client, 'localhost', SERVER_PORT)) + #loop.run_forever() + #asyncio.run(asyncio.start_server(handle_client, 'localhost', SERVER_PORT)) + + logger.info(f"Starting fake NSM server on port {SERVER_PORT}") + + #For Carla: + signal(SIGUSR1, NSMProtocol.staticSave) + + loop = asyncio.get_event_loop() + def run_loop(loop): + asyncio.set_event_loop(loop) + t = loop.create_datagram_endpoint(NSMProtocol,local_addr=('127.0.0.1',SERVER_PORT), family=socket.AF_INET) + loop.run_until_complete(t) + loop.run_forever() + Thread(target=lambda: run_loop(loop), daemon=True).start() #Daemon makes the thread just stop when main thread ends. diff --git a/template/qtgui/resources.py b/template/qtgui/resources.py new file mode 100644 index 0000000..ceab62f --- /dev/null +++ b/template/qtgui/resources.py @@ -0,0 +1,3585 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.0) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\xc6\xb0\ +\x00\ +\x01\x00\x00\x00\x0f\x00\x80\x00\x03\x00\x70\x46\x46\x54\x4d\x49\ +\xeb\x7b\x7e\x00\x00\xc6\x94\x00\x00\x00\x1c\x47\x44\x45\x46\x01\ +\xf0\x02\x9a\x00\x00\xc3\x50\x00\x00\x00\x48\x47\x50\x4f\x53\x8b\ +\x53\x9a\x33\x00\x00\xc5\xa4\x00\x00\x00\xee\x47\x53\x55\x42\x48\ +\x47\x1f\xb8\x00\x00\xc3\x98\x00\x00\x02\x0c\x4f\x53\x2f\x32\xa7\ +\x2d\x81\xee\x00\x00\x01\x78\x00\x00\x00\x56\x63\x6d\x61\x70\x70\ +\x7e\x24\xa4\x00\x00\x06\x20\x00\x00\x02\xb2\x67\x61\x73\x70\xff\ +\xff\x00\x03\x00\x00\xc3\x48\x00\x00\x00\x08\x67\x6c\x79\x66\x3d\ +\xa4\xde\x6e\x00\x00\x0b\x00\x00\x00\x76\xd0\x68\x65\x61\x64\xf6\ +\x63\x42\xf5\x00\x00\x00\xfc\x00\x00\x00\x36\x68\x68\x65\x61\x19\ +\x1b\x0d\xa7\x00\x00\x01\x34\x00\x00\x00\x24\x68\x6d\x74\x78\x56\ +\xf6\x59\x13\x00\x00\x01\xd0\x00\x00\x04\x50\x6c\x6f\x63\x61\xdd\ +\x6f\xc0\x6e\x00\x00\x08\xd4\x00\x00\x02\x2a\x6d\x61\x78\x70\x01\ +\x76\x01\x72\x00\x00\x01\x58\x00\x00\x00\x20\x6e\x61\x6d\x65\x4c\ +\x2c\x61\x0f\x00\x00\x81\xd0\x00\x00\x36\xc9\x70\x6f\x73\x74\x8f\ +\x05\xee\x61\x00\x00\xb8\x9c\x00\x00\x0a\xa9\x00\x01\x00\x00\x00\ +\x01\x19\x99\xf7\x59\x13\xfd\x5f\x0f\x3c\xf5\x00\x0b\x08\x00\x00\ +\x00\x00\x00\xc4\xa0\xef\x5f\x00\x00\x00\x00\xc4\xa0\xef\x5f\xfc\ +\x78\xfb\x43\x11\x8a\x0a\x5e\x00\x00\x00\x08\x00\x02\x00\x00\x00\ +\x00\x00\x00\x00\x01\x00\x00\x0a\x5e\xfb\x43\x00\xb8\x12\x52\xfc\ +\x78\xfe\xfd\x11\x8a\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x01\x14\x00\x01\x00\x00\x01\x14\x00\xc8\x00\ +\x15\x00\xa6\x00\x07\x00\x02\x00\x00\x00\x01\x00\x01\x00\x00\x00\ +\x40\x00\x00\x00\x04\x00\x01\x00\x01\x04\xb1\x01\x90\x00\x05\x00\ +\x00\x05\x33\x05\x99\x00\x00\x01\x1e\x05\x33\x05\x99\x00\x00\x03\ +\xd7\x00\x66\x02\x12\x00\x00\x02\x00\x06\x03\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x12\x00\x40\x00\x01\x00\x00\x00\x00\x00\x00\ +\x00\x50\x66\x45\x64\x00\x40\x00\x20\xff\xff\x06\x66\xfe\x66\x00\ +\xb8\x0a\x5e\x04\xbd\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x04\ +\xe2\x00\xc8\x00\x00\x00\x00\x02\xaa\x00\x00\x03\x4d\x00\x00\x03\ +\x0c\x00\xc8\x03\x0c\x00\xc8\x03\x0c\x00\xc8\x06\x7c\x00\x78\x08\ +\xde\x00\x78\x06\x32\x00\x8b\x08\x08\x00\x8b\x07\xee\x00\x92\x07\ +\x92\x00\x92\x05\xe0\x00\xb8\x06\x48\x00\x8b\x07\xce\x00\xb8\x0b\ +\xb8\x00\xc8\x06\xef\x00\xc8\x05\xe5\x00\x90\x05\xe5\x00\x90\x05\ +\xe5\x00\x90\x03\x48\x00\xc8\x05\x9a\x00\xc8\x0a\xe3\x00\xbe\x0b\ +\x18\x00\xc9\x12\x52\x00\xc8\x08\x02\x00\xc8\x0d\x9e\x00\xc8\x01\ +\x7a\x00\x1c\x04\xd8\x00\xc8\x04\xd8\x00\xc8\x04\xd8\x00\xc8\x03\ +\x0c\x00\xc8\x03\xb6\x00\xc8\x03\x0c\x00\xc8\x04\xce\x00\xc8\x04\ +\x38\x00\x50\x04\x38\x00\xd7\x04\x38\x00\x54\x04\x38\x00\x50\x03\ +\xa4\x00\x64\x04\x85\x00\x4b\x03\x55\x00\x46\x01\x62\x00\x5a\x03\ +\x4e\x00\x5b\x02\xe6\x00\x5b\x02\xac\xff\x65\x04\xe9\x00\x54\x03\ +\xf0\x00\x54\x03\x6d\x00\x15\x02\x9a\x00\x54\x02\xb2\x00\x5b\x03\ +\x0d\x00\x4b\x03\x24\x00\x54\x01\xe0\x00\xc8\x02\xa8\x00\xc8\x03\ +\x4c\x00\xc8\x03\x4c\x00\xc8\x01\xe0\x00\xc8\x01\xe0\x00\xc8\x04\ +\xb0\x00\xc8\x04\xb0\x00\xc8\x02\x58\x00\xc8\x08\xe8\x00\x96\x08\ +\xe8\x00\x96\x04\xf3\x00\x9d\x05\x62\x00\x96\x04\xe2\x00\xbe\x04\ +\xe2\x00\xbe\x06\x97\x00\xbe\x05\x7a\x00\xa5\x05\x7a\x00\xa5\x02\ +\x95\x00\xb1\x06\x97\x00\xbe\x02\x60\x00\x8c\x03\x40\x00\xc8\x06\ +\x40\x00\x00\x06\x40\x00\x00\x06\x40\x00\x00\x06\x40\x00\x00\x06\ +\x40\x00\x00\x06\x40\x00\x00\x06\xd6\x00\xc8\x04\xce\x00\xc8\x05\ +\x60\x00\xb8\x05\x60\x00\xb8\x05\x60\x00\xb8\x06\x1e\x00\xc8\x06\ +\x2f\x00\x90\x06\x2f\x00\x90\x06\x2f\x00\x90\x04\x32\x00\xc8\x03\ +\x9c\x00\xc8\x0b\xb8\x00\xc8\x03\x4c\x00\xc8\x04\x4c\x00\xc8\x03\ +\x0c\x00\x82\x03\x0c\x00\x82\x03\x0c\x00\x82\x03\x0c\x00\xc8\x03\ +\x0c\x00\xc8\x03\x0c\x00\xc8\x02\x9a\x00\x6c\x02\x9a\x00\x3b\x03\ +\xda\x00\xa4\x03\xda\x00\xa4\x05\xfe\x00\xc8\x05\xc4\x00\xc8\x07\ +\xc0\x00\xc8\x07\x83\x00\xc8\x05\x5f\x00\xbe\x05\x5f\x00\xbe\x05\ +\x5f\x00\xbe\x03\x2c\x00\xc0\x03\x5e\x00\xc3\x03\xe2\x00\xc3\x04\ +\x66\x00\xc3\x04\xea\x00\xc3\x05\x6e\x00\xc3\x03\x4d\x00\xc9\x03\ +\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\ +\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\ +\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\ +\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x9d\x00\xc9\x03\x9d\x00\xc9\x03\ +\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\xd9\x00\xc9\x03\ +\xd9\x00\xc9\x03\x4d\x00\x00\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x05\ +\x54\x00\xc8\x04\x33\x00\xc9\x03\xd9\x00\xc9\x03\xd9\x00\xc9\x05\ +\xa4\x00\xc9\x05\xa4\x00\xc9\x05\xa4\x00\xc9\x05\xa4\x00\xc9\x05\ +\xa4\x00\xc9\x00\x00\xfe\xf2\x00\x00\xfe\x37\x00\x00\xfd\xdf\x00\ +\x00\xfd\xdf\x00\x00\xfd\xdf\x05\x78\x00\xaf\x05\x78\x00\xaf\x05\ +\x78\x00\xaf\x00\x00\xff\xd8\x00\x00\xff\x38\x00\x00\xff\x38\x00\ +\x00\xfe\xf2\x00\x00\xfe\xf2\x00\x00\xfe\xf2\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfc\xbf\x00\ +\x00\xfd\xc4\x00\x00\xfc\xf0\x00\x00\xfd\x9c\x00\x00\xfd\x2c\x00\ +\x00\xfd\x2c\x00\x00\xfc\xbf\x00\x00\xfc\xf0\x02\x76\x00\xc8\x02\ +\x76\x00\xb4\x00\x00\xfc\xab\x00\x00\xfc\xab\x00\x00\xfd\x12\x00\ +\x00\xfd\x6c\x00\x00\xfd\x07\x00\x00\xfc\x78\x00\x00\xfc\x78\x03\ +\x5e\x00\xb4\x03\x58\x00\xb8\x03\xc0\x00\x8c\x04\x1a\x00\x78\x05\ +\x75\x00\x92\x04\x90\x00\x8b\x09\xba\x00\xc8\x09\xba\x00\xc8\x04\ +\x6a\x00\xc8\x04\x6a\x00\xc8\x05\x4c\x00\xb9\x05\xae\x00\xc1\x05\ +\xae\x00\xc1\x05\xae\x00\xc1\x03\x6e\x00\xc0\x02\xc0\x00\xc8\x04\ +\x6d\x00\xc7\x04\xfe\x00\xc7\x02\x90\x00\xc7\x03\x35\x00\xc7\x01\ +\xe5\x00\xc8\x05\x11\x00\xc8\x04\x31\x00\xc8\x04\x88\x00\xc7\x04\ +\x8a\x00\xc8\x03\xf6\x00\xc8\x06\x40\x00\xc8\x06\x40\x00\xc8\x04\ +\x1a\x00\xc8\x03\x1a\x00\xb4\x00\x00\xfd\x29\x00\x00\xfd\x6b\x00\ +\x00\xfd\xa1\x00\x00\xfd\x65\x0a\x24\x00\xa3\x05\xea\x00\xb4\x0d\ +\x9e\x00\xc8\x06\x34\x00\xc8\x06\x34\x00\xbe\x04\xb8\x00\xb4\x03\ +\xec\x00\xb4\x04\xc0\x00\xb4\x06\x22\x00\xc9\x04\x2e\x00\xc9\x04\ +\x2e\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\x4d\x00\xc9\x03\ +\x4d\x00\xc9\x04\x5d\x00\xc9\x04\x5d\x00\xc9\x04\x5d\x00\xc9\x04\ +\x5d\x00\xc9\x01\xd6\x00\xc8\x01\xd6\x00\xc8\x01\xd6\x00\xc8\x01\ +\xd6\x00\xc8\x01\xd6\x00\xc8\x02\xd7\x00\xc8\x04\x10\x00\xb4\x04\ +\x10\x00\xb4\x04\x10\x00\xb4\x03\xac\x00\xb4\x03\xac\x00\xb4\x03\ +\xac\x00\xb4\x03\xac\x00\xb4\x03\xac\x00\xb4\x03\xf7\x00\xb5\x02\ +\xb7\x00\xc9\x04\x47\x00\xc9\x02\xb7\x00\xc9\x02\xb7\x00\xc9\x02\ +\xb7\x00\xca\x03\xbf\x00\xc9\x03\xbf\x00\xc9\x05\x67\x00\xc9\x04\ +\xa9\x00\xc9\x06\x0e\x00\xc9\x07\xec\x00\xc9\x05\x45\x00\xc9\x06\ +\xef\x00\xca\x05\x67\x00\xc9\x04\xfe\x00\xc7\x04\x6d\x00\xc7\x00\ +\x00\x00\x05\x00\x00\x00\x03\x00\x00\x00\x2c\x00\x00\x00\x04\x00\ +\x00\x00\xc4\x00\x01\x00\x00\x00\x00\x01\xac\x00\x03\x00\x01\x00\ +\x00\x00\x2c\x00\x03\x00\x0a\x00\x00\x00\xc4\x00\x04\x00\x98\x00\ +\x00\x00\x22\x00\x20\x00\x04\x00\x02\x00\x20\x26\x6f\xe0\x14\xe0\ +\x21\xe0\x25\xe0\x28\xe0\x33\xe0\x42\xe0\x4a\xe0\x51\xe0\x55\xe0\ +\x5d\xe0\x5f\xe0\x62\xe0\x65\xe0\x69\xff\xff\x00\x00\x00\x20\x26\ +\x6d\xe0\x00\xe0\x18\xe0\x25\xe0\x28\xe0\x32\xe0\x42\xe0\x4a\xe0\ +\x50\xe0\x55\xe0\x5c\xe0\x5f\xe0\x61\xe0\x65\xe0\x69\xff\xff\xff\ +\xe3\xd9\x97\x20\x07\x20\x04\x20\x01\x1f\xff\x1f\xf6\x1f\xe8\x1f\ +\xe1\x1f\xdc\x1f\xd9\x1f\xd3\x1f\xd2\x1f\xd1\x1f\xcf\x1f\xcc\x00\ +\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\xe8\x00\x00\x00\x00\x00\ +\x00\x00\x12\x00\x00\x00\x20\x00\x00\x00\x20\x00\x00\x00\x03\x00\ +\x00\x26\x6d\x00\x00\x26\x6f\x00\x00\x00\x04\x00\x00\xe0\x00\x00\ +\x00\xe0\x14\x00\x00\x00\x07\x00\x00\xe0\x18\x00\x00\xe0\x21\x00\ +\x00\x00\x1c\x00\x00\xe0\x25\x00\x00\xe0\x25\x00\x00\x00\x26\x00\ +\x00\xe0\x28\x00\x00\xe0\x28\x00\x00\x00\x27\x00\x00\xe0\x32\x00\ +\x00\xe0\x33\x00\x00\x00\x28\x00\x00\xe0\x42\x00\x00\xe0\x42\x00\ +\x00\x00\x2a\x00\x00\xe0\x4a\x00\x00\xe0\x4a\x00\x00\x00\x2b\x00\ +\x00\xe0\x50\x00\x00\xe0\x51\x00\x00\x00\x2c\x00\x00\xe0\x55\x00\ +\x00\xe0\x55\x00\x00\x00\x2e\x00\x00\xe0\x5c\x00\x00\xe0\x5d\x00\ +\x00\x00\x2f\x00\x00\xe0\x5f\x00\x00\xe0\x5f\x00\x00\x00\x31\x00\ +\x00\xe0\x61\x00\x00\xe0\x62\x00\x00\x00\x32\x00\x00\xe0\x65\x00\ +\x00\xe0\x65\x00\x00\x00\x34\x00\x00\xe0\x69\x00\x00\xe0\x69\x00\ +\x00\x00\x35\x00\x01\xd1\x00\x00\x01\xd1\x26\x00\x00\x00\x36\x00\ +\x01\xd1\x29\x00\x01\xd1\xdd\x00\x00\x00\x5d\x00\x00\x01\x06\x00\ +\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x02\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\ +\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x94\x00\x94\x00\x94\x00\x94\x00\xbe\x00\ +\xd8\x01\x0c\x01\x9c\x02\x6c\x03\x08\x03\xea\x04\x90\x05\x3c\x05\ +\xd4\x06\x5e\x07\x2e\x07\x46\x07\x5a\x07\xc8\x07\xd4\x07\xe0\x08\ +\x2c\x08\x74\x08\xb6\x08\xda\x09\x00\x09\x1a\x09\x3c\x09\x7c\x09\ +\xaa\x09\xe8\x0a\x30\x0a\x52\x0a\x98\x0a\xc2\x0b\x10\x0b\x52\x0b\ +\x96\x0b\xe4\x0c\x3c\x0c\x86\x0c\xda\x0d\x44\x0d\x56\x0d\xa0\x0d\ +\xf0\x0e\x44\x0e\xa2\x0e\xe8\x0f\x38\x0f\x6e\x0f\xb8\x10\x00\x10\ +\x40\x10\x4e\x10\x62\x10\x76\x10\x8a\x10\xae\x10\xbc\x10\xc8\x10\ +\xd4\x10\xf2\x11\xc6\x12\x76\x12\xe4\x13\x46\x13\x56\x13\x7e\x13\ +\xb0\x13\xe6\x14\x20\x14\x42\x14\x5a\x14\xac\x14\xd8\x14\xe6\x14\ +\xfa\x15\x14\x15\x34\x15\x5c\x15\x8a\x16\x0a\x16\x5e\x17\x08\x17\ +\x14\x17\x20\x17\xaa\x18\x02\x18\x0e\x18\x1a\x18\x2e\x18\x44\x18\ +\x4c\x18\x68\x18\xb4\x18\xe6\x19\x18\x19\x3a\x19\x5c\x19\xa0\x19\ +\xe4\x1a\x22\x1a\x56\x1a\x9c\x1a\xec\x1b\x9c\x1c\x56\x1d\x4c\x1e\ +\x46\x1e\x5c\x1e\x6e\x1e\x80\x1e\xc6\x1e\xec\x1f\x2c\x1f\x84\x1f\ +\xf8\x20\x84\x20\x9e\x20\xb2\x20\xe4\x20\xfa\x21\x08\x21\x1a\x21\ +\x28\x21\x3a\x21\x46\x21\x5a\x21\x68\x21\x7c\x21\x88\x21\x9a\x21\ +\xa8\x21\xc6\x21\xd8\x21\xf4\x22\x08\x22\x3a\x22\x62\x22\x76\x22\ +\x76\x22\x8c\x22\x9a\x22\xe2\x23\x10\x23\x3c\x23\x54\x23\x8e\x23\ +\xde\x24\x4c\x24\xd6\x25\x7c\x25\x8a\x25\xac\x25\xbc\x25\xd2\x25\ +\xf0\x25\xfe\x26\x14\x26\x32\x26\x44\x26\x70\x26\xb0\x27\x10\x27\ +\x8c\x28\x22\x28\x22\x28\x22\x28\x22\x28\x22\x28\x22\x28\x22\x28\ +\x22\x28\x22\x28\x34\x28\x46\x28\x54\x28\x62\x28\x74\x28\x92\x28\ +\xb0\x28\xca\x29\x4e\x29\xcc\x29\xf0\x2a\x10\x2a\x24\x2a\x58\x2a\ +\x76\x2a\xa6\x2a\xe2\x2b\x18\x2b\x62\x2b\xa2\x2b\xf0\x2c\x4e\x2c\ +\xa2\x2c\xb8\x2c\xce\x2d\x1a\x2d\x52\x2d\xc4\x2e\x14\x2e\x66\x2e\ +\xc0\x2f\x10\x2f\x36\x2f\x5a\x2f\x88\x2f\xb0\x2f\xea\x2f\xf8\x30\ +\x32\x30\x72\x30\xa6\x30\xd8\x31\x0e\x31\x2a\x31\x44\x31\x54\x31\ +\x8e\x31\xa2\x31\xb4\x31\xd2\x32\x00\x33\x02\x33\xfc\x34\x18\x34\ +\x90\x35\x08\x35\x3a\x35\x80\x35\xe0\x35\xfe\x36\x1c\x36\x38\x36\ +\x4e\x36\x5c\x36\x76\x36\x88\x36\xc4\x36\xf8\x37\x4a\x37\x94\x37\ +\xa2\x37\xb0\x37\xbe\x37\xcc\x37\xda\x37\xea\x38\x16\x38\x36\x38\ +\x68\x38\x96\x38\xb8\x38\xda\x39\x0a\x39\x3c\x39\x76\x39\x8a\x39\ +\xa6\x39\xbe\x39\xce\x39\xe0\x39\xf8\x3a\x10\x3a\x30\x3a\x4e\x3a\ +\x74\x3a\xa0\x3a\xc2\x3a\xec\x3b\x0e\x3b\x40\x3b\x68\x00\x00\x00\ +\x05\x00\xc8\x00\x00\x04\x1a\x05\xcd\x00\x03\x00\x1d\x00\x28\x00\ +\x61\x00\x6b\x00\x00\x13\x21\x11\x21\x01\x2e\x01\x35\x34\x36\x37\ +\x1e\x02\x17\x06\x23\x22\x2e\x02\x35\x34\x37\x16\x17\x0e\x01\x15\ +\x14\x37\x32\x33\x32\x16\x15\x14\x07\x2e\x02\x27\x37\x3e\x01\x35\ +\x34\x26\x23\x22\x0e\x02\x15\x14\x17\x0e\x03\x15\x14\x16\x33\x32\ +\x37\x16\x15\x14\x06\x23\x22\x26\x27\x36\x35\x34\x26\x23\x22\x06\ +\x15\x14\x16\x33\x32\x36\x35\x34\x27\x3e\x01\x35\x34\x26\x23\x22\ +\x07\x27\x26\x35\x34\x36\x33\x32\x16\x15\x14\xc8\x03\x52\xfc\xae\ +\x01\xb2\x2d\x15\x27\x20\x07\x1f\x14\x08\x22\x27\x1c\x40\x39\x26\ +\x91\x18\x03\x2a\x2f\x8e\x04\x04\x34\x44\x3f\x08\x14\x1d\x3c\x02\ +\x49\x43\x31\x1a\x24\x37\x1d\x0d\x0f\x28\x35\x30\x18\x80\x67\x29\ +\x25\x16\x2c\x2e\x20\x1c\x08\x43\x2b\x22\x27\x27\x4f\x4c\x3c\x41\ +\x19\x2e\x37\x68\x3e\x0f\x0e\x32\x07\x43\x17\x18\x15\x05\xcd\xfa\ +\x33\x02\x05\x1d\x23\x2c\x0e\x32\x0b\x1b\x6d\x48\x21\x0f\x1d\x30\ +\x4b\x29\x6e\x82\x54\x0e\x12\x55\x30\x65\xb4\x34\x2f\x57\x2e\x21\ +\x48\x64\xde\x02\x3d\x89\x5d\x42\x81\x33\x4f\x4e\x24\x65\x4e\x24\ +\x3a\x49\x52\x2f\x71\x90\x0e\x6a\x6a\x26\x42\x14\x15\x04\x3c\x23\ +\x29\x2b\x27\x3f\x48\x56\x33\x72\x73\x1a\x57\x33\x54\x66\x03\xcd\ +\x33\x5e\x28\x7d\x41\x29\x5a\x00\x02\x00\xc8\xff\xd8\x02\x44\x03\ +\xe1\x00\x0d\x00\x19\x00\x00\x17\x11\x33\x11\x3e\x02\x33\x32\x16\ +\x15\x14\x06\x07\x19\x01\x3e\x03\x35\x34\x23\x22\x0e\x01\xc8\x46\ +\x25\x25\x40\x2a\x42\x40\xa1\x95\x2a\x39\x36\x1b\x32\x1a\x22\x26\ +\x28\x04\x09\xfd\x8b\x1c\x18\x13\x5c\x43\x49\x9a\x59\x01\x49\xff\ +\x00\x1e\x32\x40\x47\x27\x44\x0c\x1e\x00\x00\x00\x02\x00\xc8\xfe\ +\x8d\x02\x44\x03\x2f\x00\x07\x00\x0b\x00\x00\x17\x11\x33\x11\x25\ +\x11\x23\x11\x03\x15\x37\x35\xc8\x46\x01\x36\x46\xf0\xf0\x28\x03\ +\x57\xfe\x8c\x29\xfc\xa9\x01\x74\x01\x2d\xbb\x1b\xbb\x00\x00\x00\ +\x02\x00\xc8\xfe\x84\x02\x44\x03\x38\x00\x1b\x00\x1f\x00\x00\x05\ +\x11\x23\x11\x07\x35\x37\x35\x07\x35\x37\x11\x33\x11\x37\x11\x33\ +\x11\x37\x15\x07\x15\x37\x15\x07\x11\x23\x11\x27\x37\x35\x07\x01\ +\x54\x46\x46\x46\x46\x46\x46\x64\x46\x46\x46\x46\x46\x46\x64\x64\ +\x64\x16\xfe\x9a\x01\x5d\x09\x8c\x09\xc2\x09\x8c\x09\x01\x3f\xfe\ +\xca\x0e\x01\x66\xfe\xa3\x09\x8c\x09\xc2\x09\x8c\x09\xfe\xc1\x01\ +\x36\x7e\x0e\xc2\x0e\x00\x00\x00\x03\x00\x78\x00\x00\x05\xed\x03\ +\x89\x00\x10\x00\x23\x00\x67\x00\x00\x01\x07\x06\x15\x14\x33\x32\ +\x36\x37\x36\x35\x34\x23\x22\x0e\x02\x05\x07\x06\x15\x14\x33\x32\ +\x3e\x02\x37\x36\x35\x34\x23\x22\x0e\x02\x05\x14\x0e\x04\x23\x22\ +\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\x06\x07\x0e\x04\x23\ +\x22\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\x06\x07\x27\x3e\ +\x03\x33\x32\x17\x3e\x01\x33\x32\x16\x17\x3e\x01\x33\x32\x17\x3e\ +\x01\x33\x32\x1e\x02\x02\x18\x32\x08\x36\x2e\x66\x19\x14\x47\x10\ +\x1f\x24\x1c\x02\x6f\x32\x07\x35\x14\x2c\x33\x2d\x0e\x14\x48\x10\ +\x1e\x24\x1d\x01\x57\x0c\x1a\x30\x41\x62\x3b\x4a\x32\x34\x7d\xfe\ +\x6a\x82\x8d\x07\x15\x11\x4a\x25\x07\x1a\x30\x41\x62\x3b\x49\x33\ +\x33\x7c\xfe\x6a\x82\x8e\x07\x15\x13\x50\x27\x38\x1b\x35\x35\x56\ +\x3c\x46\x07\x33\x48\x3e\x43\x4d\x0c\x26\x5e\x51\x46\x06\x33\x49\ +\x3e\x2a\x40\x25\x11\x02\xda\xfa\x26\x1e\x4f\x6b\x80\x69\x39\x6e\ +\x08\x15\x2f\x22\xfa\x26\x1e\x4f\x15\x2f\x63\x44\x69\x39\x6e\x08\ +\x15\x2f\x53\x26\x53\x61\x55\x47\x2a\x36\xff\x00\x3f\x3f\x02\xc1\ +\x25\x6a\x63\x2f\x58\x5b\x43\x2a\x36\xff\x00\x3f\x3f\x02\xc1\x25\ +\x77\x70\x18\x44\x71\x51\x2d\x41\x26\x1b\x54\x47\x4f\x4c\x41\x26\ +\x1b\x24\x3f\x4e\x00\x00\x00\x00\x04\x00\x78\x00\x00\x08\x4f\x03\ +\x89\x00\x12\x00\x75\x00\x88\x00\x99\x00\x00\x01\x07\x06\x15\x14\ +\x33\x32\x3e\x02\x37\x36\x35\x34\x23\x22\x0e\x02\x05\x14\x0e\x04\ +\x23\x22\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\x06\x07\x0e\ +\x05\x23\x22\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\x06\x07\ +\x0e\x04\x23\x22\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\x06\ +\x07\x27\x3e\x03\x33\x32\x17\x3e\x01\x33\x32\x16\x17\x3e\x01\x33\ +\x32\x17\x3e\x01\x33\x32\x17\x3e\x01\x33\x32\x17\x3e\x01\x33\x32\ +\x1e\x02\x25\x07\x06\x15\x14\x33\x32\x3e\x02\x37\x36\x35\x34\x23\ +\x22\x0e\x02\x05\x07\x06\x15\x14\x33\x32\x36\x37\x36\x35\x34\x23\ +\x22\x0e\x02\x06\xf0\x32\x07\x35\x14\x2c\x33\x2d\x0e\x14\x48\x10\ +\x1e\x24\x1d\x01\x57\x0c\x1a\x30\x41\x62\x3b\x4a\x32\x34\x7d\xfe\ +\x6a\x82\x8d\x07\x15\x0e\x39\x1f\x01\x0e\x1c\x31\x40\x60\x38\x4a\ +\x32\x34\x7d\xfe\x6a\x82\x8d\x07\x15\x11\x4a\x25\x07\x1a\x30\x41\ +\x62\x3b\x49\x33\x33\x7c\xfe\x6a\x82\x8e\x07\x15\x13\x50\x27\x38\ +\x1b\x35\x35\x56\x3c\x46\x07\x33\x48\x3e\x43\x4d\x0c\x26\x5e\x51\ +\x46\x06\x33\x49\x3e\x74\x21\x23\x5a\x4a\x46\x06\x33\x49\x3e\x2a\ +\x40\x25\x11\xfc\x40\x32\x07\x35\x14\x2c\x33\x2d\x0e\x14\x48\x10\ +\x1e\x24\x1d\xfd\x83\x32\x08\x36\x2e\x66\x19\x14\x47\x10\x1f\x24\ +\x1c\x02\xda\xfa\x26\x1e\x4f\x15\x2f\x63\x44\x69\x39\x6e\x08\x15\ +\x2f\x53\x26\x53\x61\x55\x47\x2a\x36\xff\x00\x3f\x3f\x02\xc1\x25\ +\x49\x44\x28\x51\x5d\x50\x43\x26\x36\xff\x00\x3f\x3f\x02\xc1\x25\ +\x6a\x63\x2f\x58\x5b\x43\x2a\x36\xff\x00\x3f\x3f\x02\xc1\x25\x77\ +\x70\x18\x44\x71\x51\x2d\x41\x26\x1b\x54\x47\x4f\x4c\x41\x26\x1b\ +\x7f\x40\x3f\x41\x26\x1b\x24\x3f\x4e\x02\xfa\x26\x1e\x4f\x15\x2f\ +\x63\x44\x69\x39\x6e\x08\x15\x2f\x22\xfa\x26\x1e\x4f\x6b\x80\x69\ +\x39\x6e\x08\x15\x2f\x00\x00\x00\x01\x00\x8b\xff\xf1\x05\xdc\x05\ +\x1e\x00\x76\x00\x00\x01\x06\x07\x0a\x01\x23\x22\x26\x35\x34\x36\ +\x33\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\x33\x32\x3e\x02\x37\ +\x36\x37\x23\x35\x33\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\x26\ +\x35\x34\x36\x33\x32\x35\x34\x26\x23\x22\x0e\x02\x07\x21\x3e\x02\ +\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x36\x35\ +\x34\x26\x23\x22\x0e\x02\x07\x33\x15\x23\x06\x07\x0a\x01\x23\x22\ +\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\x33\ +\x32\x3e\x02\x37\x36\x37\x02\xbf\x1c\x01\x35\xd5\x75\x55\x43\x3c\ +\x27\x22\x32\x19\x13\x15\x11\x33\x2e\x43\x26\x24\x0e\x0c\x18\x86\ +\x98\x25\x63\x81\x42\x3f\x59\x3c\x27\x22\x32\x19\x13\x26\x21\x12\ +\x21\x34\x27\x1a\x0e\x01\x2f\x26\x63\x80\x43\x3e\x5a\x3d\x27\x22\ +\x31\x19\x12\x16\x10\x20\x12\x22\x34\x26\x1b\x0d\x95\x9f\x1c\x02\ +\x35\xd4\x75\x56\x42\x3c\x26\x23\x31\x19\x12\x16\x11\x34\x2d\x44\ +\x26\x23\x0e\x0d\x17\x03\x41\xb1\x09\xfe\xc6\xfe\xa4\x33\x3e\x3b\ +\x4d\x23\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\x3f\x75\ +\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x1f\x07\x09\x34\x65\x6f\ +\x50\x75\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x10\x0f\x07\x09\ +\x34\x65\x6f\x50\x3f\xb2\x08\xfe\xc6\xfe\xa4\x33\x3e\x3b\x4d\x23\ +\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\x00\x00\x00\x00\ +\x01\x00\x8b\xff\xf1\x07\xb2\x05\x1e\x00\xaf\x00\x00\x01\x06\x07\ +\x0a\x01\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\ +\x06\x15\x14\x33\x32\x3e\x02\x37\x36\x37\x21\x06\x07\x0a\x01\x23\ +\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\ +\x33\x32\x3e\x02\x37\x36\x37\x21\x06\x07\x0a\x01\x23\x22\x26\x35\ +\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\x33\x32\x3e\ +\x02\x37\x36\x37\x23\x35\x33\x3e\x02\x33\x32\x16\x15\x14\x06\x23\ +\x22\x26\x35\x34\x36\x33\x32\x35\x34\x26\x23\x22\x0e\x02\x07\x21\ +\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\ +\x36\x35\x34\x26\x23\x22\x0e\x02\x07\x21\x3e\x02\x33\x32\x16\x15\ +\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x35\x34\x26\x23\x22\x0e\ +\x02\x07\x33\x15\x06\x6c\x1c\x01\x35\xd5\x75\x55\x43\x3c\x27\x22\ +\x31\x18\x12\x16\x11\x33\x2e\x43\x26\x24\x0e\x0c\x18\xfe\xd9\x1c\ +\x02\x35\xd4\x75\x56\x42\x3c\x26\x23\x31\x19\x12\x16\x11\x34\x2d\ +\x44\x26\x23\x0e\x0d\x17\xfe\xd9\x1c\x01\x35\xd5\x75\x55\x43\x3c\ +\x27\x22\x32\x19\x13\x15\x11\x33\x2e\x43\x26\x24\x0e\x0c\x18\x86\ +\x98\x25\x63\x81\x42\x3f\x59\x3c\x27\x22\x32\x19\x13\x26\x21\x12\ +\x21\x34\x27\x1a\x0e\x01\x2f\x26\x63\x80\x43\x3e\x5a\x3d\x27\x22\ +\x31\x19\x12\x16\x10\x20\x12\x22\x34\x26\x1b\x0d\x01\x2f\x25\x63\ +\x81\x42\x3f\x59\x3c\x27\x22\x31\x18\x12\x27\x21\x12\x21\x34\x27\ +\x1a\x0e\x96\x03\x41\xb2\x08\xfe\xc6\xfe\xa4\x33\x3e\x3b\x4d\x23\ +\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\xb2\x08\xfe\xc6\ +\xfe\xa4\x33\x3e\x3b\x4d\x23\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\ +\x6e\x57\x57\xb1\x09\xfe\xc6\xfe\xa4\x33\x3e\x3b\x4d\x23\x1f\x1d\ +\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\x3f\x75\xb6\x73\x39\x39\ +\x3a\x4d\x23\x1f\x1c\x26\x1f\x07\x09\x34\x65\x6f\x50\x75\xb6\x73\ +\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x10\x0f\x07\x09\x34\x65\x6f\x50\ +\x75\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x1f\x07\x09\x34\x65\ +\x6f\x50\x3f\x00\x03\x00\x92\x00\x00\x07\x5f\x03\x89\x00\x3e\x00\ +\x63\x00\x74\x00\x00\x01\x22\x26\x37\x13\x36\x26\x23\x22\x0e\x02\ +\x07\x03\x23\x13\x36\x26\x23\x22\x0e\x02\x07\x03\x23\x13\x36\x23\ +\x22\x06\x07\x27\x3e\x03\x33\x32\x17\x36\x33\x32\x17\x3e\x03\x33\ +\x32\x16\x07\x03\x06\x33\x3e\x01\x37\x17\x0e\x03\x01\x14\x0e\x04\ +\x23\x22\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\x07\x27\x3e\ +\x04\x33\x32\x17\x3e\x01\x33\x32\x1e\x02\x25\x07\x06\x15\x14\x33\ +\x32\x36\x37\x36\x35\x34\x23\x22\x0e\x02\x03\xc0\x36\x26\x09\x54\ +\x04\x10\x1b\x10\x21\x26\x1e\x07\x5c\x97\x6a\x04\x10\x1b\x10\x21\ +\x26\x1e\x07\x5c\x97\x63\x07\x15\x13\x50\x27\x38\x1b\x35\x35\x56\ +\x3c\x46\x07\x56\x82\x41\x13\x18\x20\x28\x40\x38\x2b\x39\x0d\x4a\ +\x07\x15\x15\x4f\x26\x38\x1b\x35\x35\x56\x03\x63\x0c\x1a\x30\x41\ +\x63\x3b\x49\x33\x34\x7e\xfe\x6a\x81\x8e\x07\x15\x1f\x41\x38\x07\ +\x20\x1d\x30\x4a\x2f\x46\x07\x33\x48\x3e\x2b\x3f\x25\x12\xfe\xa2\ +\x32\x08\x35\x2f\x66\x19\x14\x47\x10\x1f\x24\x1c\x01\x09\x46\x2f\ +\x01\xa2\x17\x11\x08\x15\x2f\x22\xfe\x39\x02\x0d\x17\x11\x08\x15\ +\x2f\x22\xfe\x39\x01\xed\x25\x77\x70\x18\x44\x71\x51\x2d\x41\x41\ +\x41\x12\x19\x0f\x07\x4d\x3f\xfe\x95\x25\x01\x77\x6f\x18\x44\x71\ +\x51\x2d\x01\xa0\x26\x53\x61\x55\x47\x2a\x36\xff\x00\x3f\x3f\x02\ +\xc1\x25\x80\x18\x0b\x46\x2b\x35\x1b\x41\x26\x1b\x24\x3f\x4e\x02\ +\xfa\x26\x1e\x4f\x6b\x80\x69\x39\x6e\x08\x15\x2f\x00\x00\x00\x00\ +\x02\x00\x92\xff\xf1\x07\x3c\x05\x1e\x00\x3d\x00\x7c\x00\x00\x01\ +\x22\x0e\x02\x07\x33\x15\x23\x06\x07\x0a\x01\x23\x22\x26\x35\x34\ +\x36\x33\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\x33\x32\x3e\x02\ +\x37\x36\x37\x23\x35\x33\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\ +\x26\x35\x34\x36\x33\x32\x36\x35\x34\x26\x01\x22\x26\x37\x13\x36\ +\x26\x23\x22\x0e\x02\x07\x03\x23\x13\x36\x26\x23\x22\x0e\x02\x07\ +\x03\x23\x13\x36\x23\x22\x06\x07\x27\x3e\x03\x33\x32\x17\x36\x33\ +\x32\x17\x3e\x03\x33\x32\x16\x07\x03\x06\x33\x3e\x01\x37\x17\x0e\ +\x03\x06\xa4\x22\x34\x26\x1b\x0d\x95\x9f\x1c\x02\x35\xd4\x75\x56\ +\x42\x3c\x26\x23\x31\x19\x12\x16\x11\x34\x2d\x44\x26\x23\x0e\x0d\ +\x17\x85\x97\x26\x63\x80\x43\x3e\x5a\x3d\x27\x22\x31\x19\x12\x16\ +\x10\x20\xfd\x0a\x36\x26\x09\x54\x04\x10\x1b\x10\x21\x26\x1e\x07\ +\x5c\x97\x6a\x04\x10\x1b\x10\x21\x26\x1e\x07\x5c\x97\x63\x07\x15\ +\x13\x50\x27\x38\x1b\x35\x35\x56\x3c\x46\x07\x56\x82\x41\x13\x18\ +\x20\x28\x40\x38\x2b\x39\x0d\x4a\x07\x15\x15\x4f\x26\x38\x1b\x35\ +\x35\x56\x04\xd8\x34\x65\x6f\x50\x3f\xb2\x08\xfe\xc6\xfe\xa4\x33\ +\x3e\x3b\x4d\x23\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\ +\x3f\x75\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x10\x0f\x07\x09\ +\xfc\x31\x46\x2f\x01\xa2\x17\x11\x08\x15\x2f\x22\xfe\x39\x02\x0d\ +\x17\x11\x08\x15\x2f\x22\xfe\x39\x01\xed\x25\x77\x70\x18\x44\x71\ +\x51\x2d\x41\x41\x41\x12\x19\x0f\x07\x4d\x3f\xfe\x95\x25\x01\x77\ +\x6f\x18\x44\x71\x51\x2d\x00\x00\x02\x00\xb8\xff\xf1\x05\x8b\x05\ +\x1e\x00\x3d\x00\x75\x00\x00\x01\x22\x0e\x02\x07\x33\x15\x23\x06\ +\x07\x0a\x01\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\ +\x22\x06\x15\x14\x33\x32\x3e\x02\x37\x36\x37\x23\x35\x33\x3e\x02\ +\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x36\x35\ +\x34\x26\x01\x34\x26\x23\x22\x06\x15\x14\x1e\x04\x15\x14\x06\x23\ +\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x15\x14\x16\x33\x32\ +\x36\x35\x34\x2e\x04\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\ +\x26\x35\x34\x36\x04\xf2\x21\x34\x26\x1b\x0d\x95\x9f\x1c\x02\x35\ +\xd4\x76\x55\x43\x3d\x27\x22\x31\x19\x12\x16\x10\x32\x2e\x44\x26\ +\x23\x0e\x0d\x17\x85\x98\x25\x63\x80\x42\x3f\x59\x3c\x26\x23\x31\ +\x19\x12\x16\x11\x21\xfd\x34\x23\x1f\x38\x2c\x26\x3a\x43\x3a\x26\ +\x9e\x74\x5a\x72\x30\x20\x1b\x25\x12\x3a\x29\x2c\x3a\x25\x38\x40\ +\x38\x25\x83\x71\x55\x66\x28\x1a\x20\x20\x07\x04\xd8\x34\x65\x6f\ +\x50\x3f\xb2\x08\xfe\xc6\xfe\xa4\x33\x3e\x3b\x4d\x23\x1f\x1d\x25\ +\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\x3f\x75\xb6\x73\x39\x39\x3a\ +\x4d\x23\x1f\x1c\x26\x10\x0f\x07\x09\xfe\x3c\x11\x24\x23\x31\x1b\ +\x30\x21\x2f\x30\x51\x32\x53\x51\x48\x43\x31\x32\x23\x16\x10\x18\ +\x0b\x1f\x23\x33\x27\x25\x37\x20\x27\x28\x48\x30\x50\x59\x55\x3c\ +\x18\x20\x1f\x15\x07\x13\x00\x00\x02\x00\x8b\xff\xf1\x05\x95\x05\ +\x1e\x00\x29\x00\x66\x00\x00\x01\x32\x36\x35\x34\x23\x22\x26\x35\ +\x34\x36\x33\x32\x16\x15\x14\x23\x22\x26\x23\x22\x06\x07\x23\x35\ +\x01\x23\x22\x07\x06\x07\x23\x37\x21\x15\x01\x36\x33\x32\x1e\x02\ +\x01\x22\x0e\x02\x07\x33\x15\x23\x06\x07\x0a\x01\x23\x22\x26\x35\ +\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\x33\x32\x3e\ +\x02\x37\x36\x37\x23\x35\x33\x3e\x02\x33\x32\x16\x15\x14\x06\x23\ +\x22\x26\x35\x34\x36\x33\x32\x35\x34\x26\x04\xd8\x19\x29\x22\x24\ +\x2c\x28\x2e\x31\x2f\xba\x1f\x73\x2a\x2d\x39\x20\x4e\x01\xaa\x80\ +\x2f\x23\x10\x0c\x32\x28\x01\xce\xfe\x6c\x16\x1a\x1e\x30\x19\x28\ +\xfe\xae\x21\x34\x27\x1a\x0e\x95\x9f\x1c\x01\x35\xd5\x75\x55\x43\ +\x3c\x27\x22\x32\x19\x13\x15\x11\x33\x2e\x43\x26\x24\x0e\x0c\x18\ +\x86\x98\x25\x63\x81\x42\x3f\x59\x3c\x27\x22\x32\x19\x13\x26\x21\ +\x01\x52\x1b\x0c\x11\x29\x1f\x25\x25\x50\x3c\xa4\x51\x11\x19\x1d\ +\x01\xc7\x15\x08\x22\xc8\x5b\xfe\x82\x03\x1c\x20\x1c\x03\x86\x34\ +\x65\x6f\x50\x3f\xb1\x09\xfe\xc6\xfe\xa4\x33\x3e\x3b\x4d\x23\x1f\ +\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\x3f\x75\xb6\x73\x39\ +\x39\x3a\x4d\x23\x1f\x1c\x26\x1f\x07\x09\x00\x00\x03\x00\xb8\xff\ +\xf1\x07\x1a\x05\x1e\x00\x2a\x00\x68\x00\xa0\x00\x00\x01\x32\x36\ +\x35\x34\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x23\x22\x2e\ +\x01\x23\x22\x06\x07\x23\x35\x01\x23\x22\x07\x06\x07\x23\x37\x21\ +\x15\x01\x36\x33\x32\x1e\x02\x01\x22\x0e\x02\x07\x33\x15\x23\x06\ +\x07\x0a\x01\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\ +\x22\x06\x15\x14\x33\x32\x3e\x02\x37\x36\x37\x23\x35\x33\x3e\x02\ +\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x36\x35\ +\x34\x26\x01\x34\x26\x23\x22\x06\x15\x14\x1e\x04\x15\x14\x06\x23\ +\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x15\x14\x16\x33\x32\ +\x36\x35\x34\x2e\x04\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\ +\x26\x35\x34\x36\x06\x5d\x19\x2a\x23\x24\x2c\x28\x2f\x30\x2f\xb9\ +\x1b\x3d\x42\x22\x2d\x3a\x1f\x4e\x01\xaa\x80\x2f\x24\x10\x0c\x31\ +\x28\x01\xce\xfe\x6b\x17\x19\x1f\x2f\x19\x29\xfe\xad\x21\x34\x26\ +\x1b\x0d\x95\x9f\x1c\x02\x35\xd4\x76\x55\x43\x3d\x27\x22\x31\x19\ +\x12\x16\x10\x32\x2e\x44\x26\x23\x0e\x0d\x17\x85\x98\x25\x63\x80\ +\x42\x3f\x59\x3c\x26\x23\x31\x19\x12\x16\x11\x21\xfd\x34\x23\x1f\ +\x38\x2c\x26\x3a\x43\x3a\x26\x9e\x74\x5a\x72\x30\x20\x1b\x25\x12\ +\x3a\x29\x2c\x3a\x25\x38\x40\x38\x25\x83\x71\x55\x66\x28\x1a\x20\ +\x20\x07\x01\x52\x1b\x0c\x11\x29\x1f\x25\x25\x50\x3c\xa4\x28\x29\ +\x11\x19\x1d\x01\xc7\x15\x08\x22\xc8\x5b\xfe\x82\x03\x1c\x20\x1c\ +\x03\x86\x34\x65\x6f\x50\x3f\xb2\x08\xfe\xc6\xfe\xa4\x33\x3e\x3b\ +\x4d\x23\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\x57\x3f\x75\ +\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x10\x0f\x07\x09\xfe\x3c\ +\x11\x24\x23\x31\x1b\x30\x21\x2f\x30\x51\x32\x53\x51\x48\x43\x31\ +\x32\x23\x16\x10\x18\x0b\x1f\x23\x33\x27\x25\x37\x20\x27\x28\x48\ +\x30\x50\x59\x55\x3c\x18\x20\x1f\x15\x07\x13\x00\x01\x00\xc8\x01\ +\x8e\x0a\xf0\x04\xd8\x00\x0b\x00\x00\x13\x11\x33\x11\x21\x11\x33\ +\x11\x23\x11\x21\x11\xc8\x46\x09\x9c\x46\x46\xf6\x64\x01\x8e\x03\ +\x4a\xff\x00\x01\x00\xfc\xb6\x01\x00\xff\x00\xff\xff\x00\xc8\x00\ +\x00\x06\x28\x06\x66\x10\x27\x00\x3e\x04\x98\x00\x00\x10\x27\x00\ +\x36\x03\xac\x00\x00\x10\x26\x00\x3e\x00\x00\x10\x07\x00\x38\x01\ +\x64\x00\x00\x00\x03\x00\x90\x01\x8e\x05\x51\x06\x66\x00\x3b\x00\ +\x43\x00\x4b\x00\x00\x01\x36\x16\x07\x02\x21\x22\x2e\x01\x02\x35\ +\x34\x3e\x01\x33\x32\x16\x15\x14\x0e\x01\x07\x06\x26\x35\x34\x36\ +\x33\x32\x16\x15\x14\x06\x23\x22\x26\x27\x26\x06\x15\x14\x16\x33\ +\x32\x36\x35\x34\x26\x23\x22\x02\x15\x14\x12\x04\x33\x32\x3e\x02\ +\x26\x34\x36\x32\x16\x14\x06\x22\x02\x34\x36\x32\x16\x14\x06\x22\ +\x04\xed\x0e\x46\x0c\xcb\xfe\x81\x78\xd8\xa8\x63\x70\xe1\x9e\xcb\ +\xf6\x56\xa0\x67\x98\xd1\x89\x5a\x75\x67\x6f\x3b\x20\x38\x14\x11\ +\x1e\x7a\x70\x7c\x8c\xb7\xa4\xae\xd9\x93\x01\x04\x9a\x4d\x86\x6b\ +\x51\x3c\x3b\x53\x3a\x3a\x53\x3b\x3b\x53\x3a\x3a\x53\x03\x23\x14\ +\x2b\x15\xfe\x97\x5c\xa9\x01\x07\x9f\xa5\xf9\x8f\xd9\xc3\x6d\xb7\ +\x73\x06\x09\xd3\xa8\x54\xa4\x62\x62\x46\x57\x23\x01\x01\x11\x0c\ +\x5f\x69\xbe\x8c\x9f\xc0\xfe\xfe\xbf\x94\xfe\xf9\xa2\x28\x4c\x55\ +\xe7\x52\x3b\x3b\x52\x3b\x01\xc9\x52\x3b\x3b\x52\x3b\x00\x00\xff\ +\xff\x00\x90\x01\x8e\x05\x51\x08\x40\x10\x27\x00\x1c\x02\x26\x06\ +\x51\x10\x06\x00\x12\x00\x00\xff\xff\x00\x90\xff\xb0\x05\x51\x06\ +\x66\x10\x27\x00\x1c\x02\x26\xff\xb5\x10\x06\x00\x12\x00\x00\x00\ +\x07\x00\xc8\xff\xe2\x02\x80\x06\x84\x00\x03\x00\x07\x00\x16\x00\ +\x1f\x00\x25\x00\x2d\x00\x30\x00\x00\x13\x21\x15\x21\x37\x33\x11\ +\x23\x03\x33\x32\x16\x15\x14\x07\x16\x15\x14\x06\x2b\x02\x11\x17\ +\x15\x33\x32\x36\x35\x34\x26\x23\x07\x15\x33\x32\x34\x23\x13\x23\ +\x27\x23\x07\x23\x13\x33\x03\x33\x27\xc8\x01\xb8\xfe\x48\xb4\x50\ +\x50\x3c\x96\x35\x52\x2d\x46\x42\x47\xad\x50\x50\x91\x1e\x19\x1f\ +\x18\x91\xa6\x3b\x3b\x9a\x5a\x28\xb3\x28\x5b\xb4\x50\x65\x7b\x3e\ +\x06\x84\x50\x11\xfe\x4b\xfd\x46\x56\x38\x49\x17\x24\x4e\x42\x52\ +\x01\xf4\x50\x7c\x1e\x20\x1a\x24\xcc\x88\x88\x01\x7f\x73\x73\x01\ +\xf4\xfe\xcf\xb1\x00\x00\x00\x00\x02\x00\xc8\x00\x00\x04\xe9\x06\ +\x66\x00\x03\x00\x2f\x00\x00\x01\x11\x23\x11\x01\x23\x11\x33\x11\ +\x21\x11\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x26\x22\x06\x16\ +\x07\x11\x21\x15\x21\x11\x16\x06\x16\x32\x36\x33\x32\x16\x15\x14\ +\x06\x23\x22\x26\x35\x11\x21\x01\xa6\xde\x01\xba\x50\x50\x01\x0a\ +\x72\x6e\x33\x4a\x41\x2d\x19\x31\x3c\x1a\x02\x01\xfe\xa6\x01\x5a\ +\x01\x02\x1a\x3c\x31\x19\x2d\x41\x4a\x33\x6e\x72\xfe\xf6\x06\x66\ +\xf9\x9a\x06\x66\xf9\x9a\x06\x66\xfd\xb4\x01\x3a\x72\xa0\x52\x34\ +\x2a\x36\x3e\x34\x46\x22\xfe\x62\xa2\xfe\x62\x22\x46\x34\x3e\x36\ +\x2a\x34\x52\xa0\x72\x01\x3a\x00\x06\x00\xbe\x01\x90\x0a\x40\x04\ +\xd6\x00\x07\x00\x0b\x00\x0f\x00\x13\x00\x1b\x00\x1f\x00\x00\x00\ +\x34\x36\x32\x16\x14\x06\x22\x13\x01\x23\x01\x23\x01\x23\x01\x21\ +\x01\x23\x01\x00\x34\x36\x32\x16\x14\x06\x22\x09\x01\x23\x01\x09\ +\x6e\x3a\x53\x3b\x3b\x53\x98\xfd\x8e\xf4\x02\x70\xda\xfd\x8f\xf5\ +\x02\x71\xfe\x79\xfd\x8e\xf4\x02\x70\xfb\xca\x3b\x52\x3b\x3b\x52\ +\x03\x21\xfd\x8f\xf5\x02\x71\x02\x43\x52\x3b\x3b\x52\x3b\x02\xce\ +\xfc\xba\x03\x46\xfc\xba\x03\x46\xfc\xba\x03\x46\xfe\xfb\x52\x3b\ +\x3b\x52\x3b\x01\x40\xfc\xba\x03\x46\x00\x00\x00\x03\x00\xc9\x01\ +\x0e\x0a\x50\x03\x9c\x00\x03\x00\x07\x00\x0e\x00\x00\x01\x33\x35\ +\x23\x07\x11\x21\x11\x37\x01\x17\x09\x01\x07\x01\x01\x0f\x9a\x9a\ +\x46\x01\x26\x37\x08\x20\x0a\xf8\xd8\x07\x28\x0a\xf7\xe0\x02\x09\ +\x9a\xe0\x01\x26\xfe\xda\xb5\x01\x24\x45\xfe\xfe\xfe\xfe\x45\x01\ +\x24\x00\x00\x00\x02\x00\xc8\x01\x0e\x11\x8a\x03\x9c\x00\x06\x00\ +\x0d\x00\x00\x13\x01\x17\x09\x01\x07\x01\x25\x15\x01\x27\x09\x01\ +\x37\xc8\x08\x20\x0a\xf8\xd8\x07\x28\x0a\xf7\xe0\x10\xc2\xf7\xe0\ +\x0a\x07\x28\xf8\xd8\x0a\x02\x78\x01\x24\x45\xfe\xfe\xfe\xfe\x45\ +\x01\x24\x46\x46\xfe\xdc\x45\x01\x02\x01\x02\x45\x00\x00\x00\x00\ +\x01\x00\xc8\x01\x8e\x07\x3a\x04\xd8\x00\x0b\x00\x00\x01\x23\x11\ +\x21\x11\x23\x11\x21\x11\x23\x11\x21\x07\x3a\x64\xfd\x4e\x46\xfd\ +\x4e\x64\x06\x72\x01\x8e\x02\xe6\xfe\xa8\x01\x58\xfd\x1a\x03\x4a\ +\x00\x00\x00\x00\x01\x00\xc8\x01\x5e\x0c\xd6\x04\x77\x00\x0e\x00\ +\x00\x09\x01\x21\x11\x33\x11\x21\x09\x01\x21\x11\x33\x11\x21\x01\ +\x06\xe6\x01\xa1\x04\x09\x46\xfb\x89\xfe\x70\xfe\x70\xfb\x89\x46\ +\x04\x09\x01\xa2\x04\x77\xfd\x2d\x02\xd2\xfc\xe8\x02\xb5\xfd\x4b\ +\x03\x18\xfd\x2e\x02\xd3\x00\x00\x03\x00\x1c\xff\xfb\x01\x5e\x01\ +\xef\x00\x09\x00\x16\x00\x2a\x00\x00\x37\x0e\x01\x15\x14\x16\x32\ +\x36\x35\x34\x27\x3e\x01\x35\x34\x26\x22\x06\x15\x14\x1e\x02\x17\ +\x1e\x01\x15\x14\x20\x35\x34\x36\x37\x2e\x01\x35\x34\x36\x32\x16\ +\x15\x14\x06\xa9\x28\x31\x3c\x62\x3c\x5d\x2a\x24\x37\x4e\x37\x11\ +\x24\x1e\x43\x2e\x3b\xfe\xbe\x3a\x2d\x29\x2a\x56\x6e\x56\x2d\xf4\ +\x13\x3d\x21\x39\x34\x34\x39\x3d\x5f\x11\x29\x25\x28\x2e\x2e\x28\ +\x16\x20\x15\x0c\x17\x17\x4e\x28\x88\x88\x25\x46\x15\x12\x36\x33\ +\x31\x40\x40\x31\x2d\x32\x00\x00\x04\x00\xc8\xfe\x8d\x04\x10\x03\ +\x2f\x00\x07\x00\x0b\x00\x13\x00\x17\x00\x00\x05\x11\x33\x11\x25\ +\x11\x23\x11\x03\x15\x37\x35\x01\x11\x33\x11\x25\x11\x23\x11\x03\ +\x15\x37\x35\x02\x94\x46\x01\x36\x46\xf0\xf0\xfc\xfe\x46\x01\x36\ +\x46\xf0\xf0\x28\x03\x57\xfe\x8c\x29\xfc\xa9\x01\x74\x01\x2d\xbb\ +\x1b\xbb\xfe\x8f\x03\x57\xfe\x8c\x29\xfc\xa9\x01\x74\x01\x2d\xbb\ +\x1b\xbb\x00\x00\x04\x00\xc8\xfe\x8d\x04\x10\x03\xe1\x00\x0d\x00\ +\x19\x00\x21\x00\x25\x00\x00\x05\x11\x33\x11\x3e\x02\x33\x32\x16\ +\x15\x14\x06\x07\x19\x01\x3e\x03\x35\x34\x23\x22\x0e\x01\x01\x11\ +\x33\x11\x25\x11\x23\x11\x03\x15\x37\x35\x02\x94\x46\x25\x25\x40\ +\x2a\x42\x40\xa1\x95\x2a\x39\x36\x1b\x32\x1a\x22\x26\xfd\xce\x46\ +\x01\x36\x46\xf0\xf0\x28\x04\x09\xfd\x8b\x1c\x18\x13\x5c\x43\x49\ +\x9a\x59\x01\x49\xff\x00\x1e\x32\x40\x47\x27\x44\x0c\x1e\xfe\x9f\ +\x03\x57\xfe\x8c\x29\xfc\xa9\x01\x74\x01\x2d\xbb\x1b\xbb\x00\x00\ +\x04\x00\xc8\xfe\x84\x04\x10\x03\x38\x00\x1b\x00\x1f\x00\x27\x00\ +\x2b\x00\x00\x05\x11\x23\x11\x07\x35\x37\x35\x07\x35\x37\x11\x33\ +\x11\x37\x11\x33\x11\x37\x15\x07\x15\x37\x15\x07\x11\x23\x11\x27\ +\x37\x35\x07\x01\x11\x33\x11\x25\x11\x23\x11\x03\x15\x37\x35\x03\ +\x20\x46\x46\x46\x46\x46\x46\x64\x46\x46\x46\x46\x46\x46\x64\x64\ +\x64\xfd\xa8\x46\x01\x36\x46\xf0\xf0\x16\xfe\x9a\x01\x5d\x09\x8c\ +\x09\xc2\x09\x8c\x09\x01\x3f\xfe\xca\x0e\x01\x66\xfe\xa3\x09\x8c\ +\x09\xc2\x09\x8c\x09\xfe\xc1\x01\x36\x7e\x0e\xc2\x0e\xfe\xa0\x03\ +\x57\xfe\x8c\x29\xfc\xa9\x01\x74\x01\x2d\xbb\x1b\xbb\x00\x00\x00\ +\x01\x00\xc8\xfe\x84\x01\xa4\x02\xfa\x00\x13\x00\x00\x05\x11\x23\ +\x11\x07\x35\x37\x35\x07\x35\x37\x11\x33\x11\x37\x15\x07\x15\x37\ +\x15\x01\x54\x46\x46\x46\x46\x46\x46\x50\x50\x50\x16\xfe\x9a\x01\ +\x5d\x09\x8c\x09\xc2\x09\x8c\x09\x01\x3f\xfe\xca\x0b\x8c\x0b\xc2\ +\x0b\x8c\x00\x00\x03\x00\xc8\xfe\x84\x02\xee\x03\x4e\x00\x23\x00\ +\x27\x00\x2b\x00\x00\x05\x11\x23\x11\x07\x35\x37\x35\x07\x35\x37\ +\x11\x33\x11\x37\x11\x33\x11\x37\x11\x33\x11\x37\x15\x07\x15\x37\ +\x15\x07\x11\x23\x11\x07\x11\x23\x11\x13\x07\x15\x37\x27\x07\x15\ +\x37\x01\x54\x46\x46\x46\x46\x46\x46\x64\x46\x64\x46\x46\x46\x46\ +\x46\x46\x64\x46\xaa\x64\x64\xaa\x64\x64\x16\xfe\x9a\x01\x5d\x09\ +\x8c\x09\xc2\x09\x8c\x09\x01\x3f\xfe\xca\x0e\x01\x52\xfe\xb7\x0d\ +\x01\x66\xfe\xa3\x09\x8c\x09\xc2\x09\x8c\x09\xfe\xc1\x01\x36\x0d\ +\xfe\xad\x01\x4a\x01\x64\x0d\xc2\x0d\xac\x0e\xc2\x0e\x00\x00\x00\ +\x02\x00\xc8\xff\xd8\x02\x44\x03\xe1\x00\x0d\x00\x19\x00\x00\x05\ +\x23\x2e\x01\x35\x34\x36\x33\x32\x1e\x01\x17\x11\x33\x03\x2e\x02\ +\x23\x22\x15\x14\x1e\x02\x17\x02\x44\x46\x95\xa1\x40\x42\x2a\x40\ +\x25\x25\x46\x46\x20\x26\x22\x1a\x32\x1b\x36\x39\x2a\x28\x59\x9a\ +\x49\x43\x5c\x13\x18\x1c\x02\x75\xfd\x40\x18\x1e\x0c\x44\x27\x47\ +\x40\x32\x1e\x00\x04\x00\xc8\xff\xd8\x04\x06\x03\xe1\x00\x0d\x00\ +\x19\x00\x27\x00\x33\x00\x00\x05\x11\x33\x11\x3e\x02\x33\x32\x16\ +\x15\x14\x06\x07\x19\x01\x3e\x03\x35\x34\x23\x22\x0e\x01\x03\x23\ +\x2e\x01\x35\x34\x36\x33\x32\x1e\x01\x17\x11\x33\x03\x2e\x02\x23\ +\x22\x15\x14\x1e\x02\x17\x02\x8a\x46\x25\x25\x40\x2a\x42\x40\xa1\ +\x95\x2a\x39\x36\x1b\x32\x1a\x22\x26\xac\x46\x95\xa1\x40\x42\x2a\ +\x40\x25\x25\x46\x46\x20\x26\x22\x1a\x32\x1b\x36\x39\x2a\x28\x04\ +\x09\xfd\x8b\x1c\x18\x13\x5c\x43\x49\x9a\x59\x01\x49\xff\x00\x1e\ +\x32\x40\x47\x27\x44\x0c\x1e\xfe\x9f\x59\x9a\x49\x43\x5c\x13\x18\ +\x1c\x02\x75\xfd\x40\x18\x1e\x0c\x44\x27\x47\x40\x32\x1e\x00\x00\ +\x02\x00\x50\xff\xf3\x03\xe8\x05\x85\x00\x1b\x00\x2f\x00\x00\x24\ +\x32\x3e\x05\x34\x2e\x05\x22\x0e\x05\x14\x1e\x04\x12\x32\x1e\x03\ +\x14\x0e\x03\x22\x2e\x03\x34\x3e\x02\x01\xe3\x72\x5a\x3c\x2b\x18\ +\x0d\x03\x03\x0d\x18\x2b\x3c\x5a\x72\x5a\x3c\x2b\x18\x0d\x03\x03\ +\x0d\x18\x2b\x3c\x33\xc0\x9d\x68\x48\x1f\x1f\x48\x68\x9d\xc0\x9d\ +\x68\x48\x1f\x1f\x48\x68\x3e\x2c\x44\x6c\x67\x8c\x69\x8c\x69\x8c\ +\x67\x6c\x44\x2c\x2c\x44\x6c\x67\x8c\x69\x8c\x69\x8c\x67\x6c\x44\ +\x05\x1b\x55\x8d\xbb\xc5\xce\xc5\xbb\x8d\x55\x55\x8d\xbb\xc5\xce\ +\xc5\xbb\x8d\x00\x01\x00\xd7\x00\x00\x03\x61\x05\x85\x00\x2f\x00\ +\x00\x29\x01\x22\x26\x35\x34\x33\x32\x3e\x04\x35\x11\x34\x36\x26\ +\x27\x26\x0f\x01\x0e\x02\x27\x26\x27\x26\x3f\x01\x36\x33\x32\x16\ +\x15\x11\x14\x1e\x04\x33\x32\x15\x14\x06\x03\x38\xfd\xc8\x14\x15\ +\x41\x1d\x22\x32\x1d\x1e\x0d\x01\x07\x09\x10\x27\x5a\x05\x0f\x0c\ +\x08\x1c\x02\x02\x1b\xdc\x16\x2a\x1b\x2c\x0d\x1e\x1d\x32\x22\x1d\ +\x41\x15\x14\x0d\x2a\x01\x04\x0b\x11\x1c\x13\x03\xee\x0d\x15\x14\ +\x06\x0c\x16\x32\x03\x0c\x06\x01\x03\x19\x21\x15\xad\x11\x20\x19\ +\xfb\x4f\x13\x1c\x11\x0b\x04\x01\x2a\x0d\x14\x00\x01\x00\x54\xff\ +\xf3\x03\xcc\x05\xd0\x00\x35\x00\x00\x01\x0e\x03\x23\x21\x03\x32\ +\x04\x16\x15\x14\x0e\x02\x23\x22\x26\x27\x2e\x01\x35\x34\x36\x33\ +\x32\x1e\x01\x17\x1e\x02\x33\x32\x36\x35\x34\x2e\x02\x23\x13\x21\ +\x32\x36\x37\x36\x37\x36\x33\x32\x15\x14\x03\x70\x0f\x17\x28\x34\ +\x20\xfe\xac\x4e\xba\x01\x36\xb0\x50\x87\x9e\x57\x60\x8a\x46\x3f\ +\x3d\x20\x18\x11\x1d\x1e\x07\x2f\x43\x65\x4a\x85\xb1\x4e\x91\xf1\ +\x91\x77\x01\x8a\x35\x39\x18\x01\x01\x05\x07\x0f\x05\x89\x22\x2c\ +\x35\x1b\xfe\xc4\x8a\xeb\x8a\x73\xb1\x66\x33\x2d\x27\x23\x3a\x2a\ +\x18\x1a\x17\x2b\x06\x30\x2e\x1c\xc6\xac\x43\x8b\x7a\x4e\x02\x32\ +\x26\x26\x01\x02\x09\x12\x1e\x00\x03\x00\x50\xff\xf3\x03\xe8\x05\ +\x85\x00\x11\x00\x21\x00\x3c\x00\x00\x01\x0e\x01\x15\x14\x1e\x02\ +\x32\x3e\x02\x35\x34\x2e\x03\x37\x3e\x01\x35\x34\x26\x22\x06\x15\ +\x14\x1e\x05\x17\x1e\x01\x15\x10\x20\x11\x34\x36\x37\x2e\x04\x35\ +\x34\x3e\x01\x32\x1e\x01\x15\x14\x0e\x02\x01\xe1\x71\x8a\x30\x56\ +\x6d\x86\x6d\x56\x30\x2c\x46\x63\x63\x30\x78\x66\x9c\xe0\x9c\x13\ +\x1c\x33\x2e\x4a\x35\x9e\x81\xaa\xfc\x68\xa6\x80\x2c\x43\x3f\x27\ +\x18\x76\xb7\xcc\xb7\x76\x25\x46\x50\x02\xba\x38\xad\x5f\x54\x7b\ +\x47\x22\x22\x47\x7b\x54\x29\x50\x42\x40\x31\x92\x30\x76\x69\x73\ +\x84\x84\x73\x24\x3e\x2e\x29\x1b\x1d\x11\x39\x41\xdd\x74\xfe\x7d\ +\x01\x83\x6a\xc7\x3e\x14\x28\x3b\x47\x63\x3d\x5e\x96\x4e\x4e\x96\ +\x5e\x43\x6a\x49\x30\x00\x00\x00\x01\x00\x64\x01\x06\x03\x92\x04\ +\xef\x00\x34\x00\x00\x01\x22\x26\x27\x26\x23\x22\x0e\x03\x15\x14\ +\x1e\x01\x33\x32\x3e\x02\x33\x32\x16\x15\x14\x0e\x02\x23\x22\x2e\ +\x01\x35\x34\x3e\x03\x33\x32\x17\x1e\x02\x33\x32\x3e\x01\x33\x32\ +\x0f\x01\x06\x03\x5b\x07\x0e\x0b\x46\x8a\x49\x7d\x54\x3c\x1b\x38\ +\x72\x48\x41\x6a\x32\x24\x04\x07\x17\x2c\x4b\x7d\x45\x72\xad\x53\ +\x2a\x5b\x7c\xb6\x67\x2a\x51\x0b\x0f\x0b\x12\x16\x23\x16\x05\x0e\ +\x09\x1c\x06\x04\x19\x3f\x0b\x46\x44\x6e\x8b\x8a\x41\x4b\x99\x71\ +\x2a\x31\x2a\x12\x06\x0c\x37\x40\x30\x7d\xbc\x6a\x3c\x95\x9e\x83\ +\x54\x1e\x05\x04\x01\x14\x14\x2e\x89\x1f\x00\x00\x02\x00\x4b\x01\ +\x13\x04\x21\x04\xe2\x00\x13\x00\x39\x00\x00\x01\x0e\x01\x1e\x03\ +\x33\x32\x3e\x03\x35\x34\x26\x23\x22\x06\x07\x27\x21\x32\x1e\x04\ +\x15\x14\x06\x04\x23\x21\x22\x2e\x02\x35\x34\x3e\x03\x37\x3e\x01\ +\x37\x13\x36\x27\x2e\x03\x35\x34\x36\x01\x6c\x03\x01\x07\x0e\x22\ +\x29\x23\x50\x87\x5a\x3f\x1c\x8c\x80\x40\x26\x08\xd4\x01\x47\x30\ +\x5a\x67\x53\x46\x27\x9e\xfe\xfb\x98\xfe\xa0\x0e\x0e\x15\x0a\x0a\ +\x15\x10\x1b\x03\x20\x17\x04\x95\x09\x30\x07\x25\x13\x12\x25\x01\ +\x95\x0f\x11\x0f\x07\x05\x01\x41\x68\x85\x87\x41\xa3\xaa\x18\x26\ +\x84\x0d\x21\x40\x5c\x90\x5b\x98\xf9\x89\x01\x05\x0b\x0a\x0a\x10\ +\x08\x05\x04\x01\x06\x0f\x14\x02\xe3\x2f\x0b\x01\x06\x04\x0d\x0a\ +\x0d\x13\x00\x00\x01\x00\x46\x01\x06\x03\x09\x04\xef\x00\x4b\x00\ +\x00\x01\x2e\x03\x35\x34\x3e\x01\x33\x32\x1e\x01\x17\x1e\x02\x33\ +\x32\x3e\x01\x33\x32\x0f\x01\x06\x23\x22\x26\x27\x26\x23\x22\x06\ +\x15\x14\x16\x17\x1e\x03\x15\x14\x06\x23\x22\x27\x2e\x02\x23\x22\ +\x0e\x01\x23\x22\x3f\x01\x3e\x02\x33\x32\x1e\x01\x17\x1e\x01\x33\ +\x32\x36\x35\x34\x2e\x01\x01\x94\x3e\x3d\x36\x11\x5e\x79\x38\x26\ +\x3b\x1f\x13\x0b\x0f\x0b\x12\x16\x23\x16\x05\x0e\x09\x1c\x06\x10\ +\x07\x0e\x0b\x46\x8a\x3d\x3d\x49\x51\x3a\x3d\x41\x1b\x99\x8a\x86\ +\x51\x0b\x0f\x0b\x12\x16\x23\x16\x05\x0f\x0a\x25\x02\x03\x0a\x07\ +\x0a\x03\x0a\x10\x25\x61\x67\x40\x65\x27\x4c\x02\xeb\x2f\x34\x47\ +\x4a\x37\x44\x67\x2e\x0a\x0d\x07\x05\x04\x01\x14\x14\x2e\x89\x1f\ +\x3f\x0b\x46\x4b\x36\x39\x69\x38\x28\x32\x4a\x57\x39\x7d\x97\x1e\ +\x05\x04\x01\x14\x14\x2e\xae\x0a\x0a\x0b\x26\x39\x10\x25\x21\x64\ +\x43\x44\x52\x38\x00\x00\x00\x00\x01\x00\x5a\x01\x0e\x01\x08\x01\ +\xbe\x00\x07\x00\x00\x12\x34\x36\x32\x16\x14\x06\x22\x5a\x33\x48\ +\x33\x33\x48\x01\x42\x48\x34\x34\x48\x34\x00\x00\x02\x00\x5b\x01\ +\x03\x02\xfe\x03\x89\x00\x12\x00\x2f\x00\x00\x01\x22\x0e\x03\x15\ +\x14\x33\x32\x3e\x02\x37\x36\x37\x13\x36\x26\x03\x0e\x02\x22\x2e\ +\x02\x35\x34\x3e\x01\x33\x32\x16\x07\x03\x06\x33\x3e\x01\x37\x17\ +\x0e\x03\x23\x22\x01\xc4\x2f\x4d\x2d\x1f\x0b\x3e\x13\x1f\x0d\x1d\ +\x06\x01\x02\x54\x05\x0e\x3d\x15\x1b\x33\x4c\x3b\x3c\x21\x6a\xb2\ +\x5c\x48\x6a\x0d\x4a\x07\x15\x15\x4f\x26\x38\x1b\x35\x35\x56\x3c\ +\x2e\x03\x48\x37\x52\x66\x54\x23\x9e\x07\x07\x14\x03\x0b\x0a\x01\ +\xa2\x18\x10\xfd\xde\x0a\x0f\x0a\x11\x2d\x61\x48\x59\xc1\x85\x4d\ +\x3f\xfe\x95\x25\x01\x77\x6f\x18\x44\x71\x51\x2d\x00\x00\x00\x00\ +\x02\x00\x5b\x01\x09\x02\x8b\x05\x30\x00\x24\x00\x37\x00\x00\x13\ +\x34\x3e\x01\x37\x3e\x01\x33\x32\x16\x07\x03\x3e\x02\x33\x32\x16\ +\x15\x14\x0e\x03\x23\x22\x26\x27\x13\x36\x35\x34\x23\x22\x06\x23\ +\x22\x13\x07\x06\x15\x1c\x01\x15\x14\x16\x33\x32\x36\x37\x36\x35\ +\x34\x26\x23\x22\x90\x1f\x3e\x0e\x13\x61\x0f\x0f\x14\x02\x59\x1d\ +\x20\x3b\x29\x54\x50\x14\x31\x48\x74\x46\x70\x73\x06\x8f\x03\x16\ +\x0b\x24\x0c\x0c\x9d\x32\x08\x0e\x16\x2f\x77\x19\x14\x22\x28\x5d\ +\x04\xc6\x08\x0e\x14\x06\x07\x33\x10\x0c\xfe\x43\x13\x11\x0e\x6f\ +\x60\x2e\x6a\x79\x60\x40\x5f\x4e\x02\xd3\x0d\x06\x1e\x04\xfe\x24\ +\xfa\x28\x1c\x05\x12\x04\x1a\x1a\x7d\x7f\x6b\x37\x32\x2b\x00\x00\ +\x01\xff\x65\xff\xf1\x02\xdf\x05\x1e\x00\x3c\x00\x00\x01\x22\x0e\ +\x02\x07\x33\x15\x23\x06\x07\x0a\x01\x23\x22\x26\x35\x34\x36\x33\ +\x32\x16\x15\x14\x06\x23\x22\x06\x15\x14\x33\x32\x3e\x02\x37\x36\ +\x37\x23\x35\x33\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\ +\x34\x36\x33\x32\x35\x34\x26\x02\x47\x21\x34\x27\x1a\x0e\x95\x9f\ +\x1c\x01\x35\xd5\x75\x55\x43\x3c\x27\x22\x32\x19\x13\x15\x11\x33\ +\x2e\x43\x26\x24\x0e\x0c\x18\x86\x98\x25\x63\x81\x42\x3f\x59\x3c\ +\x27\x22\x32\x19\x13\x26\x21\x04\xd8\x34\x65\x6f\x50\x3f\xb1\x09\ +\xfe\xc6\xfe\xa4\x33\x3e\x3b\x4d\x23\x1f\x1d\x25\x10\x0f\x10\x6a\ +\xab\xd9\x6e\x57\x57\x3f\x75\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\ +\x26\x1f\x07\x09\x00\x00\x00\x00\x01\x00\x54\x01\x09\x04\x99\x03\ +\x89\x00\x3e\x00\x00\x01\x22\x26\x37\x13\x36\x26\x23\x22\x0e\x02\ +\x07\x03\x23\x13\x36\x26\x23\x22\x0e\x02\x07\x03\x23\x13\x36\x23\ +\x22\x06\x07\x27\x3e\x03\x33\x32\x17\x36\x33\x32\x17\x3e\x03\x33\ +\x32\x16\x07\x03\x06\x33\x3e\x01\x37\x17\x0e\x03\x03\x82\x36\x26\ +\x09\x54\x04\x10\x1b\x10\x21\x26\x1e\x07\x5c\x97\x6a\x04\x10\x1b\ +\x10\x21\x26\x1e\x07\x5c\x97\x63\x07\x15\x13\x50\x27\x38\x1b\x35\ +\x35\x56\x3c\x46\x07\x56\x82\x41\x13\x18\x20\x28\x40\x38\x2b\x39\ +\x0d\x4a\x07\x15\x15\x4f\x26\x38\x1b\x35\x35\x56\x01\x09\x46\x2f\ +\x01\xa2\x17\x11\x08\x15\x2f\x22\xfe\x39\x02\x0d\x17\x11\x08\x15\ +\x2f\x22\xfe\x39\x01\xed\x25\x77\x70\x18\x44\x71\x51\x2d\x41\x41\ +\x41\x12\x19\x0f\x07\x4d\x3f\xfe\x95\x25\x01\x77\x6f\x18\x44\x71\ +\x51\x2d\x00\x00\x01\x00\x54\x01\x09\x03\xa0\x03\x89\x00\x2b\x00\ +\x00\x01\x32\x16\x07\x03\x06\x33\x3e\x01\x37\x17\x0e\x03\x23\x22\ +\x26\x37\x13\x36\x26\x23\x22\x06\x07\x03\x23\x13\x36\x23\x22\x06\ +\x07\x27\x3e\x03\x33\x32\x17\x3e\x01\x02\x8d\x31\x68\x0c\x4a\x07\ +\x15\x15\x4f\x26\x38\x1b\x35\x35\x56\x3c\x36\x26\x09\x54\x05\x17\ +\x23\x45\x4f\x0d\x5c\x97\x63\x07\x15\x13\x50\x27\x38\x1b\x35\x35\ +\x56\x3c\x46\x07\x30\x62\x03\x89\x50\x3c\xfe\x95\x25\x01\x77\x6f\ +\x18\x44\x71\x51\x2d\x46\x2f\x01\xa2\x17\x11\x2c\x42\xfe\x39\x01\ +\xed\x25\x77\x70\x18\x44\x71\x51\x2d\x41\x24\x1d\x00\x00\x00\x00\ +\x02\x00\x15\x00\x00\x03\x13\x03\x89\x00\x24\x00\x37\x00\x00\x01\ +\x14\x0e\x04\x23\x22\x27\x03\x33\x15\x21\x35\x33\x13\x36\x23\x22\ +\x06\x07\x27\x3e\x03\x33\x32\x17\x3e\x01\x33\x32\x1e\x02\x25\x07\ +\x06\x15\x14\x33\x32\x3e\x02\x37\x36\x35\x34\x23\x22\x0e\x02\x03\ +\x12\x0c\x1a\x30\x41\x62\x3b\x4a\x32\x34\x7d\xfe\x6a\x82\x8d\x07\ +\x15\x13\x50\x27\x38\x1c\x34\x36\x55\x3d\x46\x06\x33\x49\x3e\x2a\ +\x40\x25\x11\xfe\xa2\x32\x07\x35\x14\x2c\x33\x2d\x0e\x14\x48\x10\ +\x1e\x24\x1d\x02\xa9\x26\x53\x61\x55\x47\x2a\x36\xff\x00\x3f\x3f\ +\x02\xc1\x25\x77\x70\x18\x44\x71\x51\x2d\x41\x26\x1b\x24\x3f\x4e\ +\x02\xfa\x26\x1e\x4f\x15\x2f\x63\x44\x69\x39\x6e\x08\x15\x2f\x00\ +\x01\x00\x54\x01\x13\x02\x4a\x03\x89\x00\x23\x00\x00\x01\x32\x17\ +\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\ +\x35\x34\x26\x23\x22\x07\x03\x23\x13\x36\x26\x23\x22\x07\x27\x36\ +\x01\x15\x2b\x12\x17\x12\x2f\x1f\x32\x4f\x2e\x31\x2f\x28\x2c\x24\ +\x23\x2a\x19\x50\x0f\x63\x97\x65\x02\x0b\x07\x17\x25\x30\x51\x03\ +\x89\x36\x16\x0e\x12\x49\x35\x3c\x50\x25\x25\x1f\x29\x11\x0c\x1b\ +\x4c\xfe\x16\x01\xf4\x0b\x13\x3d\x23\x7e\x00\x00\x01\x00\x5b\x01\ +\x03\x02\x57\x03\x89\x00\x36\x00\x00\x01\x34\x26\x23\x22\x06\x15\ +\x14\x1e\x04\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\ +\x14\x06\x15\x14\x16\x33\x32\x36\x35\x34\x2e\x03\x35\x34\x36\x33\ +\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x01\xdc\x23\x1f\x38\ +\x2c\x26\x3a\x43\x3a\x26\x9e\x74\x5a\x72\x30\x21\x1a\x25\x12\x3a\ +\x29\x2c\x3a\x34\x49\x49\x34\x83\x71\x55\x66\x28\x1a\x20\x20\x07\ +\x03\x14\x11\x24\x23\x31\x1b\x30\x21\x2f\x30\x51\x32\x53\x51\x48\ +\x43\x31\x32\x23\x16\x10\x18\x0b\x1f\x23\x33\x27\x2b\x3d\x27\x2c\ +\x4f\x39\x50\x59\x55\x3c\x18\x20\x1f\x15\x07\x13\x00\x00\x00\x00\ +\x01\x00\x4b\x01\x09\x02\xb2\x03\x89\x00\x31\x00\x00\x01\x32\x1e\ +\x01\x15\x14\x0e\x03\x23\x22\x26\x35\x34\x37\x13\x36\x26\x23\x22\ +\x07\x27\x36\x33\x32\x16\x07\x03\x06\x15\x1c\x01\x15\x14\x16\x33\ +\x32\x36\x37\x36\x35\x34\x26\x23\x22\x26\x35\x34\x36\x02\x51\x25\ +\x2c\x10\x14\x31\x48\x74\x46\x6e\x74\x04\x3a\x02\x0b\x07\x17\x25\ +\x30\x51\x70\x2f\x2c\x0b\x3a\x08\x29\x19\x56\x73\x28\x1a\x1e\x0f\ +\x12\x1f\x27\x03\x89\x38\x57\x40\x2e\x6a\x79\x60\x40\x5e\x42\x2d\ +\x14\x01\x1d\x0b\x13\x3d\x23\x7e\x54\x38\xfe\xe3\x27\x1d\x05\x12\ +\x04\x16\x1e\x5d\x76\x4c\x4b\x1e\x2f\x26\x1e\x1f\x22\x00\x00\x00\ +\x01\x00\x54\x00\xec\x02\xd4\x03\x80\x00\x2a\x00\x00\x01\x32\x36\ +\x35\x34\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x23\x22\x2e\ +\x01\x23\x22\x06\x07\x23\x35\x01\x23\x22\x07\x06\x07\x23\x37\x21\ +\x15\x01\x36\x33\x32\x1e\x02\x02\x17\x19\x2a\x23\x24\x2c\x28\x2f\ +\x30\x2f\xb9\x1b\x3d\x42\x22\x2d\x3a\x1f\x4e\x01\xaa\x80\x2f\x24\ +\x10\x0c\x31\x28\x01\xce\xfe\x6b\x17\x19\x1f\x2f\x19\x29\x01\x52\ +\x1b\x0c\x11\x29\x1f\x25\x25\x50\x3c\xa4\x28\x29\x11\x19\x1d\x01\ +\xc7\x15\x08\x22\xc8\x5b\xfe\x82\x03\x1c\x20\x1c\x00\x00\x00\x00\ +\x01\x00\xc8\x00\x00\x01\x18\x06\x66\x00\x03\x00\x00\x13\x33\x11\ +\x23\xc8\x50\x50\x06\x66\xf9\x9a\x00\x00\x00\x00\x02\x00\xc8\x00\ +\x00\x01\xf5\x06\x66\x00\x03\x00\x07\x00\x00\x01\x33\x11\x23\x03\ +\x33\x11\x23\x01\xa5\x50\x50\xdd\x50\x50\x06\x66\xf9\x9a\x06\x66\ +\xf9\x9a\x00\x00\x02\x00\xc8\x00\x00\x02\x84\x06\x66\x00\x03\x00\ +\x07\x00\x00\x01\x33\x11\x23\x03\x33\x11\x23\x01\xa4\xe0\xe0\xdc\ +\x50\x50\x06\x66\xf9\x9a\x06\x66\xf9\x9a\x00\x00\x02\x00\xc8\x00\ +\x00\x02\x84\x06\x66\x00\x03\x00\x07\x00\x00\x01\x11\x23\x11\x21\ +\x11\x23\x11\x01\xa8\xe0\x01\xbc\x50\x06\x66\xf9\x9a\x06\x66\xf9\ +\x9a\x06\x66\x00\x05\x00\xc8\x00\x00\x01\x18\x06\x66\x00\x03\x00\ +\x07\x00\x0b\x00\x0f\x00\x13\x00\x00\x37\x33\x15\x23\x11\x33\x11\ +\x23\x11\x33\x15\x23\x11\x33\x11\x23\x11\x33\x11\x23\xc8\x50\x50\ +\x50\x50\x50\x50\x50\x50\x50\x50\x99\x99\x05\x43\xfe\xfc\x02\x27\ +\x99\xfc\x5a\xfe\xfc\x02\x92\xfe\xfc\x00\x00\x00\x01\x00\xc8\x00\ +\xbf\x01\x18\x05\xa7\x00\x03\x00\x00\x13\x33\x11\x23\xc8\x50\x50\ +\x05\xa7\xfb\x18\x00\x00\x00\xff\xff\x00\xc8\x00\x00\x03\xe9\x06\ +\x66\x10\x27\x00\x3e\x02\x58\x00\x00\x10\x06\x00\x39\x00\x00\xff\ +\xff\x00\xc8\x00\x00\x03\xe8\x06\x66\x10\x26\x00\x3e\x00\x00\x10\ +\x07\x00\x38\x01\x64\x00\x00\x00\x02\x00\xc8\x02\x08\x01\x90\x04\ +\x5e\x00\x07\x00\x0f\x00\x00\x12\x34\x36\x32\x16\x14\x06\x22\x02\ +\x34\x36\x32\x16\x14\x06\x22\xc8\x3b\x52\x3b\x3b\x52\x3b\x3b\x52\ +\x3b\x3b\x52\x02\x43\x52\x3b\x3b\x52\x3b\x01\xc9\x52\x3b\x3b\x52\ +\x3b\x00\x00\x00\x05\x00\x96\x01\x06\x08\x52\x04\xef\x00\x50\x00\ +\x64\x00\x8a\x00\x92\x00\x9a\x00\x00\x01\x2e\x04\x35\x34\x3e\x02\ +\x33\x32\x1e\x01\x17\x1e\x02\x33\x32\x36\x33\x32\x0f\x01\x0e\x02\ +\x23\x22\x26\x27\x26\x23\x22\x06\x15\x14\x16\x17\x1e\x04\x15\x14\ +\x06\x23\x22\x26\x27\x2e\x02\x23\x22\x06\x23\x22\x3f\x01\x3e\x02\ +\x33\x32\x1e\x01\x17\x1e\x01\x33\x32\x36\x35\x34\x2e\x02\x01\x0e\ +\x01\x1e\x03\x33\x32\x3e\x03\x35\x34\x26\x23\x22\x06\x07\x27\x21\ +\x32\x1e\x04\x15\x14\x06\x04\x23\x21\x22\x2e\x02\x35\x34\x3e\x03\ +\x37\x3e\x01\x37\x13\x36\x27\x2e\x03\x35\x34\x36\x00\x34\x36\x32\ +\x16\x14\x06\x22\x24\x34\x36\x32\x16\x14\x06\x22\x06\x3c\x31\x35\ +\x36\x17\x0f\x37\x54\x5c\x29\x26\x3b\x1e\x13\x0b\x10\x0a\x13\x1d\ +\x33\x04\x0e\x0a\x1c\x02\x02\x0a\x08\x07\x0d\x0c\x46\x8a\x3c\x3e\ +\x49\x51\x2e\x34\x3d\x1f\x16\x99\x8b\x40\x79\x1d\x0b\x10\x0a\x13\ +\x1d\x33\x04\x0e\x0a\x24\x03\x02\x0a\x07\x0a\x04\x09\x11\x24\x61\ +\x67\x41\x65\x16\x2b\x40\xfb\x50\x03\x01\x07\x0e\x22\x29\x23\x50\ +\x87\x5a\x3f\x1c\x8c\x80\x40\x26\x08\xd4\x01\x47\x30\x5a\x67\x53\ +\x46\x27\x9e\xfe\xfb\x99\xfe\xa0\x0d\x0e\x15\x0a\x0a\x15\x10\x1b\ +\x03\x20\x17\x04\x95\x09\x30\x07\x25\x13\x11\x24\x02\x98\x33\x47\ +\x34\x34\x47\x03\x79\x33\x48\x33\x33\x48\x02\xeb\x25\x2c\x38\x33\ +\x43\x2c\x34\x56\x33\x1c\x0a\x0d\x07\x05\x04\x01\x28\x2e\x89\x0a\ +\x0a\x0b\x3f\x0b\x46\x4b\x36\x39\x69\x38\x20\x27\x3c\x37\x4c\x2e\ +\x7d\x97\x13\x0b\x05\x04\x01\x28\x2e\xae\x0a\x0a\x0b\x26\x39\x10\ +\x25\x21\x64\x43\x34\x46\x30\x30\xfe\xc8\x0f\x11\x0f\x07\x05\x01\ +\x41\x68\x85\x87\x41\xa3\xaa\x18\x26\x84\x0d\x21\x40\x5c\x90\x5b\ +\x98\xf9\x89\x01\x05\x0b\x0a\x0a\x10\x08\x05\x04\x01\x06\x0f\x14\ +\x02\xe3\x2f\x0b\x01\x06\x04\x0d\x0a\x0d\x13\xfc\x60\x48\x34\x34\ +\x48\x34\x34\x48\x34\x34\x48\x34\x00\x00\x00\x00\x05\x00\x96\x01\ +\x06\x08\x52\x04\xef\x00\x13\x00\x39\x00\x41\x00\x76\x00\x7e\x00\ +\x00\x01\x0e\x01\x1e\x03\x33\x32\x3e\x03\x35\x34\x26\x23\x22\x06\ +\x07\x27\x21\x32\x1e\x04\x15\x14\x06\x04\x23\x21\x22\x2e\x02\x35\ +\x34\x3e\x03\x37\x3e\x01\x37\x13\x36\x27\x2e\x03\x35\x34\x36\x00\ +\x34\x36\x32\x16\x14\x06\x22\x01\x22\x26\x27\x26\x23\x22\x0e\x03\ +\x15\x14\x1e\x01\x33\x32\x3e\x02\x33\x32\x16\x15\x14\x0e\x02\x23\ +\x22\x2e\x01\x35\x34\x3e\x03\x33\x32\x17\x1e\x02\x33\x32\x3e\x01\ +\x33\x32\x0f\x01\x06\x02\x34\x36\x32\x16\x14\x06\x22\x01\xb7\x03\ +\x01\x07\x0e\x22\x29\x23\x50\x87\x5a\x3f\x1c\x8c\x80\x40\x26\x08\ +\xd4\x01\x47\x30\x5a\x67\x53\x46\x27\x9e\xfe\xfb\x99\xfe\xa0\x0d\ +\x0e\x15\x0a\x0a\x15\x10\x1b\x03\x20\x17\x04\x95\x09\x30\x07\x25\ +\x13\x11\x24\x02\x98\x33\x47\x34\x34\x47\x03\x98\x07\x0e\x0b\x46\ +\x8a\x49\x7d\x54\x3c\x1b\x38\x72\x48\x41\x6a\x32\x24\x04\x07\x17\ +\x2c\x4b\x7d\x45\x72\xad\x53\x2a\x5b\x7c\xb6\x67\x2a\x51\x0b\x0f\ +\x0b\x12\x16\x23\x16\x05\x0e\x09\x1c\x06\x2f\x33\x48\x33\x33\x48\ +\x01\x95\x0f\x11\x0f\x07\x05\x01\x41\x68\x85\x87\x41\xa3\xaa\x18\ +\x26\x84\x0d\x21\x40\x5c\x90\x5b\x98\xf9\x89\x01\x05\x0b\x0a\x0a\ +\x10\x08\x05\x04\x01\x06\x0f\x14\x02\xe3\x2f\x0b\x01\x06\x04\x0d\ +\x0a\x0d\x13\xfc\x60\x48\x34\x34\x48\x34\x03\x0b\x3f\x0b\x46\x44\ +\x6e\x8b\x8a\x41\x4b\x99\x71\x2a\x31\x2a\x12\x06\x0c\x37\x40\x30\ +\x7d\xbc\x6a\x3c\x95\x9e\x83\x54\x1e\x05\x04\x01\x14\x14\x2e\x89\ +\x1f\xfd\x29\x48\x34\x34\x48\x34\x00\x00\x00\x00\x03\x00\x9d\x00\ +\xe4\x04\x63\x05\x5a\x00\x07\x00\x0f\x00\x4f\x00\x00\x12\x34\x36\ +\x32\x16\x14\x06\x22\x00\x34\x36\x32\x16\x14\x06\x22\x27\x1e\x04\ +\x15\x14\x06\x23\x22\x35\x34\x36\x33\x32\x1e\x02\x33\x32\x36\x35\ +\x34\x2e\x04\x27\x01\x23\x01\x2e\x04\x35\x34\x36\x33\x32\x15\x14\ +\x06\x23\x22\x2e\x02\x23\x22\x06\x15\x14\x1e\x04\x17\x01\x33\x9d\ +\x3b\x52\x3b\x3b\x52\x02\xc3\x3b\x52\x3b\x3b\x52\xe4\x37\x55\x55\ +\x38\x23\xa5\x78\xc6\x4b\x2a\x2d\x3c\x15\x14\x0a\x26\x3e\x19\x2d\ +\x3e\x49\x52\x2b\xfe\xe3\x7f\x01\x34\x37\x55\x55\x38\x23\xa5\x78\ +\xc6\x4b\x2a\x2d\x3c\x15\x14\x0a\x27\x3d\x19\x2d\x3e\x49\x52\x2b\ +\x01\x1d\x7f\x02\x51\x52\x3b\x3b\x52\x3b\x01\x85\x52\x3b\x3b\x52\ +\x3b\x0c\x14\x2b\x44\x50\x73\x45\x71\x8c\x7d\x38\x4a\x2e\x36\x2e\ +\x42\x33\x22\x39\x29\x23\x1a\x1c\x0c\xfe\x4b\x01\xd8\x14\x2b\x44\ +\x50\x73\x45\x71\x8c\x7d\x38\x4a\x2e\x36\x2e\x42\x33\x22\x39\x29\ +\x23\x1a\x1c\x0c\x01\xb5\x00\x00\x05\x00\x96\x00\xa0\x04\xce\x05\ +\xa0\x00\x1f\x00\x29\x00\x33\x00\x3d\x00\x47\x00\x00\x01\x33\x15\ +\x1e\x03\x17\x33\x15\x23\x0e\x03\x07\x15\x23\x35\x2e\x03\x27\x23\ +\x35\x33\x3e\x03\x37\x15\x0e\x06\x15\x33\x19\x01\x23\x14\x1e\x05\ +\x17\x3e\x06\x35\x23\x19\x01\x33\x34\x2e\x05\x02\x8a\x50\x4a\x7d\ +\x54\x33\x05\xa1\xa1\x05\x33\x54\x7d\x4a\x50\x4a\x7d\x54\x33\x05\ +\xa1\xa1\x05\x33\x54\x7d\x4a\x1a\x29\x1b\x13\x0a\x06\x01\x82\x82\ +\x01\x06\x0a\x13\x1b\x29\x6a\x1a\x29\x1b\x13\x0a\x06\x01\x82\x82\ +\x01\x06\x0a\x13\x1b\x29\x05\xa0\x98\x09\x52\x7c\x97\x52\x50\x52\ +\x97\x7c\x52\x09\x98\x98\x09\x52\x7c\x97\x52\x50\x52\x97\x7c\x52\ +\x09\x49\x06\x22\x29\x3f\x37\x4f\x38\x29\xfe\x39\x01\x77\x29\x38\ +\x4f\x37\x3f\x29\x22\x06\x06\x22\x29\x3f\x37\x4f\x38\x29\x01\xc7\ +\xfe\x89\x29\x38\x4f\x37\x3f\x29\x22\x00\x00\x00\x01\x00\xbe\x01\ +\x90\x04\x24\x04\xd6\x00\x03\x00\x00\x09\x01\x23\x01\x04\x24\xfd\ +\x8f\xf5\x02\x71\x04\xd6\xfc\xba\x03\x46\x00\x00\x03\x00\xbe\x01\ +\x90\x04\x24\x04\xd6\x00\x07\x00\x0f\x00\x13\x00\x00\x00\x34\x36\ +\x32\x16\x14\x06\x22\x00\x34\x36\x32\x16\x14\x06\x22\x09\x01\x23\ +\x01\x03\x52\x3b\x52\x3b\x3b\x52\xfd\x3b\x3b\x52\x3b\x3b\x52\x03\ +\x21\xfd\x8f\xf5\x02\x71\x02\x43\x52\x3b\x3b\x52\x3b\x01\xc9\x52\ +\x3b\x3b\x52\x3b\x01\x40\xfc\xba\x03\x46\x00\x00\x04\x00\xbe\x01\ +\x90\x05\xf4\x04\xd6\x00\x07\x00\x0b\x00\x13\x00\x17\x00\x00\x00\ +\x34\x36\x32\x16\x14\x06\x22\x13\x01\x23\x01\x00\x34\x36\x32\x16\ +\x14\x06\x22\x09\x01\x23\x01\x05\x22\x3a\x53\x3b\x3b\x53\x98\xfd\ +\x8e\xf4\x02\x70\xfb\xca\x3b\x52\x3b\x3b\x52\x03\x21\xfd\x8f\xf5\ +\x02\x71\x02\x43\x52\x3b\x3b\x52\x3b\x02\xce\xfc\xba\x03\x46\xfe\ +\xfb\x52\x3b\x3b\x52\x3b\x01\x40\xfc\xba\x03\x46\x00\x00\x00\x00\ +\x02\x00\xa5\x04\x09\x04\xd3\x06\x66\x00\x0b\x00\x23\x00\x00\x01\ +\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x26\x12\x22\x0e\x01\x15\ +\x14\x06\x26\x35\x34\x3e\x02\x32\x1e\x02\x15\x14\x06\x26\x35\x34\ +\x26\x02\x58\x3b\x29\x2a\x3a\x3a\x2a\x29\x3b\xdf\xf6\xdc\x7d\x22\ +\x21\x52\x8c\xc9\xe0\xc9\x8c\x52\x21\x22\x7d\x04\x90\x2a\x3a\x3a\ +\x2a\x29\x3b\x3b\x01\x8b\x8d\xd6\x6b\x11\x0d\x0d\x11\x6f\xd3\x9f\ +\x61\x61\x9f\xd3\x6f\x11\x0d\x0d\x11\x6b\xd6\x00\x02\x00\xa5\x00\ +\x00\x04\xd3\x02\x5d\x00\x0b\x00\x25\x00\x00\x01\x34\x36\x33\x32\ +\x16\x15\x14\x06\x23\x22\x26\x12\x32\x3e\x02\x35\x34\x36\x16\x15\ +\x14\x0e\x02\x22\x2e\x02\x35\x34\x36\x16\x15\x14\x1e\x01\x02\x58\ +\x3b\x29\x2a\x3a\x3a\x2a\x29\x3b\x07\xba\xaf\x7d\x4b\x22\x21\x52\ +\x8c\xc9\xe0\xc9\x8c\x52\x21\x22\x4b\x7d\x01\xd6\x29\x3b\x3b\x29\ +\x2a\x3a\x3a\xfe\xc8\x54\x84\xa6\x50\x11\x0d\x0d\x11\x6f\xd3\x9f\ +\x61\x61\x9f\xd3\x6f\x11\x0d\x0d\x11\x50\xa6\x84\x00\x00\x00\x00\ +\x01\x00\xb1\x03\x93\x01\xcc\x05\x62\x00\x13\x00\x00\x01\x32\x16\ +\x15\x14\x07\x23\x22\x26\x35\x34\x37\x3e\x01\x37\x22\x26\x35\x34\ +\x36\x01\x35\x41\x56\xd9\x04\x0a\x0f\x08\x33\x2d\x02\x4b\x44\x49\ +\x05\x62\x4f\x45\xe8\x53\x18\x0d\x0c\x04\x1d\x5c\x4d\x2a\x3d\x31\ +\x3c\x00\x00\x00\x02\x00\xbe\x01\x90\x05\xf4\x04\xd6\x00\x03\x00\ +\x07\x00\x00\x09\x01\x23\x01\x23\x01\x23\x01\x05\xf4\xfd\x8e\xf4\ +\x02\x70\xda\xfd\x8f\xf5\x02\x71\x04\xd6\xfc\xba\x03\x46\xfc\xba\ +\x03\x46\x00\x00\x01\x00\x8c\xff\xca\x01\xce\x06\x9c\x00\x39\x00\ +\x00\x13\x1e\x05\x15\x14\x06\x15\x14\x16\x17\x16\x06\x27\x2e\x03\ +\x35\x34\x3e\x02\x35\x34\x26\x27\x3e\x01\x35\x34\x2e\x02\x35\x34\ +\x3e\x02\x37\x36\x16\x07\x0e\x01\x15\x14\x16\x15\x14\x0e\x04\xdc\ +\x0e\x3a\x23\x31\x1b\x14\x4a\x2f\x40\x08\x27\x09\x28\x4c\x3b\x24\ +\x1f\x25\x1f\x5c\x4c\x4c\x5c\x1f\x25\x1f\x24\x3b\x4c\x28\x09\x27\ +\x08\x40\x2f\x4a\x14\x1b\x31\x23\x3a\x03\x33\x09\x24\x17\x2e\x2f\ +\x4b\x2d\x46\xc8\x46\x43\x4e\x3c\x08\x2c\x07\x1f\x44\x40\x57\x32\ +\x4a\x7a\x41\x53\x29\x19\x67\x3a\x3a\x67\x19\x29\x53\x41\x7a\x4a\ +\x32\x57\x40\x44\x1f\x07\x2c\x08\x3c\x4e\x43\x46\xc8\x46\x2d\x4b\ +\x2f\x2e\x17\x24\x00\x00\x00\x00\x01\x00\xc8\xfe\x62\x02\xd0\x08\ +\x04\x00\x1d\x00\x00\x21\x1e\x01\x15\x14\x06\x23\x22\x2e\x05\x27\ +\x11\x3e\x06\x33\x32\x16\x15\x14\x06\x07\x01\x7c\x97\xbd\x20\x0a\ +\x0b\x17\x1e\x28\x45\x57\x88\x52\x52\x88\x57\x45\x28\x1e\x17\x0b\ +\x0a\x20\xbd\x97\x5a\xef\x35\x08\x18\x1b\x30\x3e\x4c\x4e\x56\x25\ +\x06\x66\x25\x56\x4e\x4c\x3e\x30\x1b\x18\x08\x35\xef\x5a\x00\x00\ +\x01\x00\x00\x03\x1c\x06\x40\x03\x4a\x00\x03\x00\x00\x11\x21\x15\ +\x21\x06\x40\xf9\xc0\x03\x4a\x2e\x00\x00\x00\x00\x02\x00\x00\x02\ +\x55\x06\x40\x04\x11\x00\x03\x00\x07\x00\x00\x11\x21\x15\x21\x11\ +\x21\x15\x21\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x04\x11\x2e\xfe\xa0\ +\x2e\x00\x00\x00\x03\x00\x00\x01\x8e\x06\x40\x04\xd8\x00\x03\x00\ +\x07\x00\x0b\x00\x00\x11\x21\x15\x21\x11\x21\x15\x21\x11\x21\x15\ +\x21\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x04\xd8\x2e\ +\xfe\xa0\x2e\xfe\xa0\x2e\x00\x00\x04\x00\x00\x00\xc7\x06\x40\x05\ +\x9f\x00\x03\x00\x07\x00\x0b\x00\x0f\x00\x00\x11\x21\x15\x21\x11\ +\x21\x15\x21\x11\x21\x15\x21\x11\x21\x15\x21\x06\x40\xf9\xc0\x06\ +\x40\xf9\xc0\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x05\x9f\x2e\xfe\xa0\ +\x2e\xfe\xa0\x2e\xfe\xa0\x2e\x00\x05\x00\x00\x00\x00\x06\x40\x06\ +\x66\x00\x03\x00\x07\x00\x0b\x00\x0f\x00\x13\x00\x00\x11\x21\x15\ +\x21\x11\x21\x15\x21\x11\x21\x15\x21\x11\x21\x15\x21\x11\x21\x15\ +\x21\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x06\x40\xf9\ +\xc0\x06\x40\xf9\xc0\x04\xd8\x2e\xfe\xa0\x2e\xfe\xa0\x2e\xfe\xa0\ +\x2e\x06\x66\x2e\x00\x00\x00\x00\x06\x00\x00\xff\x39\x06\x40\x07\ +\x2d\x00\x03\x00\x07\x00\x0b\x00\x0f\x00\x13\x00\x17\x00\x00\x15\ +\x21\x15\x21\x11\x21\x15\x21\x11\x21\x15\x21\x11\x21\x15\x21\x11\ +\x21\x15\x21\x11\x21\x15\x21\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x06\ +\x40\xf9\xc0\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x06\x40\xf9\xc0\x99\ +\x2e\x06\x66\x2e\xfe\xa0\x2e\xfe\xa0\x2e\xfe\xa0\x2e\x06\x66\x2e\ +\x00\x00\x00\x00\x15\x00\xc8\x00\xc8\x06\x0e\x04\xfe\x00\x03\x00\ +\x07\x00\x0b\x00\x0f\x00\x13\x00\x17\x00\x1b\x00\x1f\x00\x23\x00\ +\x27\x00\x2b\x00\x2f\x00\x33\x00\x37\x00\x3b\x00\x3f\x00\x43\x00\ +\x47\x00\x4b\x00\x4f\x00\x53\x00\x00\x01\x33\x35\x23\x11\x33\x35\ +\x23\x35\x33\x35\x23\x35\x33\x35\x23\x25\x33\x35\x23\x07\x35\x23\ +\x15\x21\x33\x35\x23\x05\x33\x35\x23\x01\x21\x11\x21\x01\x33\x35\ +\x23\x35\x33\x35\x23\x35\x33\x35\x23\x03\x35\x23\x15\x13\x35\x23\ +\x15\x13\x35\x23\x15\x01\x33\x35\x23\x35\x33\x35\x23\x35\x33\x35\ +\x23\x01\x33\x35\x23\x35\x33\x35\x23\x35\x33\x35\x23\x05\x0a\xd2\ +\xd2\xd2\xd2\xd2\xd2\xd2\xd2\xfd\xf8\xd2\xd2\x32\xd2\x02\x08\xd2\ +\xd2\xfc\xf4\xd2\xd2\x05\x14\xfa\xba\x05\x46\xfc\xf4\xd2\xd2\xd2\ +\xd2\xd2\xd2\x32\xd2\xd2\xd2\xd2\xd2\x02\x08\xd2\xd2\xd2\xd2\xd2\ +\xd2\xfc\xf4\xd2\xd2\xd2\xd2\xd2\xd2\x03\xfc\xd4\xfc\x26\xd4\x2e\ +\xd4\x2e\xd4\x2e\xd4\xd4\xd4\xd4\xd4\xd4\xd4\xfb\xf8\x04\x36\xfb\ +\xf8\xd4\x2e\xd4\x2e\xd4\xfd\x28\xd4\xd4\x01\x02\xd4\xd4\x01\x02\ +\xd4\xd4\xfd\xfc\xd4\x2e\xd4\x2e\xd4\xfd\x28\xd4\x2e\xd4\x2e\xd4\ +\x00\x00\x00\x00\x0d\x00\xc8\x00\xc8\x04\x06\x04\xfe\x00\x03\x00\ +\x07\x00\x0b\x00\x0f\x00\x13\x00\x17\x00\x1b\x00\x1f\x00\x23\x00\ +\x27\x00\x2b\x00\x2f\x00\x33\x00\x00\x01\x33\x35\x23\x07\x35\x23\ +\x15\x21\x33\x35\x23\x01\x21\x11\x21\x01\x33\x35\x23\x35\x33\x35\ +\x23\x35\x33\x35\x23\x03\x35\x23\x15\x13\x35\x23\x15\x13\x35\x23\ +\x15\x01\x33\x35\x23\x35\x33\x35\x23\x35\x33\x35\x23\x03\x02\xd2\ +\xd2\x32\xd2\xfe\xfc\xd2\xd2\x03\x0c\xfc\xc2\x03\x3e\xfe\xfc\xd2\ +\xd2\xd2\xd2\xd2\xd2\x32\xd2\xd2\xd2\xd2\xd2\xfe\xfc\xd2\xd2\xd2\ +\xd2\xd2\xd2\x03\xfc\xd4\xd4\xd4\xd4\xd4\xfb\xf8\x04\x36\xfb\xf8\ +\xd4\x2e\xd4\x2e\xd4\xfd\x28\xd4\xd4\x01\x02\xd4\xd4\x01\x02\xd4\ +\xd4\xfd\xfc\xd4\x2e\xd4\x2e\xd4\x00\x00\x00\x00\x04\x00\xb8\xfd\ +\x13\x04\x8c\x08\x79\x00\x1e\x00\x2a\x00\x6d\x00\x7a\x00\x00\x25\ +\x2e\x04\x35\x34\x36\x37\x26\x27\x00\x15\x14\x1e\x02\x33\x32\x37\ +\x2e\x02\x27\x0e\x02\x15\x14\x16\x13\x1e\x02\x17\x3e\x01\x35\x34\ +\x26\x23\x22\x03\x17\x36\x33\x32\x1e\x02\x15\x14\x06\x07\x16\x15\ +\x14\x0e\x02\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x07\ +\x1e\x01\x33\x32\x3e\x02\x35\x34\x27\x06\x23\x22\x00\x35\x34\x3e\ +\x03\x37\x26\x35\x34\x3e\x03\x33\x32\x1e\x01\x15\x14\x02\x07\x27\ +\x24\x35\x34\x2e\x02\x23\x22\x0e\x01\x15\x14\x02\xbc\x23\x3b\x42\ +\x2e\x1e\x68\x5e\x08\x35\xfe\xbf\x55\x7e\x8f\x3c\x58\x4a\x11\x2b\ +\x46\x10\x31\x4c\x21\x30\xb4\x10\x41\x2c\x12\x43\x4a\x97\x74\x09\ +\x7d\x43\x1f\x21\x3f\x81\x6d\x45\x79\x67\x36\x21\x40\x6e\x46\xa8\ +\xb1\x58\x55\x4c\x5f\x4e\x46\x11\x3e\x48\x36\x51\x2c\x15\x30\x54\ +\x5b\xe3\xfe\xe3\x25\x3d\x62\x65\x46\x23\x12\x2c\x41\x69\x40\x25\ +\x50\x33\x95\xa3\x30\x01\x1c\x09\x15\x2a\x1d\x21\x60\x46\xe7\x03\ +\x0e\x22\x33\x56\x39\x6b\xbc\x2a\x1e\xbc\xfe\xde\xf5\x5b\xa6\x6c\ +\x3f\x20\x4a\xa1\xf2\x3c\x11\x46\x3e\x13\x61\x4e\x01\x65\x3b\xde\ +\xa0\x48\x30\x97\x5f\x69\x73\x01\xb1\xfb\x06\x37\x64\xa1\x60\x71\ +\xc2\x3a\xfe\xfe\x34\x67\x5c\x39\xa0\x8c\x56\x5f\x5c\x4d\x42\x47\ +\x05\x2e\x2c\x2d\x48\x4c\x25\xec\xea\x1f\x01\x42\xfa\x54\x9a\x7e\ +\x80\x67\x3f\xad\xe1\x3e\x89\x95\x75\x4c\x86\xcc\x5f\xcd\xfe\xce\ +\x88\xc9\xfd\xc9\x28\x4b\x4b\x2e\x86\xb1\x39\xd1\x00\x00\x00\xff\ +\xff\x00\xb8\xfd\x13\x04\x8c\x0a\x4d\x10\x27\x00\x1c\x02\x26\x08\ +\x5e\x10\x06\x00\x54\x00\x00\xff\xff\x00\xb8\xfb\x43\x04\x8c\x08\ +\x79\x10\x27\x00\x1c\x02\x26\xfb\x48\x10\x06\x00\x54\x00\x00\x00\ +\x02\x00\xc8\xff\xff\x05\x6d\x06\x66\x00\x60\x00\x64\x00\x00\x01\ +\x17\x36\x33\x32\x16\x15\x14\x0e\x03\x23\x22\x26\x35\x34\x36\x33\ +\x32\x16\x15\x14\x07\x1e\x01\x33\x32\x11\x34\x26\x23\x22\x0e\x02\ +\x07\x0e\x02\x23\x22\x26\x27\x2e\x01\x27\x11\x23\x11\x33\x11\x3e\ +\x01\x37\x3e\x01\x33\x32\x1e\x01\x17\x1e\x03\x33\x32\x36\x35\x10\ +\x23\x22\x06\x07\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\ +\x1e\x03\x15\x14\x06\x23\x22\x27\x01\x11\x23\x11\x03\x46\x4e\x34\ +\x5a\xb0\x9a\x16\x32\x4b\x70\x46\x9f\xb2\x4d\x32\x3b\x4b\x4c\x14\ +\x42\x42\x9c\x43\x59\x24\x3a\x2e\x20\x14\x04\x06\x0f\x0b\x0b\x2a\ +\x05\x27\x30\x2c\x50\x50\x2c\x30\x27\x05\x2a\x0b\x0b\x0f\x06\x04\ +\x14\x20\x2e\x3a\x24\x59\x43\x9c\x42\x42\x14\x4c\x4b\x3b\x32\x4d\ +\xb2\x9f\x46\x70\x4b\x32\x16\x9a\xb0\x5a\x34\xfe\x12\xde\x03\x33\ +\xa5\x2c\xa5\xa5\x3d\x6b\x5e\x43\x27\x83\x53\x3b\x4e\x45\x38\x54\ +\x24\x15\x11\x01\x2c\x95\x75\x16\x2f\x2f\x25\x08\x08\x07\x5b\x10\ +\x54\x5b\x43\xfc\xd9\x06\x66\xfc\xd9\x43\x5b\x54\x10\x5b\x07\x08\ +\x08\x25\x2f\x2f\x16\x75\x95\x01\x2c\x11\x15\x24\x54\x38\x45\x4e\ +\x3b\x53\x83\x27\x43\x5e\x6b\x3d\xa5\xa5\x2c\x02\x8e\xf9\x9a\x06\ +\x66\x00\x00\x00\x03\x00\x90\x01\x39\x05\x9a\x06\x59\x00\x07\x00\ +\x0f\x00\x3d\x00\x00\x00\x34\x36\x32\x16\x14\x06\x22\x02\x34\x36\ +\x32\x16\x14\x06\x22\x05\x22\x26\x35\x34\x3e\x02\x33\x32\x1e\x02\ +\x15\x14\x0e\x05\x07\x06\x23\x22\x26\x35\x34\x37\x24\x00\x35\x34\ +\x26\x23\x22\x0e\x03\x07\x36\x33\x32\x15\x14\x04\xd2\x3a\x53\x3b\ +\x3b\x53\x3a\x3a\x53\x3b\x3b\x53\xfc\xb9\x54\x68\x28\x56\x9d\x6a\ +\x67\xaa\x71\x3e\x2b\x4b\x7c\x8c\xc2\xc3\x80\x04\x04\x11\x22\x04\ +\x01\x4e\x01\x6c\x63\x61\x3d\x60\x3b\x2b\x14\x06\x21\x35\xb1\x03\ +\xd1\x52\x3b\x3b\x52\x3b\x01\xc9\x52\x3b\x3b\x52\x3b\xd8\x61\x62\ +\x3f\x6f\x62\x3a\x3d\x6d\x94\x57\x60\xaa\x8e\x83\x6c\x6f\x5d\x36\ +\x02\x2c\x0f\x06\x02\x9c\x01\xa1\xfc\xa2\xb7\x16\x23\x32\x2e\x1b\ +\x05\x8f\x84\xff\xff\x00\x90\x01\x39\x05\x9a\x08\x40\x10\x27\x00\ +\x1c\x02\x26\x06\x51\x10\x06\x00\x58\x00\x00\xff\xff\x00\x90\xff\ +\xe2\x05\x9a\x06\x59\x10\x27\x00\x1c\x02\x26\xff\xe7\x10\x06\x00\ +\x58\x00\x00\x00\x02\x00\xc8\x01\x90\x03\x6a\x04\xd6\x00\x03\x00\ +\x07\x00\x00\x01\x11\x23\x11\x23\x11\x23\x11\x03\x6a\xe0\xe2\xe0\ +\x04\xd6\xfc\xba\x03\x46\xfc\xba\x03\x46\x00\x00\x02\x00\xc8\x00\ +\x82\x02\xd4\x05\xe4\x00\x03\x00\x07\x00\x00\x37\x11\x21\x11\x01\ +\x11\x21\x11\xc8\x02\x0c\xfe\x5c\x01\x3c\x82\x05\x62\xfa\x9e\x04\ +\xae\xfc\x06\x03\xfa\x00\x00\xff\xff\x00\xc8\x01\x8e\x0a\xf0\x04\ +\xd8\x10\x06\x00\x10\x00\x00\x00\x01\x00\xc8\x00\x00\x02\x84\x01\ +\xbc\x00\x13\x00\x00\x13\x35\x33\x15\x33\x35\x33\x15\x23\x15\x33\ +\x15\x23\x35\x23\x15\x23\x35\x33\x35\xc8\x9e\x80\x9e\x8a\x8a\x9e\ +\x80\x9e\x8a\x01\x1e\x9e\x8a\x8a\x9e\x80\x9e\x8a\x8a\x9e\x80\x00\ +\x03\x00\xc8\xff\xd8\x03\x84\x03\xe1\x00\x0b\x00\x26\x00\x32\x00\ +\x00\x01\x11\x3e\x03\x35\x34\x23\x22\x0e\x01\x03\x11\x33\x11\x3e\ +\x02\x33\x32\x17\x11\x33\x11\x3e\x02\x33\x32\x16\x15\x14\x06\x07\ +\x23\x35\x06\x07\x01\x11\x3e\x03\x35\x34\x23\x22\x0e\x01\x01\x0e\ +\x2a\x39\x36\x1b\x32\x1a\x22\x26\x66\x46\x25\x25\x40\x2a\x29\x1d\ +\x46\x25\x25\x40\x2a\x42\x40\xa1\x95\x46\x49\xb1\x01\x40\x2a\x39\ +\x36\x1b\x32\x1a\x22\x26\x01\x21\xff\x00\x1e\x32\x40\x47\x27\x44\ +\x0c\x1e\xfe\x9f\x04\x09\xfd\x8b\x1c\x18\x13\x13\x02\x41\xfd\x8b\ +\x1c\x18\x13\x5c\x43\x49\x9a\x59\xbb\x52\x69\x01\x49\xff\x00\x1e\ +\x32\x40\x47\x27\x44\x0c\x1e\x00\x02\x00\x82\xff\xd8\x02\x44\x04\ +\xc7\x00\x12\x00\x1e\x00\x00\x13\x16\x17\x23\x11\x3e\x02\x33\x32\ +\x16\x15\x14\x06\x07\x23\x11\x23\x36\x13\x11\x3e\x03\x35\x34\x23\ +\x22\x0e\x01\xeb\x47\x22\x46\x25\x25\x40\x2a\x42\x40\xa1\x95\x46\ +\x46\x22\x6a\x2a\x39\x36\x1b\x32\x1a\x22\x26\x04\xc7\x97\x2a\xfd\ +\x66\x1c\x18\x13\x5c\x43\x49\x9a\x59\x04\x2e\x2a\xfc\xf1\xff\x00\ +\x1e\x32\x40\x47\x27\x44\x0c\x1e\x00\x00\x00\x00\x02\x00\x82\xfe\ +\xa2\x02\x44\x03\xe1\x00\x13\x00\x1f\x00\x00\x05\x33\x06\x07\x26\ +\x27\x33\x11\x33\x11\x3e\x02\x33\x32\x16\x15\x14\x06\x07\x19\x01\ +\x3e\x03\x35\x34\x23\x22\x0e\x01\x01\x0e\x46\x22\x47\x47\x22\x46\ +\x46\x25\x25\x40\x2a\x42\x40\xa1\x95\x2a\x39\x36\x1b\x32\x1a\x22\ +\x26\x9d\x2a\x97\x97\x2a\x04\x7e\xfd\x8b\x1c\x18\x13\x5c\x43\x49\ +\x9a\x59\x01\x49\xff\x00\x1e\x32\x40\x47\x27\x44\x0c\x1e\x00\x00\ +\x02\x00\x82\xfe\x8d\x02\x44\x04\x31\x00\x03\x00\x10\x00\x00\x01\ +\x15\x37\x35\x03\x11\x25\x11\x23\x11\x05\x11\x23\x36\x37\x16\x17\ +\x01\x0e\xf0\xf0\x01\x36\x46\xfe\xca\x46\x22\x47\x47\x22\x01\x2e\ +\xbb\x1b\xbb\x02\x27\xfe\x4b\x29\xfc\xa9\x01\x74\x29\x03\x98\x2a\ +\x97\x97\x2a\x00\x02\x00\xc8\xfd\x8b\x02\x8a\x03\x2f\x00\x03\x00\ +\x10\x00\x00\x01\x15\x37\x35\x13\x33\x06\x07\x26\x27\x33\x11\x05\ +\x11\x33\x11\x25\x01\x0e\xf0\x46\x46\x22\x47\x47\x22\x46\xfe\xca\ +\x46\x01\x36\x01\x2e\xbb\x1b\xbb\xfd\x03\x2a\x97\x97\x2a\x01\xb5\ +\x29\x03\x57\xfe\x8c\x29\x00\x00\x02\x00\xc8\xfe\x84\x02\x44\x04\ +\xc7\x00\x27\x00\x2b\x00\x00\x01\x23\x3e\x02\x37\x1e\x05\x17\x23\ +\x11\x37\x15\x07\x15\x37\x15\x07\x11\x23\x11\x07\x11\x23\x11\x07\ +\x35\x37\x35\x07\x35\x37\x11\x33\x11\x37\x03\x37\x35\x07\x01\xb8\ +\x46\x12\x29\x14\x1a\x08\x12\x11\x11\x11\x13\x09\x46\x46\x46\x46\ +\x46\x46\x64\x46\x46\x46\x46\x46\x46\x64\x64\x64\x64\x04\x06\x16\ +\x49\x2a\x38\x12\x26\x22\x22\x1e\x1c\x0b\xfd\xd5\x09\x8c\x09\xc2\ +\x09\x8c\x09\xfe\xc1\x01\x36\x0e\xfe\x9a\x01\x5d\x09\x8c\x09\xc2\ +\x09\x8c\x09\x01\x3f\xfe\xca\x0e\xfe\xa4\x0e\xc2\x0e\x00\x00\x00\ +\x02\x00\xc8\xfc\xf5\x02\x44\x03\x38\x00\x27\x00\x2b\x00\x00\x01\ +\x33\x0e\x02\x07\x2e\x05\x27\x33\x11\x07\x11\x23\x11\x07\x35\x37\ +\x35\x07\x35\x37\x11\x33\x11\x37\x11\x33\x11\x37\x15\x07\x15\x37\ +\x15\x07\x27\x37\x35\x07\x01\xfe\x46\x12\x29\x14\x1a\x08\x12\x11\ +\x11\x11\x13\x09\x46\x64\x46\x46\x46\x46\x46\x46\x64\x46\x46\x46\ +\x46\x46\xaa\x64\x64\xfd\xb6\x16\x49\x2a\x38\x12\x26\x22\x22\x1e\ +\x1c\x0b\x02\x42\x0e\xfe\x9a\x01\x5d\x09\x8c\x09\xc2\x09\x8c\x09\ +\x01\x3f\xfe\xca\x0e\x01\x66\xfe\xa3\x09\x8c\x09\xc2\x09\x8c\x09\ +\x75\x0e\xc2\x0e\x00\x00\x00\x00\x03\x00\x6c\xff\x39\x01\xd2\x03\ +\xe4\x00\x02\x00\x06\x00\x29\x00\x00\x13\x35\x07\x13\x37\x35\x07\ +\x03\x37\x33\x15\x33\x15\x23\x11\x37\x35\x33\x15\x37\x15\x07\x15\ +\x37\x15\x07\x15\x23\x35\x07\x15\x23\x35\x07\x35\x37\x35\x07\x35\ +\x37\x11\x23\xf9\x4d\x7e\x46\x46\xbe\x8d\x31\x23\x23\x46\x31\x31\ +\x31\x31\x31\x31\x46\x31\x31\x31\x31\x31\x8d\x02\xee\x8c\x8c\xfd\ +\xa7\x0a\x88\x0a\x01\xc9\xfe\xf6\x32\xfe\xc3\x0a\xfa\xf4\x06\x62\ +\x06\x88\x07\x62\x07\xdf\xd9\x0a\xfa\xf4\x06\x62\x06\x88\x07\x62\ +\x07\x01\x43\x00\x03\x00\x3b\x00\x73\x01\xd2\x03\xe4\x00\x09\x00\ +\x1e\x00\x21\x00\x00\x13\x15\x3e\x01\x35\x34\x23\x22\x0e\x01\x03\ +\x11\x3e\x02\x33\x32\x16\x15\x14\x06\x07\x23\x11\x23\x35\x37\x33\ +\x15\x33\x15\x27\x35\x07\xf9\x3d\x41\x23\x12\x18\x1b\x16\x1a\x1a\ +\x2d\x1d\x2e\x2d\x70\x69\x31\x8d\x8d\x31\x23\x54\x4d\x01\x59\xb3\ +\x2c\x52\x34\x2f\x08\x15\x01\x52\xfe\xd2\x13\x11\x0d\x40\x2f\x33\ +\x6b\x3f\x02\x49\x2a\xfe\xf6\x32\x32\x8c\x8c\x00\x01\x00\xa4\x01\ +\x9f\x03\x2c\x04\xc4\x00\x31\x00\x00\x13\x34\x3e\x02\x33\x32\x16\ +\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x17\x2e\x01\x23\x22\ +\x0e\x02\x15\x14\x1e\x02\x33\x32\x3e\x01\x3f\x01\x32\x16\x15\x14\ +\x0e\x02\x23\x22\x2e\x02\xa4\x31\x58\x8b\x53\x85\x8f\x53\x4d\x40\ +\x40\x4c\x37\x1e\x1c\x19\x5d\x40\x33\x4b\x26\x11\x10\x27\x4c\x38\ +\x36\x5c\x2e\x10\x0f\x15\x2c\x24\x41\x75\x46\x5b\x8d\x55\x2b\x03\ +\x33\x47\x8c\x75\x49\x86\x58\x6a\x52\x52\x3f\x36\x44\x0b\x29\x35\ +\x40\x6b\x6e\x3c\x4a\x70\x64\x35\x41\x4d\x20\x21\x09\x03\x16\x51\ +\x5b\x42\x41\x72\x8f\x00\x00\x00\x02\x00\xa4\x00\xe1\x03\x2c\x05\ +\x82\x00\x32\x00\x38\x00\x00\x01\x33\x15\x36\x33\x32\x16\x15\x14\ +\x06\x23\x22\x26\x35\x34\x36\x33\x32\x17\x2e\x01\x23\x22\x07\x11\ +\x16\x33\x32\x3e\x01\x3f\x01\x32\x16\x15\x14\x0e\x02\x23\x22\x27\ +\x15\x23\x35\x2e\x01\x35\x34\x36\x37\x19\x01\x0e\x01\x15\x14\x01\ +\xae\x3c\x16\x0b\x85\x8f\x53\x4d\x40\x40\x4c\x37\x1e\x1c\x19\x5d\ +\x40\x0e\x0e\x10\x12\x36\x5c\x2e\x10\x0f\x15\x2c\x24\x41\x75\x46\ +\x17\x0b\x3c\x84\x86\x90\x7a\x31\x2c\x05\x82\xc0\x02\x86\x58\x6a\ +\x52\x52\x3f\x36\x44\x0b\x29\x35\x03\xfd\x5e\x03\x41\x4d\x20\x21\ +\x09\x03\x16\x51\x5b\x42\x01\xbf\xc8\x1e\xd9\x93\x7e\xe3\x23\xfd\ +\x45\x02\x6c\x2b\xaa\x60\xf4\x00\x06\x00\xc8\x01\xa9\x05\x36\x04\ +\x63\x00\x09\x00\x16\x00\x2c\x00\x3b\x00\x55\x00\x7f\x00\x00\x01\ +\x0e\x01\x15\x14\x16\x32\x36\x35\x34\x27\x3e\x01\x35\x34\x26\x22\ +\x06\x15\x14\x1e\x02\x17\x1e\x01\x15\x14\x23\x22\x35\x34\x36\x37\ +\x2e\x01\x35\x34\x36\x33\x32\x16\x15\x14\x06\x25\x22\x0e\x01\x15\ +\x14\x33\x32\x36\x37\x34\x3f\x01\x36\x26\x03\x0e\x02\x23\x22\x35\ +\x34\x36\x33\x32\x16\x0f\x01\x06\x33\x32\x36\x37\x17\x0e\x03\x23\ +\x22\x01\x32\x15\x14\x0e\x01\x23\x22\x26\x35\x34\x3f\x01\x36\x26\ +\x23\x22\x07\x27\x36\x33\x32\x16\x0f\x01\x06\x1d\x01\x14\x16\x33\ +\x32\x36\x37\x36\x35\x34\x26\x22\x26\x35\x34\x01\x8d\x38\x43\x53\ +\x88\x54\x81\x3b\x32\x4d\x6e\x4c\x17\x33\x2a\x5e\x3f\x54\xe2\xe1\ +\x51\x3f\x39\x3b\x78\x4d\x4e\x78\x3f\x02\x6d\x26\x32\x10\x1e\x11\ +\x16\x0a\x01\x29\x02\x06\x1e\x0a\x0e\x18\x13\x5d\x74\x44\x23\x34\ +\x06\x25\x02\x0a\x09\x27\x13\x1b\x0d\x1a\x1a\x2a\x1d\x19\xfe\xff\ +\x2f\x1b\x4d\x38\x36\x39\x02\x1d\x01\x06\x03\x0a\x13\x18\x29\x35\ +\x18\x16\x06\x1d\x03\x14\x0c\x2a\x39\x13\x0d\x0f\x10\x10\x03\x05\ +\x1b\x55\x2f\x4f\x4a\x4a\x4f\x56\x85\x18\x3a\x33\x38\x41\x41\x38\ +\x1f\x2d\x1e\x10\x21\x20\x6c\x39\xbd\xbd\x34\x61\x1f\x19\x4c\x47\ +\x44\x59\x59\x44\x3f\x46\xfc\x46\x4a\x20\x4d\x0a\x08\x07\x03\xcd\ +\x0c\x08\xfe\xf4\x05\x07\x05\x71\x45\x87\x26\x1f\xb2\x12\x3b\x36\ +\x0c\x21\x38\x27\x16\x01\x3a\x65\x29\x5b\x51\x2f\x20\x16\x09\x8c\ +\x06\x09\x1e\x11\x3e\x29\x1c\x8c\x1b\x06\x0d\x0a\x10\x2e\x3a\x28\ +\x21\x0f\x17\x13\x0f\x20\x00\x00\x06\x00\xc8\x01\xd4\x04\xfc\x05\ +\x56\x00\x21\x00\x30\x00\x5c\x00\x66\x00\x74\x00\x89\x00\x00\x01\ +\x34\x3e\x01\x37\x3e\x01\x33\x32\x16\x0f\x01\x3e\x02\x33\x32\x15\ +\x14\x0e\x02\x23\x22\x26\x27\x13\x36\x35\x34\x23\x22\x06\x22\x17\ +\x07\x06\x1d\x01\x14\x33\x32\x36\x37\x36\x35\x34\x23\x22\x27\x32\ +\x15\x14\x0e\x02\x23\x22\x26\x35\x34\x3f\x01\x36\x26\x23\x22\x07\ +\x27\x36\x33\x32\x16\x0f\x01\x06\x1d\x01\x14\x16\x33\x32\x36\x37\ +\x36\x35\x34\x26\x23\x22\x26\x35\x34\x01\x0e\x01\x15\x14\x16\x32\ +\x36\x35\x34\x27\x3e\x01\x35\x34\x26\x22\x06\x15\x14\x1e\x03\x17\ +\x1e\x01\x15\x14\x23\x22\x35\x34\x36\x37\x2e\x01\x35\x34\x36\x32\ +\x16\x15\x14\x06\x04\x04\x10\x1d\x07\x09\x30\x07\x08\x0a\x01\x2c\ +\x0f\x0e\x1e\x14\x50\x11\x22\x42\x2b\x37\x39\x02\x46\x01\x0a\x05\ +\x12\x0c\x4d\x19\x04\x12\x17\x3a\x0c\x0a\x24\x2e\xc7\x2f\x11\x23\ +\x42\x2a\x36\x39\x02\x1d\x01\x06\x03\x0b\x13\x17\x29\x35\x17\x15\ +\x05\x1c\x04\x14\x0c\x2b\x38\x14\x0c\x0f\x07\x09\x0f\xfe\x1c\x37\ +\x44\x54\x88\x54\x82\x3c\x32\x4d\x6e\x4d\x12\x19\x2f\x22\x57\x3f\ +\x54\xe2\xe1\x51\x3f\x3a\x3a\x77\x9c\x78\x3f\x05\x23\x04\x07\x09\ +\x03\x04\x18\x07\x06\xda\x0a\x08\x07\x66\x1d\x43\x46\x2e\x2f\x26\ +\x01\x62\x04\x05\x0f\x02\xe9\x7b\x12\x0f\x0d\x1a\x3e\x3e\x30\x1f\ +\x2e\x20\x66\x1c\x44\x46\x2e\x2f\x20\x15\x0a\x8c\x05\x0a\x1e\x11\ +\x3e\x2a\x1b\x8c\x12\x0f\x0d\x0b\x0f\x2e\x3a\x25\x24\x0f\x17\x13\ +\x0f\x20\xfe\xa8\x1c\x55\x2e\x4f\x4a\x4a\x4f\x54\x87\x17\x3a\x34\ +\x38\x41\x41\x38\x19\x28\x19\x17\x0b\x1f\x20\x6c\x39\xbd\xbd\x34\ +\x62\x1e\x19\x4b\x48\x45\x58\x58\x45\x40\x46\x00\x04\x00\xc8\x01\ +\xa8\x06\xf8\x04\x87\x00\x51\x00\x61\x00\x8d\x00\xb5\x00\x00\x01\ +\x22\x26\x3f\x01\x36\x26\x23\x22\x0f\x01\x23\x13\x36\x26\x23\x22\ +\x0f\x01\x23\x37\x36\x23\x22\x06\x07\x27\x3e\x03\x33\x32\x17\x36\ +\x33\x32\x17\x3e\x03\x33\x32\x16\x0f\x01\x06\x33\x32\x36\x37\x3e\ +\x01\x33\x32\x16\x0f\x01\x06\x33\x32\x36\x37\x17\x0e\x03\x23\x22\ +\x27\x0e\x04\x23\x22\x27\x0e\x01\x13\x22\x0e\x02\x15\x14\x33\x32\ +\x36\x37\x34\x3f\x01\x36\x26\x25\x0e\x03\x2b\x01\x07\x32\x16\x15\ +\x14\x0e\x01\x23\x22\x27\x2e\x01\x35\x34\x33\x32\x1e\x01\x17\x1e\ +\x01\x33\x32\x36\x35\x34\x26\x23\x13\x33\x32\x37\x36\x33\x32\x15\ +\x14\x01\x21\x22\x35\x34\x33\x32\x3e\x02\x35\x11\x36\x26\x27\x26\ +\x0f\x01\x0e\x01\x27\x26\x27\x34\x3f\x01\x36\x33\x32\x16\x15\x11\ +\x14\x1e\x02\x33\x32\x15\x14\x05\x4d\x1b\x12\x04\x29\x02\x08\x0d\ +\x32\x0a\x2e\x4a\x34\x02\x08\x0d\x32\x0a\x2e\x4a\x31\x02\x09\x0a\ +\x26\x14\x1b\x0d\x1a\x1b\x29\x1e\x22\x03\x29\x41\x20\x09\x0c\x0f\ +\x14\x1f\x1c\x15\x1c\x06\x24\x04\x0b\x08\x20\x10\x0b\x6e\x3d\x24\ +\x34\x06\x25\x02\x09\x0a\x27\x12\x1c\x0d\x1a\x1b\x29\x1e\x18\x09\ +\x03\x0e\x08\x0e\x11\x0c\x4b\x0e\x12\x2d\xec\x1c\x2b\x16\x0a\x1e\ +\x11\x16\x09\x02\x29\x02\x07\xfd\x40\x07\x0c\x13\x19\x10\xa7\x26\ +\x8b\xbe\x45\x62\x3a\x4d\x48\x1e\x1f\x1c\x08\x0e\x0f\x03\x22\x38\ +\x34\x41\x56\xa0\x8a\x3a\xc2\x29\x18\x05\x02\x07\xfe\x44\xfe\xea\ +\x15\x21\x16\x17\x1f\x0e\x01\x02\x06\x07\x14\x2c\x01\x0e\x05\x0d\ +\x02\x0d\x6c\x09\x16\x0e\x14\x0e\x20\x17\x16\x20\x03\x29\x22\x17\ +\xcd\x0b\x09\x37\xde\x01\x01\x0b\x09\x37\xde\xf1\x12\x3a\x37\x0c\ +\x21\x38\x27\x16\x1f\x1f\x1f\x09\x0c\x07\x03\x26\x1e\xb2\x12\x2b\ +\x29\x42\x72\x26\x1e\xb3\x11\x3a\x36\x0c\x21\x37\x27\x16\x0e\x01\ +\x08\x03\x04\x02\x48\x22\x22\x01\x19\x28\x3b\x37\x16\x4d\x0b\x08\ +\x05\x04\xcd\x0c\x08\x22\x11\x15\x1a\x0d\x9b\x93\x68\x4a\x66\x29\ +\x29\x10\x1e\x14\x18\x0b\x15\x03\x22\x1a\x61\x54\x49\x7f\x01\x13\ +\x25\x06\x09\x0f\xfd\x3f\x10\x15\x01\x07\x11\x0e\x01\xed\x08\x11\ +\x04\x05\x09\x19\x01\x0a\x01\x02\x0c\x10\x0a\x55\x08\x0f\x0d\xfd\ +\xb4\x0e\x11\x07\x01\x15\x10\x00\x04\x00\xc8\x01\xcc\x06\xbb\x05\ +\x55\x00\x29\x00\x52\x00\xa9\x00\xb9\x00\x00\x01\x21\x22\x26\x35\ +\x34\x33\x32\x3e\x02\x35\x11\x34\x27\x26\x0f\x01\x0e\x02\x27\x22\ +\x27\x34\x3f\x01\x36\x33\x32\x16\x15\x11\x14\x1e\x02\x33\x32\x15\ +\x14\x06\x01\x06\x2b\x01\x07\x32\x16\x15\x14\x06\x23\x22\x27\x2e\ +\x01\x35\x34\x33\x32\x1e\x01\x17\x1e\x01\x33\x32\x36\x35\x34\x26\ +\x23\x13\x33\x32\x36\x37\x36\x33\x32\x15\x14\x01\x22\x26\x3f\x01\ +\x36\x26\x23\x22\x0f\x01\x23\x13\x36\x26\x23\x22\x0f\x01\x23\x37\ +\x36\x23\x22\x06\x07\x27\x3e\x03\x33\x32\x17\x36\x33\x32\x17\x3e\ +\x03\x33\x32\x16\x0f\x01\x06\x33\x3e\x01\x37\x13\x36\x35\x34\x23\ +\x22\x06\x23\x22\x35\x34\x3e\x01\x37\x3e\x01\x33\x32\x16\x0f\x01\ +\x3e\x02\x33\x32\x15\x14\x0e\x02\x23\x22\x27\x06\x37\x07\x06\x1d\ +\x01\x14\x33\x32\x36\x37\x36\x35\x34\x26\x23\x22\x01\xf2\xfe\xea\ +\x09\x0b\x20\x17\x17\x1f\x0e\x08\x07\x14\x2c\x02\x08\x05\x04\x0e\ +\x02\x0d\x6c\x0b\x14\x0e\x15\x0e\x1f\x17\x17\x20\x0b\x01\xae\x1f\ +\x30\xa6\x27\x8b\xbe\x85\x5c\x49\x4c\x1e\x1e\x1b\x09\x0e\x0e\x03\ +\x23\x37\x34\x41\x57\xa1\x8a\x3b\xc1\x1a\x1b\x0d\x03\x04\x07\x01\ +\x9d\x1b\x12\x04\x2a\x02\x09\x0d\x32\x0a\x2e\x4a\x35\x02\x09\x0d\ +\x32\x0a\x2e\x4a\x31\x05\x0c\x09\x27\x14\x1b\x0d\x1a\x1b\x29\x1e\ +\x22\x03\x2b\x40\x20\x08\x0c\x10\x13\x20\x1c\x15\x1b\x06\x24\x02\ +\x09\x0a\x25\x12\x38\x01\x0a\x06\x12\x05\x06\x0f\x1e\x07\x09\x30\ +\x07\x08\x0a\x01\x2c\x0f\x0f\x1d\x14\x50\x11\x22\x42\x2b\x5c\x12\ +\x21\x84\x19\x04\x12\x17\x3a\x0d\x09\x11\x13\x2e\x01\xd2\x0a\x06\ +\x15\x01\x07\x12\x0e\x01\xec\x18\x06\x05\x0a\x19\x01\x06\x03\x01\ +\x0d\x10\x0a\x55\x09\x10\x0c\xfd\xb4\x0e\x12\x07\x01\x15\x06\x0a\ +\x02\xb7\x4e\x9b\x92\x68\x6d\x6d\x29\x10\x1e\x14\x19\x0c\x14\x03\ +\x23\x1a\x62\x54\x48\x7f\x01\x13\x12\x13\x07\x09\x10\xfe\xba\x22\ +\x17\xcd\x0b\x09\x37\xde\x01\x01\x0b\x09\x37\xde\xf1\x13\x3b\x37\ +\x0c\x22\x37\x28\x16\x20\x20\x20\x09\x0c\x08\x03\x27\x1e\xb2\x12\ +\x01\x37\x34\x01\x1a\x04\x05\x0f\x02\x07\x04\x07\x0a\x03\x04\x18\ +\x07\x06\xdb\x0a\x09\x07\x66\x1d\x44\x45\x2e\x3e\x3e\xe3\x7a\x14\ +\x0e\x0c\x1a\x3d\x3f\x32\x1d\x18\x15\x00\x00\x00\x01\x00\xbe\x03\ +\x1c\x04\x96\x04\xd8\x00\x0b\x00\x00\x13\x21\x15\x23\x11\x33\x15\ +\x21\x35\x33\x11\x23\xbe\x03\xd8\xfa\xfa\xfc\x28\xfa\xfa\x04\xd8\ +\x2e\xfe\xa0\x2e\x2e\x01\x60\x00\x01\x00\xbe\x03\xe0\x04\x96\x04\ +\xd8\x00\x07\x00\x00\x01\x35\x23\x35\x21\x15\x23\x15\x01\xb8\xfa\ +\x03\xd8\xfa\x03\xe0\xca\x2e\x2e\xca\x00\x00\x00\x01\x00\xbe\x03\ +\x1c\x04\x96\x04\x14\x00\x07\x00\x00\x01\x15\x33\x15\x21\x35\x33\ +\x35\x03\x9c\xfa\xfc\x28\xfa\x04\x14\xca\x2e\x2e\xca\x00\x00\x00\ +\x01\x00\xc0\x00\xf5\x02\x5f\x05\xca\x00\x31\x00\x00\x01\x22\x26\ +\x23\x22\x06\x15\x14\x1e\x02\x14\x06\x23\x22\x2e\x02\x35\x34\x36\ +\x33\x32\x17\x2e\x01\x35\x34\x3e\x01\x35\x34\x2e\x01\x35\x34\x36\ +\x33\x32\x1e\x01\x15\x14\x02\x15\x14\x16\x15\x14\x06\x02\x0b\x01\ +\x52\x26\x1d\x25\x2a\x34\x2a\x1f\x0e\x0c\x49\x55\x41\x45\x35\x44\ +\x30\x46\x62\x54\x54\x47\x47\x33\x0b\x0d\x7f\x75\xb4\x99\x28\x02\ +\x01\x14\x24\x26\x22\x42\x2a\x24\x0e\x16\x29\x40\x60\x2e\x51\x5d\ +\x1e\x50\xad\x24\x0d\x74\x85\x22\x19\x60\x51\x06\x0b\x2a\x99\xaf\ +\x1a\x1d\xfe\xe1\x17\x16\xd1\x06\x0f\x18\x00\x00\x01\x00\xc3\x01\ +\x90\x02\xb7\x04\x9d\x00\x15\x00\x00\x01\x17\x03\x27\x13\x06\x23\ +\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x07\x3e\x03\x02\x6f\x48\ +\xed\x47\xbd\x6e\x8c\x3e\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\ +\x04\x76\x21\xfd\x3b\x21\x02\x3a\x5f\x49\x3f\x37\x52\x52\x37\x0f\ +\x13\x04\x22\x31\x1f\x00\x00\x00\x01\x00\xc3\x01\x90\x03\x3c\x06\ +\x2b\x00\x2a\x00\x00\x01\x27\x13\x06\x23\x22\x26\x35\x34\x36\x33\ +\x32\x16\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\x26\x35\x34\x36\ +\x33\x32\x1e\x02\x15\x14\x07\x3e\x03\x37\x17\x01\xca\x47\xbd\x6e\ +\x8c\x3e\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\x0a\x56\x6e\x8d\ +\x3d\x45\x48\x3a\x21\x31\x1b\x0d\x02\x1d\x3e\x31\x1c\x0a\x48\x01\ +\x90\x21\x02\x3a\x5f\x49\x3f\x37\x52\x52\x37\x0f\x13\x04\x22\x31\ +\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x19\x29\x2e\x19\x0f\x13\x04\ +\x22\x31\x1f\x0e\x21\x00\x00\x00\x01\x00\xc3\x00\x02\x03\xc0\x06\ +\x2b\x00\x3d\x00\x00\x09\x01\x27\x13\x06\x23\x22\x26\x35\x34\x36\ +\x33\x32\x16\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\x26\x35\x34\ +\x36\x33\x32\x1e\x02\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\x26\ +\x35\x34\x36\x33\x32\x16\x15\x14\x07\x3e\x03\x37\x03\xc0\xfe\x0a\ +\x47\xbd\x6e\x8c\x3e\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\x0a\ +\x56\x6e\x8d\x3d\x45\x48\x3a\x21\x31\x1b\x0d\x02\x1d\x3e\x31\x1c\ +\x0a\x55\x6e\x8c\x3e\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\x0a\ +\x05\xe3\xfa\x1f\x21\x02\x3a\x5f\x49\x3f\x37\x52\x52\x37\x0f\x13\ +\x04\x22\x31\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x19\x29\x2e\x19\ +\x0f\x13\x04\x22\x31\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x52\x37\ +\x0f\x13\x04\x22\x31\x1f\x0e\x00\x01\x00\xc3\x00\x02\x04\x45\x07\ +\xb9\x00\x52\x00\x00\x09\x01\x27\x13\x06\x23\x22\x26\x35\x34\x36\ +\x33\x32\x16\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\x26\x35\x34\ +\x36\x33\x32\x1e\x02\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\x26\ +\x35\x34\x36\x33\x32\x16\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\ +\x26\x35\x34\x36\x33\x32\x1e\x02\x15\x14\x07\x3e\x03\x37\x04\x44\ +\xfd\x86\x47\xbd\x6e\x8c\x3e\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\ +\x1b\x0a\x56\x6e\x8d\x3d\x45\x48\x3a\x21\x31\x1b\x0d\x02\x1d\x3e\ +\x31\x1c\x0a\x55\x6e\x8c\x3e\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\ +\x1b\x0a\x56\x6e\x8c\x3e\x46\x49\x3b\x20\x31\x1b\x0e\x04\x1e\x3e\ +\x31\x1c\x0a\x07\x71\xf8\x91\x21\x02\x3a\x5f\x49\x3f\x37\x52\x52\ +\x37\x0f\x13\x04\x22\x31\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x19\ +\x29\x2e\x19\x0f\x13\x04\x22\x31\x1f\x0e\x01\x03\x5f\x49\x3f\x37\ +\x52\x52\x37\x0f\x13\x04\x22\x31\x1f\x0e\x01\x03\x5f\x49\x3f\x37\ +\x52\x19\x29\x2e\x19\x0f\x13\x04\x22\x31\x1f\x0e\x00\x00\x00\x00\ +\x01\x00\xc3\xfe\x74\x04\xc9\x07\xb9\x00\x65\x00\x00\x09\x01\x27\ +\x13\x06\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\x07\x3e\x03\ +\x37\x13\x06\x23\x22\x26\x35\x34\x36\x33\x32\x1e\x02\x15\x14\x07\ +\x3e\x03\x37\x13\x06\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\ +\x07\x3e\x03\x37\x13\x06\x23\x22\x26\x35\x34\x36\x33\x32\x1e\x02\ +\x15\x14\x07\x3e\x03\x37\x13\x06\x23\x22\x26\x35\x34\x36\x33\x32\ +\x16\x15\x14\x07\x3e\x03\x37\x04\xc9\xfd\x01\x47\xbd\x6e\x8c\x3e\ +\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\x0a\x56\x6e\x8d\x3d\x45\ +\x48\x3a\x21\x31\x1b\x0d\x02\x1d\x3e\x31\x1c\x0a\x55\x6e\x8c\x3e\ +\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\x0a\x56\x6e\x8c\x3e\x46\ +\x49\x3b\x20\x31\x1b\x0e\x04\x1e\x3e\x31\x1c\x0a\x55\x6e\x8c\x3e\ +\x45\x49\x3a\x3f\x3b\x03\x1d\x3e\x32\x1b\x0b\x07\x71\xf7\x03\x21\ +\x02\x3a\x5f\x49\x3f\x37\x52\x52\x37\x0f\x13\x04\x22\x31\x1f\x0e\ +\x01\x03\x5f\x49\x3f\x37\x52\x19\x29\x2e\x19\x0f\x13\x04\x22\x31\ +\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x52\x37\x0f\x13\x04\x22\x31\ +\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x19\x29\x2e\x19\x0f\x13\x04\ +\x22\x31\x1f\x0e\x01\x03\x5f\x49\x3f\x37\x52\x52\x37\x0f\x13\x04\ +\x22\x31\x1f\x0e\x00\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\ +\xbc\x00\x0b\x00\x00\x13\x37\x17\x37\x17\x07\x17\x07\x27\x07\x27\ +\x37\xc9\x2f\xaf\xaf\x2f\xaf\xaf\x2f\xaf\xaf\x2f\xaf\x01\x8d\x2f\ +\xaf\xaf\x2f\xaf\xaf\x2f\xaf\xaf\x2f\xaf\x00\x00\x01\x00\xc9\x00\ +\x00\x02\x85\x01\xbc\x00\x0b\x00\x00\x25\x23\x35\x33\x35\x33\x15\ +\x33\x15\x23\x15\x23\x01\x84\xbb\xbb\x46\xbb\xbb\x46\xbb\x46\xbb\ +\xbb\x46\xbb\x00\x05\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x07\x00\ +\x0c\x00\x11\x00\x16\x00\x1b\x00\x00\x36\x34\x36\x32\x16\x14\x06\ +\x22\x37\x36\x34\x27\x07\x27\x17\x37\x26\x22\x07\x06\x14\x17\x37\ +\x17\x27\x07\x16\x32\xc9\x82\xb8\x82\x82\xb8\xd9\x1b\x1b\x56\x7d\ +\x56\x56\x27\x5e\x4e\x1b\x1b\x56\x7d\x56\x56\x27\x5e\x82\xb8\x82\ +\x82\xb8\x82\x88\x27\x5e\x27\x56\x7d\x56\x56\x1b\x42\x27\x5e\x27\ +\x56\x7d\x56\x56\x1b\x00\x00\x00\x02\x00\xc9\x00\x00\x02\x85\x01\ +\xbc\x00\x03\x00\x07\x00\x00\x25\x21\x11\x21\x03\x11\x21\x11\x01\ +\x0f\x01\x30\xfe\xd0\x46\x01\xbc\x46\x01\x30\xfe\x8a\x01\xbc\xfe\ +\x44\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x03\x00\ +\x00\x33\x11\x21\x11\xc9\x01\xbc\x01\xbc\xfe\x44\x00\x00\x00\x00\ +\x02\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\x05\x00\x00\x25\ +\x33\x27\x35\x13\x21\x01\x3c\xd6\x6b\xde\xfe\x44\x46\xd6\xa0\xfe\ +\x44\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\ +\x00\x01\x13\x21\x01\xa7\xde\xfe\x44\x01\xbc\xfe\x44\x00\x00\x00\ +\x02\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\x05\x00\x00\x01\ +\x15\x33\x17\x21\x11\x01\x0f\xcc\xaa\xfe\x44\x01\x12\xcc\x46\x01\ +\xbc\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\ +\x00\x29\x01\x11\x02\x85\xfe\x44\x01\xbc\x00\x00\x02\x00\xc9\x00\ +\x00\x02\x85\x01\xbc\x00\x02\x00\x05\x00\x00\x01\x07\x33\x05\x01\ +\x11\x02\x3f\xcc\xcc\xfe\x8a\x01\xbc\x01\x12\xcc\x46\x01\xbc\xfe\ +\x44\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\ +\x00\x01\x11\x21\x02\x85\xfe\x44\x01\xbc\xfe\x44\x00\x00\x00\x00\ +\x02\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\x05\x00\x00\x01\ +\x17\x37\x0b\x01\x21\x01\x3c\x6b\x6b\x6b\xde\x01\xbc\x01\x76\xd6\ +\xd6\xfe\x8a\x01\xbc\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\ +\xbc\x00\x02\x00\x00\x21\x03\x21\x01\xa7\xde\x01\xbc\x01\xbc\x00\ +\x02\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\x05\x00\x00\x25\ +\x35\x23\x27\x21\x11\x02\x3f\xcc\xaa\x01\xbc\xaa\xcc\x46\xfe\x44\ +\x00\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x02\x00\ +\x00\x21\x01\x21\x02\x85\xfe\x44\x01\xbc\x01\xbc\x00\x00\x00\x00\ +\x02\x00\xc9\x00\x00\x02\xd5\x01\xbc\x00\x07\x00\x0f\x00\x00\x25\ +\x14\x16\x32\x36\x3d\x01\x21\x07\x35\x21\x15\x14\x06\x22\x26\x01\ +\x0f\x6d\xa6\x6d\xfe\x80\x46\x02\x0c\x93\xe6\x93\xf7\x54\x5d\x5d\ +\x54\x7f\x81\xc7\xc7\x75\x80\x80\x00\x00\x00\x00\x01\x00\xc9\x00\ +\x00\x02\xd5\x01\xbc\x00\x07\x00\x00\x37\x35\x21\x15\x14\x06\x22\ +\x26\xc9\x02\x0c\x93\xe6\x93\xf5\xc7\xc7\x75\x80\x80\x00\x00\x00\ +\x02\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x04\x00\x0b\x00\x00\x00\ +\x22\x07\x17\x37\x26\x32\x16\x17\x0b\x01\x36\x02\x05\xbc\x1f\x7d\ +\x7d\xcb\x9c\x76\x1a\xde\xde\x1a\x01\x76\x43\xb6\xb6\x89\x3c\x3c\ +\xfe\xbc\x01\x44\x3c\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x01\ +\xbc\x00\x06\x00\x00\x00\x32\x16\x17\x0b\x01\x36\x01\x59\x9c\x76\ +\x1a\xde\xde\x1a\x01\xbc\x3c\x3c\xfe\xbc\x01\x44\x3c\x00\x00\x00\ +\x02\x00\xc9\xff\xec\x02\x85\x01\xd0\x00\x0d\x00\x1b\x00\x00\x00\ +\x14\x0e\x01\x07\x27\x3e\x01\x34\x26\x27\x37\x1e\x01\x04\x34\x3e\ +\x01\x37\x17\x0e\x01\x14\x16\x17\x07\x2e\x01\x02\x85\x25\x30\x2d\ +\x19\x34\x25\x25\x34\x19\x2d\x30\xfe\x69\x25\x30\x2d\x19\x34\x25\ +\x25\x34\x19\x2d\x30\x01\x11\x66\x58\x3b\x2c\x15\x3b\x5c\x8c\x5c\ +\x3b\x15\x2c\x3b\xbe\x66\x58\x3b\x2c\x15\x3b\x5c\x8c\x5c\x3b\x15\ +\x2c\x3b\x00\x00\x02\x00\xc9\x00\x00\x03\x11\x01\xbc\x00\x09\x00\ +\x17\x00\x00\x01\x32\x15\x14\x06\x23\x22\x35\x34\x36\x03\x32\x3e\ +\x02\x34\x26\x23\x22\x0e\x02\x14\x16\x02\x30\xe1\xdb\x8c\xe1\xda\ +\x49\x3d\x88\x61\x41\x29\x18\x3d\x88\x61\x41\x29\x01\xbc\x95\x7d\ +\xaa\x95\x7d\xaa\xfe\x87\x43\x5a\x57\x2a\x18\x43\x5a\x57\x2a\x18\ +\x00\x00\x00\x00\x01\x00\xc9\x00\x00\x03\x11\x01\xbc\x00\x09\x00\ +\x00\x01\x32\x15\x14\x06\x23\x22\x35\x34\x36\x02\x30\xe1\xdb\x8c\ +\xe1\xda\x01\xbc\x95\x7d\xaa\x95\x7d\xaa\x00\x00\x02\x00\xc9\x00\ +\x00\x02\x85\x03\x4a\x00\x03\x00\x07\x00\x00\x25\x21\x11\x21\x03\ +\x11\x21\x11\x01\x0f\x01\x30\xfe\xd0\x46\x01\xbc\x46\x02\xbe\xfc\ +\xfc\x03\x4a\xfc\xb6\x00\x00\x00\x01\x00\xc9\x00\x00\x02\x85\x03\ +\x4a\x00\x03\x00\x00\x33\x11\x21\x11\xc9\x01\xbc\x03\x4a\xfc\xb6\ +\x00\x00\x00\x00\x04\x00\xc8\xff\xd8\x04\x8c\x01\xe4\x00\x0b\x00\ +\x0f\x00\x13\x00\x2f\x00\x00\x01\x22\x06\x15\x14\x16\x33\x32\x36\ +\x35\x34\x26\x25\x11\x23\x11\x21\x11\x23\x11\x23\x11\x23\x35\x0e\ +\x02\x23\x22\x2e\x02\x27\x15\x23\x11\x33\x15\x3e\x02\x33\x32\x1e\ +\x02\x17\x35\x02\x96\x2f\x44\x50\x4b\x2f\x44\x4d\xfe\x14\x30\x03\ +\xc4\x30\x34\x30\x0b\x59\x73\x39\x57\x83\x6b\x3f\x08\x30\x30\x0b\ +\x59\x73\x39\x57\x83\x6b\x3f\x08\x01\x76\x3f\x33\x53\x6b\x3f\x33\ +\x55\x69\x6e\xfd\xf4\x02\x0c\xfd\xf4\x02\x0c\xfd\xf4\xd1\x33\x4f\ +\x27\x13\x2c\x50\x3a\xf1\x02\x0c\xd1\x33\x4f\x27\x13\x2c\x50\x3a\ +\xf1\x00\x00\x00\x02\x00\xc9\x00\x00\x03\x6b\x01\xbc\x00\x0b\x00\ +\x1d\x00\x00\x01\x22\x06\x15\x14\x16\x33\x32\x36\x35\x34\x26\x27\ +\x32\x1e\x02\x15\x14\x0e\x01\x23\x22\x2e\x02\x35\x34\x3e\x01\x02\ +\x06\x2f\x44\x50\x4b\x2f\x44\x4d\x78\x5f\x8b\x6e\x37\x59\x7c\x3e\ +\x5f\x8b\x6e\x37\x59\x7c\x01\x76\x3f\x33\x53\x6b\x3f\x33\x55\x69\ +\x46\x16\x36\x62\x47\x3b\x5e\x2e\x16\x36\x62\x47\x3b\x5e\x2e\x00\ +\x02\x00\xc9\x00\x00\x03\x11\x06\x66\x00\x0d\x00\x1a\x00\x00\x25\ +\x32\x3e\x02\x34\x26\x23\x22\x0e\x02\x14\x16\x01\x33\x11\x14\x06\ +\x23\x22\x35\x34\x36\x33\x32\x17\x01\x5a\x3d\x88\x61\x41\x29\x18\ +\x3d\x88\x61\x41\x29\x01\x89\x46\xdb\x8c\xe1\xda\x8d\x63\x38\x43\ +\x43\x5a\x57\x2a\x18\x43\x5a\x57\x2a\x18\x06\x23\xfa\xc1\x7d\xaa\ +\x95\x7d\xaa\x1e\x00\x00\x00\x00\x01\x00\xc9\x00\x00\x03\x11\x06\ +\x66\x00\x0c\x00\x00\x01\x33\x11\x14\x06\x23\x22\x35\x34\x36\x33\ +\x32\x17\x02\xcb\x46\xdb\x8c\xe1\xda\x8d\x63\x38\x06\x66\xfa\xc1\ +\x7d\xaa\x95\x7d\xaa\x1e\x00\x00\x01\x00\xc9\x00\x00\x04\xdc\x06\ +\x66\x00\x28\x00\x00\x01\x33\x1e\x06\x15\x14\x06\x07\x06\x26\x37\ +\x3e\x04\x35\x34\x2e\x05\x27\x11\x14\x06\x23\x22\x35\x34\x36\x33\ +\x32\x17\x02\xcb\x46\x0f\x43\x52\x5e\x57\x47\x2b\x51\x67\x0c\x37\ +\x0b\x20\x35\x2b\x1e\x10\x26\x3f\x4c\x51\x46\x37\x0a\xdb\x8c\xe1\ +\xda\x8d\x63\x38\x06\x66\x4a\x8b\x6c\x69\x63\x66\x79\x41\x81\xad\ +\x79\x0e\x1b\x0e\x2a\x45\x45\x44\x53\x30\x37\x5c\x43\x3c\x3a\x3d\ +\x52\x30\xfb\xf4\x7d\xaa\x95\x7d\xaa\x1e\x00\x00\x02\x00\xc9\x00\ +\x00\x04\xdc\x06\x66\x00\x29\x00\x38\x00\x00\x01\x11\x14\x06\x23\ +\x22\x35\x34\x36\x33\x32\x17\x11\x33\x1e\x06\x15\x14\x07\x16\x15\ +\x14\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x03\x03\x1e\x04\x17\ +\x36\x34\x2e\x04\x27\x03\x11\xdb\x8c\xe1\xda\x8d\x63\x38\x46\x0c\ +\x40\x52\x5e\x5a\x49\x2c\x21\x21\x4d\x6b\x0f\x37\x0e\x31\x38\x31\ +\x14\x49\x6b\x6f\x5a\x0c\x0c\x40\x57\x59\x63\x1f\x0b\x34\x50\x61\ +\x56\x44\x0a\x04\x14\xfd\x13\x7d\xaa\x95\x7d\xaa\x1e\x04\xc8\x36\ +\x61\x4c\x4b\x4e\x55\x72\x41\x74\x46\x3f\x4f\x87\x93\x4e\x0b\x1b\ +\x0b\x27\x35\x4a\x59\x3d\x48\x5f\x32\x2d\x4c\x01\x11\x36\x5c\x47\ +\x3c\x4a\x24\x2c\x7a\x5e\x39\x3a\x30\x4e\x30\x00\x03\x00\xc9\x00\ +\x00\x04\xdd\x07\x4c\x00\x0f\x00\x1f\x00\x4f\x00\x00\x01\x1e\x03\ +\x17\x36\x35\x34\x2e\x05\x27\x01\x36\x35\x34\x2e\x04\x27\x15\x1e\ +\x04\x17\x16\x15\x14\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x05\ +\x27\x11\x14\x06\x23\x22\x35\x34\x36\x33\x32\x17\x11\x33\x1e\x06\ +\x15\x14\x07\x16\x07\x06\x03\x11\x0f\x61\x6a\x85\x25\x05\x27\x3f\ +\x4e\x51\x46\x35\x09\x01\x82\x07\x34\x50\x61\x56\x44\x0a\x0c\x41\ +\x58\x5b\x63\x4c\x1c\x4d\x6b\x0f\x37\x0e\x33\x35\x33\x13\x27\x3f\ +\x4e\x51\x46\x35\x09\xdb\x8c\xe1\xda\x8d\x63\x38\x46\x0c\x40\x52\ +\x5e\x5a\x49\x2c\x19\x1a\x01\x03\x06\x1c\x43\x7b\x50\x6f\x30\x23\ +\x26\x35\x54\x39\x30\x2d\x30\x44\x2a\xfc\xc6\x22\x33\x3c\x5d\x37\ +\x38\x2f\x4d\x30\x58\x37\x63\x4f\x48\x57\x73\x42\x47\x68\x80\x4e\ +\x0b\x1b\x0b\x28\x2f\x41\x44\x2e\x35\x54\x39\x30\x2d\x30\x44\x2a\ +\xfd\x13\x7d\xaa\x95\x7d\xaa\x1e\x05\xae\x36\x61\x4c\x4b\x4e\x55\ +\x71\x41\x66\x42\x3e\x43\x6e\x00\x04\x00\xc9\x00\x00\x04\xdd\x08\ +\x7d\x00\x33\x00\x43\x00\x53\x00\x64\x00\x00\x01\x16\x15\x14\x06\ +\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x05\x27\x11\x14\x06\x23\x22\ +\x35\x34\x36\x33\x32\x17\x11\x37\x1e\x06\x15\x14\x07\x16\x15\x14\ +\x07\x16\x07\x06\x27\x36\x35\x34\x2e\x04\x27\x15\x1e\x04\x01\x1e\ +\x03\x17\x36\x35\x34\x2e\x05\x27\x11\x15\x1e\x04\x17\x36\x35\x34\ +\x2e\x05\x04\xc0\x1c\x4d\x6b\x0f\x37\x0e\x33\x35\x33\x13\x27\x3f\ +\x4e\x51\x46\x35\x09\xdb\x8c\xe1\xda\x8d\x63\x38\x46\x0c\x40\x52\ +\x5e\x5a\x49\x2c\x1b\x1b\x19\x1a\x01\x03\x46\x07\x34\x50\x61\x56\ +\x44\x0a\x0c\x41\x58\x5b\x63\xfe\x9d\x0f\x61\x6a\x85\x25\x05\x27\ +\x3f\x4e\x51\x46\x35\x09\x0c\x41\x59\x5b\x63\x1f\x06\x27\x3f\x4e\ +\x51\x46\x35\x02\xf1\x42\x47\x68\x80\x4e\x0b\x1b\x0b\x28\x2f\x41\ +\x44\x2e\x35\x54\x39\x30\x2d\x30\x44\x2a\xfd\x13\x7d\xaa\x95\x7d\ +\xaa\x1e\x06\xde\x01\x36\x62\x4b\x4c\x4e\x55\x71\x41\x67\x44\x3e\ +\x47\x66\x42\x3e\x43\x6e\x09\x22\x33\x3c\x5d\x37\x38\x2f\x4d\x30\ +\x58\x37\x63\x4f\x48\x57\x02\xb8\x43\x7b\x50\x6f\x30\x23\x26\x35\ +\x54\x39\x30\x2d\x30\x44\x2a\x01\x30\x59\x37\x63\x50\x47\x58\x29\ +\x21\x2d\x35\x54\x39\x30\x2d\x30\x44\x00\x00\x00\x05\x00\xc9\x00\ +\x00\x04\xdd\x09\xad\x00\x10\x00\x20\x00\x30\x00\x68\x00\x79\x00\ +\x00\x01\x15\x1e\x04\x17\x36\x35\x34\x2e\x05\x03\x1e\x03\x17\x36\ +\x35\x34\x2e\x05\x27\x01\x36\x35\x34\x2e\x04\x27\x15\x1e\x04\x17\ +\x16\x15\x14\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x05\x27\x11\ +\x14\x06\x23\x22\x35\x34\x36\x33\x32\x17\x11\x33\x1e\x06\x15\x14\ +\x07\x16\x15\x14\x07\x16\x15\x14\x07\x16\x07\x06\x01\x15\x1e\x04\ +\x17\x36\x35\x34\x2e\x05\x03\x11\x0c\x41\x59\x5b\x63\x1f\x06\x27\ +\x3f\x4e\x51\x46\x35\x09\x0f\x61\x6a\x85\x25\x05\x27\x3f\x4e\x51\ +\x46\x35\x09\x01\x82\x07\x34\x50\x61\x56\x44\x0a\x0c\x41\x58\x5b\ +\x63\x4c\x1c\x4d\x6b\x0f\x37\x0e\x33\x35\x33\x13\x27\x3f\x4e\x51\ +\x46\x35\x09\xdb\x8c\xe1\xda\x8d\x63\x38\x46\x0c\x40\x52\x5e\x5a\ +\x49\x2c\x1b\x1b\x1b\x1b\x19\x1a\x01\x03\xfe\x38\x0c\x41\x59\x5b\ +\x63\x1f\x06\x27\x3f\x4e\x51\x46\x35\x07\xa5\x59\x37\x63\x50\x47\ +\x58\x29\x21\x2d\x35\x54\x39\x30\x2d\x30\x44\xfe\xa1\x43\x7b\x50\ +\x6f\x30\x23\x26\x35\x54\x39\x30\x2d\x30\x44\x2a\xfc\xc6\x22\x33\ +\x3c\x5d\x37\x38\x2f\x4d\x30\x58\x37\x63\x4f\x48\x57\x73\x42\x47\ +\x68\x80\x4e\x0b\x1b\x0b\x28\x2f\x41\x44\x2e\x35\x54\x39\x30\x2d\ +\x30\x44\x2a\xfd\x13\x7d\xaa\x95\x7d\xaa\x1e\x08\x0f\x36\x61\x4c\ +\x4b\x4e\x55\x71\x41\x68\x44\x3e\x47\x67\x44\x3e\x47\x66\x42\x3e\ +\x43\x6e\x05\xa4\x59\x37\x63\x50\x48\x58\x29\x22\x2d\x35\x54\x39\ +\x30\x2d\x30\x44\x00\x00\x00\x00\x01\xfe\xf2\x01\x27\xff\x38\x06\ +\x66\x00\x03\x00\x00\x01\x11\x33\x11\xfe\xf2\x46\x01\x27\x05\x3f\ +\xfa\xc1\x00\x00\x01\xfe\x37\x01\x27\xff\xf3\x06\x66\x00\x11\x00\ +\x00\x01\x37\x17\x11\x33\x11\x37\x17\x07\x17\x07\x27\x11\x23\x11\ +\x07\x27\x37\xfe\x37\x2f\x8c\x46\x8c\x2f\xaf\xaf\x2f\x8c\x46\x8c\ +\x2f\xaf\x04\xa9\x2f\x8c\x02\x1a\xfd\xe6\x8c\x2f\xaf\xaf\x2f\x8c\ +\xfd\x7f\x02\x81\x8c\x2f\xaf\x00\x01\xfd\xdf\x03\x93\x00\x4b\x05\ +\x0b\x00\x03\x00\x00\x01\x25\x15\x05\xfd\xdf\x02\x6c\xfd\x94\x04\ +\x22\xe9\x95\xe3\x00\x00\x00\x00\x02\xfd\xdf\x03\x16\x00\x4b\x05\ +\x88\x00\x03\x00\x07\x00\x00\x01\x25\x15\x05\x11\x25\x15\x05\xfd\ +\xdf\x02\x6c\xfd\x94\x02\x6c\xfd\x94\x03\xa5\xe9\x95\xe3\x01\x89\ +\xe9\x95\xe3\x00\x03\xfd\xdf\x02\x99\x00\x4b\x06\x05\x00\x03\x00\ +\x07\x00\x0b\x00\x00\x01\x25\x15\x05\x11\x25\x15\x05\x11\x25\x15\ +\x05\xfd\xdf\x02\x6c\xfd\x94\x02\x6c\xfd\x94\x02\x6c\xfd\x94\x05\ +\x1c\xe9\x95\xe3\xfe\x9b\xe9\x95\xe3\x01\x89\xe9\x95\xe3\x00\x00\ +\x01\x00\xaf\x03\x93\x04\xc9\x05\x33\x00\x03\x00\x00\x13\x25\x15\ +\x05\xaf\x04\x1a\xfb\xe6\x04\x4a\xe9\xbd\xe3\x00\x02\x00\xaf\x02\ +\xfd\x04\xc9\x05\xc9\x00\x03\x00\x07\x00\x00\x13\x25\x15\x05\x11\ +\x25\x15\x05\xaf\x04\x1a\xfb\xe6\x04\x1a\xfb\xe6\x03\xb4\xe9\xbd\ +\xe3\x01\xe3\xe9\xbd\xe3\x00\x00\x03\x00\xaf\x02\x67\x04\xc9\x06\ +\x5f\x00\x03\x00\x07\x00\x0b\x00\x00\x13\x25\x15\x05\x11\x25\x15\ +\x05\x11\x25\x15\x05\xaf\x04\x1a\xfb\xe6\x04\x1a\xfb\xe6\x04\x1a\ +\xfb\xe6\x05\x76\xe9\xbd\xe3\xfe\x5f\xe9\xbd\xe3\x01\xe3\xe9\xbd\ +\xe3\x00\x00\x00\x01\xff\xd8\x00\x7a\x00\xa0\x01\x42\x00\x07\x00\ +\x00\x26\x34\x36\x32\x16\x14\x06\x22\x28\x3b\x52\x3b\x3b\x52\xb5\ +\x52\x3b\x3b\x52\x3b\x00\x00\x00\x01\xff\x38\x01\x8b\x01\x03\x06\ +\x66\x00\x1c\x00\x00\x03\x1e\x06\x15\x14\x06\x07\x06\x26\x37\x3e\ +\x04\x35\x34\x2e\x05\x27\xc8\x0f\x43\x52\x5e\x57\x47\x2b\x51\x67\ +\x0c\x37\x0b\x20\x35\x2b\x1e\x10\x26\x3f\x4c\x51\x46\x37\x0a\x06\ +\x66\x4a\x8b\x6c\x69\x63\x66\x79\x41\x81\xad\x79\x0e\x1b\x0e\x2a\ +\x45\x45\x44\x53\x30\x37\x5c\x43\x3c\x3a\x3d\x52\x30\x00\x00\x00\ +\x01\xff\x38\x01\x2d\x01\x03\x06\x66\x00\x2c\x00\x00\x03\x1e\x06\ +\x15\x14\x07\x16\x15\x14\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\ +\x03\x27\x35\x1e\x04\x17\x36\x34\x2e\x04\x27\xc8\x0c\x40\x52\x5e\ +\x5a\x49\x2c\x21\x21\x4d\x6b\x0f\x37\x0e\x31\x38\x31\x14\x49\x6b\ +\x6f\x5a\x0c\x0c\x40\x57\x59\x63\x1f\x0b\x34\x50\x61\x56\x44\x0a\ +\x06\x66\x36\x61\x4c\x4b\x4e\x55\x72\x41\x74\x46\x3f\x4f\x87\x93\ +\x4e\x0b\x1b\x0b\x27\x35\x4a\x59\x3d\x48\x5f\x32\x2d\x4c\x39\xd8\ +\x36\x5c\x47\x3c\x4a\x24\x2c\x7a\x5e\x39\x3a\x30\x4e\x30\x00\x00\ +\x01\xfe\xf2\x01\x2d\x01\x04\x07\x4c\x00\x46\x00\x00\x01\x35\x33\ +\x1e\x06\x15\x14\x07\x16\x07\x06\x07\x16\x15\x14\x06\x07\x06\x26\ +\x37\x3e\x03\x35\x34\x2e\x05\x27\x35\x1e\x04\x17\x36\x35\x34\x2e\ +\x04\x27\x35\x1e\x03\x17\x36\x35\x34\x2e\x05\x27\x15\xfe\xf2\x46\ +\x0c\x40\x52\x5e\x5a\x49\x2c\x19\x1a\x01\x03\x19\x1c\x4d\x6b\x0f\ +\x37\x0e\x34\x33\x34\x13\x27\x3f\x4e\x51\x46\x35\x09\x0c\x41\x58\ +\x5b\x63\x1f\x07\x34\x50\x61\x56\x44\x0a\x0f\x61\x6a\x85\x25\x05\ +\x27\x3f\x4e\x51\x46\x35\x09\x06\x52\xfa\x36\x61\x4c\x4b\x4e\x55\ +\x71\x41\x66\x42\x3e\x43\x6e\x41\x42\x47\x68\x80\x4e\x0b\x1b\x0b\ +\x29\x2e\x41\x44\x2e\x35\x54\x39\x30\x2d\x30\x44\x2a\xd8\x37\x63\ +\x4f\x48\x57\x29\x22\x33\x3c\x5d\x37\x38\x2f\x4d\x30\xd8\x43\x7b\ +\x50\x6f\x30\x23\x26\x35\x54\x39\x30\x2d\x30\x44\x2a\x23\x00\x00\ +\x02\xfe\xf2\x01\x2d\x01\x04\x08\x7d\x00\x4a\x00\x5b\x00\x00\x01\ +\x11\x33\x1e\x06\x15\x14\x07\x16\x15\x14\x07\x16\x07\x06\x07\x16\ +\x15\x14\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x05\x27\x35\x1e\ +\x04\x17\x36\x35\x34\x2e\x04\x27\x35\x1e\x03\x17\x36\x35\x34\x2e\ +\x05\x27\x15\x11\x15\x1e\x04\x17\x36\x35\x34\x2e\x05\xfe\xf2\x46\ +\x0c\x40\x52\x5e\x5a\x49\x2c\x1b\x1b\x19\x1a\x01\x03\x19\x1c\x4d\ +\x6b\x0f\x37\x0e\x34\x33\x34\x13\x27\x3f\x4e\x51\x46\x35\x09\x0c\ +\x41\x58\x5b\x63\x1f\x07\x34\x50\x61\x56\x44\x0a\x0f\x61\x6a\x85\ +\x25\x05\x27\x3f\x4e\x51\x46\x35\x09\x0c\x41\x59\x5b\x63\x1f\x06\ +\x27\x3f\x4e\x51\x46\x35\x06\x52\x02\x2b\x36\x62\x4b\x4c\x4e\x55\ +\x71\x41\x67\x44\x3e\x47\x66\x42\x3e\x43\x6e\x41\x42\x47\x68\x80\ +\x4e\x0b\x1b\x0b\x29\x2e\x41\x44\x2e\x35\x54\x39\x30\x2d\x30\x44\ +\x2a\xd8\x37\x63\x4f\x48\x57\x29\x22\x33\x3c\x5d\x37\x38\x2f\x4d\ +\x30\xd8\x43\x7b\x50\x6f\x30\x23\x26\x35\x54\x39\x30\x2d\x30\x44\ +\x2a\x23\x01\x53\x59\x37\x63\x50\x47\x58\x29\x21\x2d\x35\x54\x39\ +\x30\x2d\x30\x44\x00\x00\x00\x00\x03\xfe\xf2\x01\x2d\x01\x04\x09\ +\xad\x00\x4e\x00\x5f\x00\x70\x00\x00\x01\x11\x33\x1e\x06\x15\x14\ +\x07\x16\x15\x14\x07\x16\x15\x14\x07\x16\x07\x06\x07\x16\x15\x14\ +\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x05\x27\x35\x1e\x04\x17\ +\x36\x35\x34\x2e\x04\x27\x35\x1e\x03\x17\x36\x35\x34\x2e\x05\x27\ +\x15\x11\x15\x1e\x04\x17\x36\x35\x34\x2e\x05\x03\x15\x1e\x04\x17\ +\x36\x35\x34\x2e\x05\xfe\xf2\x46\x0c\x40\x52\x5e\x5a\x49\x2c\x1b\ +\x1b\x1b\x1b\x19\x1a\x01\x03\x19\x1c\x4d\x6b\x0f\x37\x0e\x34\x33\ +\x34\x13\x27\x3f\x4e\x51\x46\x35\x09\x0c\x41\x58\x5b\x63\x1f\x07\ +\x34\x50\x61\x56\x44\x0a\x0f\x61\x6a\x85\x25\x05\x27\x3f\x4e\x51\ +\x46\x35\x09\x0c\x41\x59\x5b\x63\x1f\x06\x27\x3f\x4e\x51\x46\x35\ +\x09\x0c\x41\x59\x5b\x63\x1f\x06\x27\x3f\x4e\x51\x46\x35\x06\x52\ +\x03\x5b\x36\x61\x4c\x4b\x4e\x55\x71\x41\x68\x44\x3e\x47\x67\x44\ +\x3e\x47\x66\x42\x3e\x43\x6e\x41\x42\x47\x68\x80\x4e\x0b\x1b\x0b\ +\x29\x2e\x41\x44\x2e\x35\x54\x39\x30\x2d\x30\x44\x2a\xd8\x37\x63\ +\x4f\x48\x57\x29\x22\x33\x3c\x5d\x37\x38\x2f\x4d\x30\xd8\x43\x7b\ +\x50\x6f\x30\x23\x26\x35\x54\x39\x30\x2d\x30\x44\x2a\x23\x02\x84\ +\x59\x37\x63\x50\x48\x58\x29\x22\x2d\x35\x54\x39\x30\x2d\x30\x44\ +\xfe\xf9\x59\x37\x63\x50\x47\x58\x29\x21\x2d\x35\x54\x39\x30\x2d\ +\x30\x44\x00\x00\x01\xfc\xbf\xfd\xb5\xff\x8f\xff\x85\x00\x05\x00\ +\x00\x0d\x02\x35\x2d\x01\xfc\xbf\x02\xd0\xfd\x30\x01\xd3\xfe\x2d\ +\x7b\xe8\xe8\x4d\x9b\x95\x00\x00\x01\xfd\xc4\xfe\xb1\xfe\x64\xff\ +\x51\x00\x07\x00\x00\x00\x34\x36\x32\x16\x14\x06\x22\xfd\xc4\x2f\ +\x42\x2f\x2f\x42\xfe\xe0\x42\x2f\x2f\x42\x2f\x00\x01\xfc\xf0\xfe\ +\xca\xff\x38\xff\x10\x00\x03\x00\x00\x05\x21\x15\x21\xfc\xf0\x02\ +\x48\xfd\xb8\xf0\x46\x00\x00\x00\x01\xfd\x9c\xfe\x11\xfe\x8c\xff\ +\xa6\x00\x02\x00\x00\x01\x1b\x01\xfd\x9c\x78\x78\xfe\x11\x01\x95\ +\xfe\x6b\x00\x00\x01\xfd\x2c\xfc\xcf\xfe\xfc\xff\x9f\x00\x05\x00\ +\x00\x05\x0b\x01\x33\x1b\x01\xfe\xfc\xe8\xe8\x4d\x9b\x95\x61\xfd\ +\x30\x02\xd0\xfe\x2d\x01\xd3\x00\x02\xfd\x2c\xfc\xcf\xfe\xfc\xff\ +\x9f\x00\x05\x00\x0d\x00\x00\x05\x0b\x01\x33\x1b\x01\x06\x34\x36\ +\x32\x16\x14\x06\x22\xfe\xfc\xe8\xe8\x4d\x9b\x95\xe5\x2f\x42\x2f\ +\x2f\x42\x61\xfd\x30\x02\xd0\xfe\x2d\x01\xd3\x74\x42\x2f\x2f\x42\ +\x2f\x00\x00\x00\x02\xfc\xbf\xfc\xcf\xff\x8f\xff\x9c\x00\x07\x00\ +\x0d\x00\x00\x04\x34\x36\x32\x16\x14\x06\x22\x0d\x02\x35\x2d\x01\ +\xfd\xc4\x2f\x42\x2f\x2f\x42\xfe\xcc\x02\xd0\xfd\x30\x01\xd3\xfe\ +\x2d\xd5\x42\x2f\x2f\x42\x2f\x5d\xe8\xe8\x4d\x9b\x95\x00\x00\x00\ +\x02\xfc\xf0\xfe\x02\xff\x38\xff\x51\x00\x03\x00\x0b\x00\x00\x01\ +\x21\x15\x21\x36\x34\x36\x32\x16\x14\x06\x22\xfc\xf0\x02\x48\xfd\ +\xb8\xd4\x2f\x42\x2f\x2f\x42\xfe\x48\x46\xde\x42\x2f\x2f\x42\x2f\ +\x00\x00\x00\x00\x01\x00\xc8\xff\xf8\x01\xae\x06\x6e\x00\x65\x00\ +\x00\x25\x2e\x01\x35\x34\x3e\x02\x35\x34\x2e\x02\x35\x34\x3e\x02\ +\x35\x34\x2e\x02\x35\x34\x3e\x02\x35\x34\x2e\x02\x35\x34\x3e\x02\ +\x35\x34\x2e\x02\x27\x26\x35\x34\x36\x33\x32\x17\x1e\x02\x15\x14\ +\x0e\x02\x15\x14\x1e\x03\x15\x14\x0e\x02\x15\x14\x1e\x03\x15\x14\ +\x0e\x02\x15\x14\x1e\x03\x15\x14\x0e\x02\x15\x14\x16\x17\x16\x15\ +\x14\x06\x23\x22\x01\x02\x20\x1a\x24\x2c\x24\x24\x2c\x24\x24\x2c\ +\x24\x24\x2c\x24\x24\x2c\x24\x24\x2c\x24\x24\x2c\x24\x0e\x0a\x20\ +\x03\x05\x15\x0a\x0d\x2a\x07\x3a\x1a\x28\x31\x28\x1b\x26\x26\x1b\ +\x29\x30\x29\x1b\x26\x26\x1b\x29\x30\x29\x1b\x26\x26\x1b\x29\x30\ +\x29\x12\x15\x05\x15\x0a\x19\x17\x21\x34\x2d\x1d\x34\x20\x29\x14\ +\x16\x33\x2a\x46\x27\x1d\x34\x20\x29\x14\x16\x33\x2a\x46\x27\x1d\ +\x34\x20\x29\x14\x16\x33\x2a\x46\x27\x1d\x34\x20\x29\x14\x0d\x1b\ +\x0d\x1e\x03\x05\x0a\x07\x11\x1f\x07\x2e\x26\x18\x21\x3e\x27\x2e\ +\x13\x1e\x30\x1f\x1c\x26\x17\x21\x3f\x27\x2e\x13\x1e\x30\x1f\x1c\ +\x26\x17\x21\x3f\x27\x2e\x13\x1e\x30\x1f\x1c\x26\x17\x21\x3f\x27\ +\x2e\x13\x19\x1e\x15\x05\x0a\x07\x11\x00\x00\x00\x01\x00\xb4\xfe\ +\xcf\x01\xc2\x06\x6e\x00\x60\x00\x00\x01\x03\x33\x2e\x01\x35\x34\ +\x3e\x02\x35\x34\x2e\x02\x35\x34\x3e\x02\x35\x34\x2e\x02\x35\x34\ +\x3e\x02\x35\x34\x2e\x02\x35\x34\x3e\x02\x35\x34\x26\x27\x26\x35\ +\x34\x36\x33\x32\x17\x1e\x02\x15\x14\x0e\x02\x15\x14\x1e\x03\x15\ +\x14\x0e\x02\x15\x14\x1e\x03\x15\x14\x0e\x02\x15\x14\x1e\x03\x15\ +\x14\x0e\x02\x15\x14\x16\x17\x33\x01\x3b\x87\x44\x1a\x16\x24\x2c\ +\x24\x24\x2c\x24\x24\x2c\x24\x24\x2c\x24\x24\x2c\x24\x24\x2c\x24\ +\x24\x2c\x24\x28\x13\x05\x15\x0a\x0f\x28\x07\x3a\x1a\x28\x31\x28\ +\x1b\x26\x26\x1b\x29\x30\x29\x1b\x26\x26\x1b\x29\x30\x29\x1b\x26\ +\x26\x1b\x29\x30\x29\x11\x14\x71\xfe\xcf\x01\x52\x1c\x33\x29\x1d\ +\x34\x20\x29\x14\x16\x33\x2a\x46\x27\x1d\x34\x20\x29\x14\x16\x33\ +\x2a\x46\x27\x1d\x34\x20\x29\x14\x16\x33\x2a\x46\x27\x1d\x34\x20\ +\x29\x14\x18\x2f\x0f\x05\x0a\x07\x11\x1f\x07\x2e\x26\x18\x21\x3e\ +\x27\x2e\x13\x1e\x30\x1f\x1c\x26\x17\x21\x3f\x27\x2e\x13\x1e\x30\ +\x1f\x1c\x26\x17\x21\x3f\x27\x2e\x13\x1e\x30\x1f\x1c\x26\x17\x21\ +\x3f\x27\x2e\x13\x18\x1d\x15\x00\x01\xfc\xab\x06\xda\x00\x0f\x08\ +\x81\x00\x13\x00\x00\x01\x32\x3e\x03\x37\x17\x0e\x03\x07\x06\x23\ +\x22\x26\x27\x37\x16\xfd\xaf\x5e\xac\x74\x65\x33\x12\x38\x12\x3f\ +\x5b\x7c\x42\x51\x68\x5a\xae\x39\x15\x6b\x07\x37\x35\x4b\x62\x47\ +\x20\x25\x28\x5e\x69\x5c\x18\x1e\x2a\x1b\x41\x29\x00\x00\x00\x00\ +\x01\xfc\xab\x06\xda\x00\x0f\x08\x81\x00\x11\x00\x00\x01\x22\x07\ +\x27\x3e\x01\x33\x32\x17\x1e\x01\x17\x07\x2e\x04\xfd\xaf\x84\x6b\ +\x15\x39\xae\x5a\x68\x51\x7c\xc4\x2a\x38\x12\x33\x65\x74\xac\x08\ +\x23\x29\x41\x1b\x2a\x1e\x2e\xd7\x5e\x25\x20\x47\x62\x4b\x35\x00\ +\x01\xfd\x12\x06\xcd\xff\xdc\x08\x7c\x00\x06\x00\x00\x01\x11\x33\ +\x01\x07\x01\x11\xfd\x12\x46\x02\x84\x23\xfd\x9f\x06\xda\x01\xa2\ +\xfe\x8e\x3c\x01\x5d\xfe\xaf\x00\x01\xfd\x6c\x06\xda\xff\x38\x07\ +\xc1\x00\x22\x00\x00\x01\x32\x1e\x03\x33\x32\x3e\x01\x33\x32\x16\ +\x15\x14\x07\x0e\x02\x23\x22\x2e\x03\x23\x22\x0e\x01\x23\x22\x26\ +\x35\x34\x36\xfd\xf8\x1b\x30\x23\x20\x24\x12\x17\x26\x1c\x0b\x07\ +\x11\x1e\x09\x2d\x26\x18\x1c\x32\x25\x23\x29\x16\x10\x1c\x17\x0a\ +\x07\x11\x5e\x07\xc0\x18\x23\x23\x18\x20\x20\x14\x0a\x0d\x2b\x08\ +\x38\x1a\x1b\x26\x27\x1b\x18\x17\x15\x0b\x1a\x58\x00\x00\x00\x00\ +\x01\xfd\x07\x06\xda\xff\x9f\x08\xb4\x00\x10\x00\x00\x03\x11\x14\ +\x07\x06\x23\x22\x27\x26\x35\x11\x33\x11\x14\x20\x35\x11\x61\x94\ +\x3a\x7e\x9d\x41\x6e\x46\x02\x0c\x08\xb4\xfe\xc8\x7a\x1d\x0b\x15\ +\x23\x6a\x01\x38\xfe\xca\x5e\x5e\x01\x36\x00\x00\x03\xfc\x78\xfd\ +\xb8\xff\xb0\xff\x51\x00\x07\x00\x0f\x00\x1b\x00\x00\x00\x34\x36\ +\x32\x16\x14\x06\x22\x24\x34\x36\x32\x16\x14\x06\x22\x04\x22\x26\ +\x27\x37\x1e\x01\x32\x36\x37\x17\x06\xfe\x5a\x2f\x42\x2f\x2f\x42\ +\xfe\xa5\x2f\x42\x2f\x2f\x42\x01\x2e\xee\xdf\x46\x2a\x41\xc5\xd8\ +\xc5\x43\x28\x46\xfe\xe0\x42\x2f\x2f\x42\x2f\x2f\x42\x2f\x2f\x42\ +\x2f\xf9\x76\x48\x2c\x3b\x51\x51\x3b\x2c\x48\x00\x04\xfc\x78\xfd\ +\xb8\xff\xb0\xff\x51\x00\x07\x00\x0f\x00\x17\x00\x23\x00\x00\x00\ +\x34\x36\x32\x16\x14\x06\x22\x36\x34\x36\x32\x16\x14\x06\x22\x24\ +\x34\x36\x32\x16\x14\x06\x22\x04\x22\x26\x27\x37\x1e\x01\x32\x36\ +\x37\x17\x06\xfd\xc4\x2f\x42\x2f\x2f\x42\xcb\x2f\x42\x2f\x2f\x42\ +\xfd\xdd\x2f\x42\x2f\x2f\x42\x01\x92\xee\xdf\x46\x2a\x41\xc5\xd8\ +\xc5\x43\x28\x46\xfe\xe0\x42\x2f\x2f\x42\x2f\x2f\x42\x2f\x2f\x42\ +\x2f\x2f\x42\x2f\x2f\x42\x2f\xf9\x76\x48\x2c\x3b\x51\x51\x3b\x2c\ +\x48\x00\x00\x00\x01\x00\xb4\x01\x13\x02\xaa\x03\x89\x00\x23\x00\ +\x00\x01\x32\x17\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\ +\x34\x36\x33\x32\x35\x34\x26\x23\x22\x07\x03\x23\x13\x36\x26\x23\ +\x22\x07\x27\x36\x01\x75\x2b\x12\x17\x12\x2f\x1f\x32\x4f\x2e\x31\ +\x2f\x28\x2c\x24\x23\x2a\x19\x50\x0f\x63\x97\x65\x02\x0b\x07\x17\ +\x25\x30\x51\x03\x89\x36\x16\x0e\x12\x49\x35\x3c\x50\x25\x25\x1f\ +\x29\x11\x0c\x1b\x4c\xfe\x16\x01\xf4\x0b\x13\x3d\x23\x7e\x00\x00\ +\x01\x00\xb8\x01\x03\x02\xb4\x03\x89\x00\x37\x00\x00\x01\x34\x26\ +\x23\x22\x06\x15\x14\x1e\x04\x15\x14\x06\x23\x22\x26\x35\x34\x36\ +\x33\x32\x16\x15\x14\x06\x15\x14\x16\x33\x32\x36\x35\x34\x2e\x04\ +\x35\x34\x36\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x02\ +\x39\x23\x1f\x38\x2c\x26\x3a\x43\x3a\x26\x9e\x74\x5a\x72\x30\x20\ +\x1b\x25\x12\x3a\x29\x2c\x3a\x25\x38\x40\x38\x25\x83\x71\x55\x66\ +\x28\x1a\x20\x20\x07\x03\x14\x11\x24\x23\x31\x1b\x30\x21\x2f\x30\ +\x51\x32\x53\x51\x48\x43\x31\x32\x23\x16\x10\x18\x0b\x1f\x23\x33\ +\x27\x25\x37\x20\x27\x28\x48\x30\x50\x59\x55\x3c\x18\x20\x1f\x15\ +\x07\x13\x00\x00\x01\x00\x8c\x00\xec\x03\x0c\x03\x80\x00\x2a\x00\ +\x00\x01\x32\x36\x35\x34\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\ +\x14\x23\x22\x2e\x01\x23\x22\x06\x07\x23\x35\x01\x23\x22\x07\x06\ +\x07\x23\x37\x21\x15\x01\x36\x33\x32\x1e\x02\x02\x4f\x19\x2a\x23\ +\x24\x2c\x28\x2f\x30\x2f\xb9\x1b\x3d\x42\x22\x2d\x3a\x1f\x4e\x01\ +\xaa\x80\x2f\x24\x10\x0c\x31\x28\x01\xce\xfe\x6b\x17\x19\x1f\x2f\ +\x19\x29\x01\x52\x1b\x0c\x11\x29\x1f\x25\x25\x50\x3c\xa4\x28\x29\ +\x11\x19\x1d\x01\xc7\x15\x08\x22\xc8\x5b\xfe\x82\x03\x1c\x20\x1c\ +\x00\x00\x00\x00\x02\x00\x78\x00\x00\x03\x76\x03\x89\x00\x24\x00\ +\x35\x00\x00\x01\x14\x0e\x04\x23\x22\x27\x03\x33\x15\x21\x35\x33\ +\x13\x36\x23\x22\x06\x07\x27\x3e\x03\x33\x32\x17\x3e\x01\x33\x32\ +\x1e\x02\x25\x07\x06\x15\x14\x33\x32\x36\x37\x36\x35\x34\x23\x22\ +\x0e\x02\x03\x76\x0c\x1a\x30\x41\x63\x3b\x49\x33\x33\x7c\xfe\x6a\ +\x82\x8e\x07\x15\x13\x50\x27\x38\x1b\x35\x35\x56\x3c\x46\x07\x33\ +\x48\x3e\x2b\x3f\x25\x12\xfe\xa2\x32\x08\x36\x2e\x66\x19\x14\x47\ +\x10\x1f\x24\x1c\x02\xa9\x26\x53\x61\x55\x47\x2a\x36\xff\x00\x3f\ +\x3f\x02\xc1\x25\x77\x70\x18\x44\x71\x51\x2d\x41\x26\x1b\x24\x3f\ +\x4e\x02\xfa\x26\x1e\x4f\x6b\x80\x69\x39\x6e\x08\x15\x2f\x00\x00\ +\x01\x00\x92\x01\x09\x04\xd7\x03\x89\x00\x3e\x00\x00\x01\x22\x26\ +\x37\x13\x36\x26\x23\x22\x0e\x02\x07\x03\x23\x13\x36\x26\x23\x22\ +\x0e\x02\x07\x03\x23\x13\x36\x23\x22\x06\x07\x27\x3e\x03\x33\x32\ +\x17\x36\x33\x32\x17\x3e\x03\x33\x32\x16\x07\x03\x06\x33\x3e\x01\ +\x37\x17\x0e\x03\x03\xc0\x36\x26\x09\x54\x04\x10\x1b\x10\x21\x26\ +\x1e\x07\x5c\x97\x6a\x04\x10\x1b\x10\x21\x26\x1e\x07\x5c\x97\x63\ +\x07\x15\x13\x50\x27\x38\x1b\x35\x35\x56\x3c\x46\x07\x56\x82\x41\ +\x13\x18\x20\x28\x40\x38\x2b\x39\x0d\x4a\x07\x15\x15\x4f\x26\x38\ +\x1b\x35\x35\x56\x01\x09\x46\x2f\x01\xa2\x17\x11\x08\x15\x2f\x22\ +\xfe\x39\x02\x0d\x17\x11\x08\x15\x2f\x22\xfe\x39\x01\xed\x25\x77\ +\x70\x18\x44\x71\x51\x2d\x41\x41\x41\x12\x19\x0f\x07\x4d\x3f\xfe\ +\x95\x25\x01\x77\x6f\x18\x44\x71\x51\x2d\x00\x00\x01\x00\x8b\xff\ +\xf1\x04\x05\x05\x1e\x00\x3c\x00\x00\x01\x22\x0e\x02\x07\x33\x15\ +\x23\x06\x07\x0a\x01\x23\x22\x26\x35\x34\x36\x33\x32\x16\x15\x14\ +\x06\x23\x22\x06\x15\x14\x33\x32\x3e\x02\x37\x36\x37\x23\x35\x33\ +\x3e\x02\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\ +\x35\x34\x26\x03\x6d\x21\x34\x27\x1a\x0e\x95\x9f\x1c\x01\x35\xd5\ +\x75\x55\x43\x3c\x27\x22\x32\x19\x13\x15\x11\x33\x2e\x43\x26\x24\ +\x0e\x0c\x18\x86\x98\x25\x63\x81\x42\x3f\x59\x3c\x27\x22\x32\x19\ +\x13\x26\x21\x04\xd8\x34\x65\x6f\x50\x3f\xb1\x09\xfe\xc6\xfe\xa4\ +\x33\x3e\x3b\x4d\x23\x1f\x1d\x25\x10\x0f\x10\x6a\xab\xd9\x6e\x57\ +\x57\x3f\x75\xb6\x73\x39\x39\x3a\x4d\x23\x1f\x1c\x26\x1f\x07\x09\ +\x00\x00\x00\x00\x01\x00\xc8\x01\x0e\x08\xf2\x03\x9c\x00\x06\x00\ +\x00\x13\x01\x17\x09\x01\x07\x01\xc8\x08\x20\x0a\xf8\xd8\x07\x28\ +\x0a\xf7\xe0\x02\x78\x01\x24\x45\xfe\xfe\xfe\xfe\x45\x01\x24\x00\ +\x01\x00\xc8\x01\x0e\x08\xf2\x03\x9c\x00\x06\x00\x00\x01\x15\x01\ +\x27\x09\x01\x37\x08\xf2\xf7\xe0\x0a\x07\x28\xf8\xd8\x0a\x02\x78\ +\x46\xfe\xdc\x45\x01\x02\x01\x02\x45\x00\x00\x00\x02\x00\xc8\x01\ +\x09\x03\xa2\x05\x83\x00\x2e\x00\x32\x00\x00\x01\x33\x1e\x01\x17\ +\x37\x17\x07\x1e\x01\x15\x14\x06\x07\x06\x26\x37\x3e\x05\x35\x34\ +\x2e\x04\x27\x07\x11\x14\x06\x23\x22\x35\x34\x36\x33\x32\x17\x11\ +\x07\x27\x3f\x02\x26\x27\x02\x30\x31\x0f\x4c\x3e\x5d\x23\x5f\x43\ +\x44\x39\x48\x08\x27\x08\x13\x20\x1b\x15\x0f\x08\x0c\x16\x1d\x22\ +\x25\x13\x7a\x99\x62\x9e\x98\x63\x44\x29\x75\x23\x98\x31\x53\x45\ +\x0e\x05\x83\x4c\x79\x47\x5d\x23\x5e\x4b\x74\x4a\x5b\x79\x54\x0a\ +\x13\x0a\x17\x2c\x26\x29\x29\x32\x1c\x1a\x2f\x24\x22\x1c\x1c\x0d\ +\x79\xfe\x3e\x57\x77\x68\x57\x78\x15\x01\x3d\x75\x23\x98\x31\x52\ +\x39\x42\x00\x00\x01\x00\xc8\x01\x09\x03\xa2\x05\x83\x00\x25\x00\ +\x00\x01\x33\x1e\x04\x15\x14\x06\x07\x06\x26\x37\x3e\x05\x35\x34\ +\x2e\x03\x27\x11\x14\x06\x23\x22\x35\x34\x36\x33\x32\x17\x02\x30\ +\x31\x0e\x4d\x59\x55\x38\x39\x48\x08\x27\x08\x13\x20\x1b\x15\x0f\ +\x08\x32\x4a\x4d\x40\x0a\x99\x62\x9e\x98\x63\x44\x29\x05\x83\x46\ +\x82\x61\x60\x72\x3e\x5b\x79\x54\x0a\x13\x0a\x17\x2c\x26\x29\x29\ +\x32\x1c\x34\x53\x39\x36\x4b\x2d\xfd\x2b\x57\x77\x68\x57\x78\x15\ +\x00\x00\x00\x00\x03\x00\xb9\x01\x0c\x04\x98\x05\x02\x00\x3d\x00\ +\x47\x00\x4f\x00\x00\x01\x03\x23\x13\x36\x26\x23\x22\x0e\x01\x0f\ +\x01\x1e\x01\x15\x14\x06\x23\x22\x26\x3f\x01\x06\x23\x22\x35\x34\ +\x36\x33\x32\x17\x13\x37\x03\x3e\x01\x33\x32\x16\x17\x3e\x02\x33\ +\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x35\x34\x26\ +\x23\x22\x05\x26\x23\x22\x06\x15\x14\x16\x33\x32\x17\x07\x06\x33\ +\x32\x36\x35\x34\x03\xb3\x63\x97\x6a\x03\x0a\x0d\x13\x41\x33\x33\ +\x16\x22\x28\x4d\x3f\x4d\x3e\x18\x25\x54\x37\x6b\x57\x35\x3d\x44\ +\x54\xa0\x5a\x51\x7b\x37\x1b\x19\x09\x17\x12\x2f\x1f\x32\x4f\x2e\ +\x31\x2f\x28\x2c\x24\x23\x2a\x19\x50\xfd\xf8\x3f\x34\x28\x34\x18\ +\x21\x3b\xcf\x24\x0b\x1b\x18\x25\x02\xfd\xfe\x16\x02\x0d\x0e\x17\ +\x17\x19\x19\x71\x3b\x75\x2a\x46\x5f\x71\x73\xbf\x20\x63\x33\x3a\ +\x3b\x01\xb1\x2d\xfe\x36\x27\x2a\x1b\x1b\x16\x0e\x12\x49\x35\x3c\ +\x50\x25\x25\x1f\x29\x11\x0c\x1b\x5f\x43\x1e\x1d\x16\x18\x8e\xba\ +\x3b\x48\x2d\x29\x00\x00\x00\x00\x01\x00\xc1\x01\x76\x04\xe9\x03\ +\x64\x00\x3b\x00\x00\x01\x22\x2e\x03\x23\x22\x06\x15\x14\x16\x33\ +\x32\x36\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\x3e\x03\x33\ +\x32\x1e\x04\x33\x32\x36\x35\x34\x26\x23\x22\x06\x23\x22\x26\x35\ +\x34\x36\x33\x32\x16\x15\x14\x0e\x02\x03\xc3\x4a\x88\x67\x5f\x67\ +\x31\x33\x4c\x0f\x0d\x06\x1f\x0a\x3e\x38\x48\x2a\x49\x59\x2c\x41\ +\x54\x46\x1f\x41\x7a\x58\x5a\x47\x54\x28\x30\x4f\x0e\x0e\x05\x20\ +\x0a\x3e\x38\x48\x2a\x49\x59\x40\x62\x5b\x01\x76\x4c\x6c\x6c\x4c\ +\x3f\x2e\x18\x10\x02\x44\x23\x32\x44\x77\x83\x38\x58\x35\x22\x0d\ +\x37\x51\x60\x51\x37\x3f\x2d\x19\x0f\x02\x44\x24\x32\x44\x77\x83\ +\x45\x66\x32\x17\x00\x00\x00\x00\x01\x00\xc1\x01\x76\x04\xe9\x03\ +\x64\x00\x3d\x00\x00\x01\x22\x2e\x02\x35\x34\x36\x33\x32\x16\x15\ +\x14\x06\x23\x22\x26\x23\x22\x0e\x01\x15\x14\x16\x33\x32\x3e\x04\ +\x33\x32\x1e\x03\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x16\ +\x33\x32\x3e\x01\x35\x34\x26\x23\x22\x0e\x03\x01\xe7\x29\x5b\x62\ +\x40\x59\x49\x2a\x48\x38\x3e\x0a\x20\x05\x09\x0a\x09\x4f\x30\x28\ +\x54\x47\x5a\x58\x7a\x41\x1f\x46\x54\x41\x2c\x59\x49\x2a\x48\x38\ +\x3e\x0a\x1f\x06\x08\x0b\x09\x4c\x33\x31\x67\x5f\x67\x88\x01\x76\ +\x17\x32\x66\x45\x83\x77\x44\x32\x24\x44\x02\x04\x13\x11\x2d\x3f\ +\x37\x51\x60\x51\x37\x0d\x22\x35\x58\x38\x83\x77\x44\x32\x23\x44\ +\x02\x04\x13\x11\x2e\x3f\x4c\x6c\x6c\x4c\x00\x00\x01\x00\xc1\x01\ +\x26\x04\xe9\x03\xb4\x00\x44\x00\x00\x01\x33\x15\x1e\x04\x33\x32\ +\x36\x35\x34\x26\x23\x22\x06\x23\x22\x26\x35\x34\x36\x33\x32\x16\ +\x15\x14\x0e\x02\x23\x22\x27\x15\x23\x35\x2e\x04\x23\x22\x06\x15\ +\x14\x16\x33\x32\x36\x33\x32\x16\x15\x14\x06\x23\x22\x26\x35\x34\ +\x3e\x03\x33\x32\x17\x02\xb7\x3c\x1a\x46\x32\x3b\x3a\x1d\x30\x4f\ +\x0e\x0e\x05\x20\x0a\x3e\x38\x48\x2a\x49\x59\x40\x62\x5b\x29\x6a\ +\x66\x3c\x17\x48\x2f\x3c\x3c\x1e\x33\x4c\x0f\x0d\x06\x1f\x0a\x3e\ +\x38\x48\x2a\x49\x59\x2c\x41\x54\x46\x1f\x6c\x64\x03\xb4\xd2\x19\ +\x4e\x36\x36\x1b\x3f\x2d\x19\x0f\x02\x44\x24\x32\x44\x77\x83\x45\ +\x66\x32\x17\x52\xa2\xd9\x17\x50\x31\x35\x1a\x3f\x2e\x18\x10\x02\ +\x44\x23\x32\x44\x77\x83\x38\x58\x35\x22\x0d\x4d\x00\x00\x00\x00\ +\x01\x00\xc0\x00\x59\x02\xae\x04\x81\x00\x3b\x00\x00\x01\x14\x0e\ +\x03\x15\x14\x16\x33\x32\x36\x35\x34\x26\x35\x34\x36\x33\x32\x16\ +\x15\x14\x06\x23\x22\x2e\x03\x35\x34\x3e\x04\x35\x34\x26\x23\x22\ +\x06\x15\x14\x16\x15\x14\x06\x23\x22\x26\x35\x34\x36\x33\x32\x1e\ +\x02\x02\xae\x4c\x6c\x6c\x4c\x3f\x2f\x18\x10\x02\x43\x23\x33\x43\ +\x77\x83\x37\x59\x34\x23\x0d\x37\x51\x60\x51\x37\x3f\x2d\x19\x0f\ +\x02\x43\x25\x32\x44\x78\x82\x45\x66\x32\x17\x03\x5b\x4a\x88\x67\ +\x5f\x67\x31\x33\x4c\x0f\x0d\x06\x1f\x0a\x3e\x38\x48\x2a\x49\x59\ +\x2c\x42\x53\x46\x1f\x41\x7a\x58\x5a\x47\x54\x28\x30\x4f\x0e\x0e\ +\x05\x20\x0a\x3e\x38\x48\x2a\x49\x59\x40\x62\x5b\x00\x00\x00\x00\ +\x01\x00\xc8\x02\x40\x01\xf8\x06\x66\x00\x17\x00\x00\x13\x34\x36\ +\x33\x32\x16\x15\x11\x14\x33\x32\x3e\x01\x33\x32\x16\x15\x14\x0e\ +\x01\x23\x22\x26\x35\xc8\x73\x08\x05\x0c\x15\x12\x2a\x1d\x02\x09\ +\x2b\x3e\x67\x2a\x30\x31\x06\x13\x0c\x47\x0d\x0a\xfc\xeb\x83\x1e\ +\x1e\x2c\x09\x04\x3b\x3f\x74\x48\x00\x00\x00\x00\x01\x00\xc7\x02\ +\x34\x03\xa5\x04\x32\x00\x13\x00\x00\x01\x22\x24\x23\x22\x06\x07\ +\x06\x26\x37\x3e\x03\x33\x32\x00\x15\x14\x06\x02\xd7\x0c\xfe\xd7\ +\x10\x13\x27\x5d\x05\x32\x04\x4c\x94\x41\x32\x0c\x1b\x01\x63\xc0\ +\x02\x34\xf0\x25\x61\x05\x39\x05\x4c\x9b\x47\x2d\xfe\xdc\x0e\x0f\ +\xbd\x00\x00\x00\x01\x00\xc7\x02\x34\x04\x37\x04\x32\x00\x1a\x00\ +\x00\x01\x32\x36\x37\x36\x16\x07\x0e\x03\x22\x24\x23\x22\x06\x07\ +\x06\x26\x37\x3e\x03\x33\x32\x04\x03\x6c\x0d\x28\x62\x05\x33\x05\ +\x4b\x92\x42\x34\x18\xfe\xd7\x10\x13\x27\x5d\x05\x32\x04\x4c\x94\ +\x41\x32\x0c\x16\x01\x28\x03\x42\x24\x62\x05\x39\x05\x4b\x9a\x48\ +\x2e\xf0\x25\x61\x05\x39\x05\x4c\x9b\x47\x2d\xf0\x00\x00\x00\x00\ +\x01\x00\xc7\x03\x66\x01\xc8\x06\x23\x00\x17\x00\x00\x13\x3e\x03\ +\x35\x34\x27\x26\x3f\x01\x1e\x04\x15\x14\x0e\x02\x07\x06\x26\xc8\ +\x37\x33\x3d\x17\x43\x04\x0d\x04\x14\x19\x25\x15\x11\x18\x3f\x39\ +\x3c\x04\x34\x03\x9f\x37\x39\x5c\x5f\x3e\xbd\x37\x03\x1d\x06\x11\ +\x18\x33\x38\x5a\x36\x4a\x6e\x64\x40\x3b\x04\x38\x00\x00\x00\x00\ +\x01\x00\xc7\x00\x8a\x02\x6d\x04\x32\x00\x27\x00\x00\x01\x14\x0e\ +\x03\x23\x22\x26\x35\x37\x3e\x03\x35\x34\x2e\x02\x23\x22\x0e\x01\ +\x07\x06\x26\x37\x3e\x03\x33\x32\x1e\x05\x02\x6d\x32\x48\x4c\x34\ +\x06\x03\x0c\x0f\x0f\x2c\x2c\x1e\x27\x36\x2f\x0e\x06\x0c\x26\x25\ +\x05\x33\x05\x1e\x48\x23\x23\x0c\x0a\x1f\x2e\x2d\x2f\x24\x16\x02\ +\x58\x4f\x99\x6a\x53\x29\x43\x05\x12\x13\x42\x57\x7a\x3f\x59\x8f\ +\x4a\x26\x08\x25\x26\x05\x39\x05\x1e\x4d\x25\x1b\x09\x1b\x2a\x4a\ +\x60\x8e\x00\x00\x01\x00\xc8\x01\x46\x01\x1d\x05\x20\x00\x03\x00\ +\x00\x01\x11\x23\x11\x01\x1d\x55\x05\x20\xfc\x26\x03\xda\x00\x00\ +\x01\x00\xc8\x00\xc4\x04\x49\x04\x24\x00\x27\x00\x00\x01\x32\x1e\ +\x02\x15\x14\x06\x23\x22\x2e\x01\x23\x22\x0e\x01\x15\x14\x16\x33\ +\x32\x3e\x01\x33\x32\x16\x15\x14\x0e\x02\x23\x22\x26\x35\x34\x3e\ +\x03\x02\x80\x4c\xae\x7c\x53\xc0\x0e\x09\x8c\xa7\x29\x3c\x57\x27\ +\x88\x54\x33\x58\x31\x04\x0a\x2a\x26\x3e\x5f\x31\x9e\xd2\x23\x49\ +\x65\x93\x04\x24\x56\x6d\x59\x08\x0f\xbd\x74\x74\x48\x65\x37\x66\ +\xa6\x1f\x1f\x23\x05\x07\x27\x2d\x23\xc7\xc1\x3c\x81\x7c\x62\x3d\ +\x00\x00\x00\x00\x01\x00\xc8\x02\x28\x03\x6a\x05\x9c\x00\x2c\x00\ +\x00\x01\x34\x3e\x02\x35\x34\x26\x23\x22\x06\x15\x14\x1e\x02\x33\ +\x32\x37\x36\x16\x07\x0e\x02\x23\x22\x2e\x02\x35\x34\x3e\x02\x33\ +\x32\x1e\x02\x15\x14\x23\x22\x26\x02\x6b\x13\x17\x13\x3f\x39\x55\ +\x83\x26\x3e\x48\x25\xa2\x67\x04\x38\x05\x3b\x50\x7c\x57\x3e\x72\ +\x5c\x37\x3f\x67\x7b\x3f\x3c\x5c\x33\x19\x51\x2d\x23\x04\xb4\x17\ +\x19\x07\x12\x0f\x0d\x17\xb8\xa2\x4b\x78\x48\x26\x67\x04\x34\x04\ +\x43\x43\x2a\x39\x6a\xa8\x67\x6e\xb1\x6b\x38\x1f\x30\x33\x18\x86\ +\x1c\x00\x00\x00\x01\x00\xc7\x03\x4d\x03\xc2\x04\xef\x00\x24\x00\ +\x00\x01\x22\x2e\x04\x23\x22\x0e\x04\x07\x30\x23\x22\x26\x35\x34\ +\x37\x3e\x04\x33\x32\x1e\x02\x17\x16\x15\x14\x06\x03\x9a\x08\x12\ +\x10\x21\x2a\x4e\x31\x1d\x47\x3d\x58\x36\x65\x17\x01\x07\x2c\x01\ +\x12\x68\x52\x6f\x70\x32\x3a\x4f\x41\x30\x21\x01\x1a\x03\xf3\x19\ +\x25\x2c\x25\x19\x1f\x29\x50\x36\x68\x17\x2f\x08\x01\x01\x12\x72\ +\x52\x5e\x34\x16\x3a\x4b\x43\x03\x03\x09\x0f\x00\x01\x00\xc8\x01\ +\x7a\x03\xc2\x03\x1b\x00\x20\x00\x00\x01\x33\x32\x16\x15\x07\x0e\ +\x04\x23\x22\x2e\x02\x27\x26\x35\x34\x36\x33\x32\x17\x1e\x01\x33\ +\x32\x3e\x04\x03\x8e\x01\x07\x2c\x01\x12\x68\x52\x6f\x70\x32\x3a\ +\x4f\x41\x30\x21\x01\x1a\x0d\x0a\x03\x2c\x67\x54\x1d\x47\x3d\x58\ +\x36\x65\x03\x1b\x2f\x08\x02\x12\x72\x52\x5e\x34\x16\x3a\x4b\x43\ +\x02\x04\x09\x0f\x06\x5c\x46\x1f\x29\x50\x36\x68\x00\x00\x00\x00\ +\x01\x00\xc8\x01\xca\x03\x2e\x04\x1a\x00\x23\x00\x00\x01\x22\x26\ +\x35\x34\x3e\x01\x37\x36\x33\x32\x16\x15\x14\x07\x06\x15\x14\x16\ +\x33\x32\x3e\x03\x37\x33\x32\x16\x15\x07\x0e\x04\x01\x83\x4d\x6e\ +\x32\x2d\x04\x02\x06\x0b\x1b\x01\x43\x43\x3b\x29\x4c\x4c\x35\x59\ +\x18\x01\x07\x2c\x01\x13\x55\x3d\x5c\x6c\x01\xca\xad\x7d\x42\x92\ +\x4e\x02\x02\x14\x0a\x03\x02\x96\x68\x64\x7f\x1c\x3d\x34\x60\x18\ +\x2f\x08\x02\x13\x60\x3b\x47\x23\x00\x00\x00\x00\x01\x00\xc8\x01\ +\x5c\x05\x78\x05\x0a\x00\x0d\x00\x00\x13\x11\x33\x11\x21\x11\x21\ +\x15\x21\x11\x23\x11\x21\x11\xc8\x64\x01\xc2\x02\x8a\xfd\xda\x64\ +\xfe\x3e\x01\x5c\x03\xae\xfe\x5b\x01\xa5\x64\xfc\xb6\x01\xa5\xfe\ +\x5b\x00\x00\x00\x01\x00\xc8\x01\x5c\x05\x78\x05\x0a\x00\x0b\x00\ +\x00\x01\x23\x11\x33\x01\x11\x21\x15\x21\x11\x23\x01\x01\x2c\x64\ +\x64\x01\xc2\x02\x8a\xfd\xda\x64\xfe\x3e\x01\x5c\x03\xae\xfd\x16\ +\x02\xea\x64\xfc\xb6\x02\xea\x00\x01\x00\xc8\x01\x5c\x03\x52\x05\ +\x0a\x00\x05\x00\x00\x01\x23\x11\x21\x35\x21\x03\x52\x64\xfd\xda\ +\x02\x8a\x01\x5c\x03\x4a\x64\x00\x03\x00\xb4\x02\xf4\x02\x66\x04\ +\xa6\x00\x13\x00\x1b\x00\x23\x00\x00\x13\x34\x36\x33\x32\x17\x37\ +\x17\x07\x16\x15\x14\x06\x23\x22\x27\x07\x27\x37\x26\x25\x07\x16\ +\x33\x32\x36\x35\x34\x27\x26\x23\x22\x06\x15\x14\x17\xdb\x69\x49\ +\x35\x2e\x45\x31\x45\x1e\x69\x49\x35\x2e\x45\x31\x45\x1e\x01\x15\ +\x95\x17\x1b\x2e\x41\x3d\x17\x1b\x2e\x41\x0c\x03\xcd\x49\x69\x1e\ +\x45\x31\x45\x2e\x35\x49\x69\x1e\x45\x31\x45\x2e\x67\x95\x0c\x41\ +\x2e\x1b\x48\x0c\x41\x2e\x1b\x17\x00\x00\x00\x00\x01\xfd\x29\x06\ +\xd2\xff\xc7\x09\x0c\x00\x07\x00\x00\x03\x11\x21\x11\x23\x11\x21\ +\x11\x7f\xfd\xee\x46\x02\x9e\x06\xd2\x01\x54\xfe\xac\x02\x3a\xfd\ +\xc6\x00\x00\x00\x01\xfd\x6b\x06\xd0\xff\x3b\x09\xa0\x00\x05\x00\ +\x00\x0b\x02\x33\x1b\x01\xc5\xe8\xe8\x4d\x9b\x95\x09\xa0\xfd\x30\ +\x02\xd0\xfe\x2d\x01\xd3\x00\x00\x02\xfd\xa1\x06\xd1\xff\x05\x08\ +\x35\x00\x07\x00\x0f\x00\x00\x00\x14\x16\x32\x36\x34\x26\x22\x06\ +\x34\x36\x32\x16\x14\x06\x22\xfd\xe4\x41\x5c\x41\x41\x5c\x84\x69\ +\x92\x69\x69\x92\x07\xb1\x5c\x41\x41\x5c\x41\xb8\x92\x69\x69\x92\ +\x69\x00\x00\x00\x02\xfd\x65\x06\xda\xff\x41\x0a\x5e\x00\x0d\x00\ +\x1b\x00\x00\x01\x14\x16\x32\x36\x35\x34\x26\x27\x15\x23\x35\x0e\ +\x01\x37\x1e\x01\x15\x14\x06\x22\x26\x35\x34\x36\x37\x11\x33\xfd\ +\xaa\x63\x8c\x63\x4c\x3a\x46\x3a\x4c\xcc\x56\x75\x8c\xc4\x8c\x75\ +\x56\x46\x07\xc8\x46\x63\x63\x46\x3c\x5d\x0c\xa5\xa5\x0c\x5d\xaf\ +\x0c\x86\x59\x62\x8c\x8c\x62\x59\x86\x0c\x01\xab\x00\x00\x00\x00\ +\x04\x00\xa3\x00\xb4\x09\x9a\x05\xaf\x00\x9f\x00\xa7\x00\xb1\x00\ +\xc3\x00\x00\x13\x34\x3e\x03\x33\x32\x1e\x01\x15\x14\x0e\x02\x23\ +\x22\x26\x27\x26\x35\x34\x36\x33\x32\x17\x16\x33\x32\x36\x35\x34\ +\x2e\x02\x27\x06\x15\x14\x1e\x01\x15\x14\x06\x07\x1e\x04\x33\x32\ +\x37\x26\x35\x34\x36\x33\x32\x16\x15\x14\x06\x07\x1e\x01\x33\x32\ +\x37\x26\x35\x34\x36\x37\x26\x24\x27\x26\x35\x34\x36\x33\x32\x33\ +\x1e\x04\x15\x14\x0e\x03\x23\x22\x26\x27\x0e\x01\x23\x22\x2e\x02\ +\x27\x0e\x01\x23\x22\x2e\x02\x27\x0e\x01\x23\x22\x26\x35\x34\x3e\ +\x01\x37\x3e\x01\x35\x34\x2e\x01\x35\x34\x36\x37\x22\x23\x22\x06\ +\x15\x14\x16\x33\x32\x3e\x03\x37\x36\x33\x32\x16\x15\x16\x15\x10\ +\x23\x22\x26\x00\x34\x36\x32\x16\x14\x06\x22\x01\x36\x35\x34\x26\ +\x23\x22\x06\x15\x14\x01\x0e\x06\x15\x14\x16\x33\x32\x3e\x02\x35\ +\x34\xa3\x2a\x5c\x80\xbe\x6e\x7c\xe6\x8c\x1c\x33\x30\x21\x50\x89\ +\x24\x01\x1f\x0a\x03\x02\x3d\x61\x15\x18\x27\x41\x4f\x2c\x6c\x6e\ +\x6e\x55\x44\x3b\x5d\x34\x24\x15\x06\x0e\x4c\x1c\x9a\x7b\x4c\x5b\ +\x8e\x6f\x24\x60\x25\x4f\x69\x10\x85\x5d\x64\xfe\xd3\xbd\x0b\x17\ +\x0c\x02\x01\x82\xf5\xee\xb0\x6c\x25\x3c\x52\x54\x2e\x45\x6b\x21\ +\x47\xad\x36\x15\x35\x3f\x3e\x16\x4c\xa6\x27\x10\x20\x1e\x3d\x25\ +\x54\x97\x2c\x22\x1b\x66\x92\x45\x34\x39\x68\x68\x65\x41\x06\x07\ +\xbb\xda\x24\x2f\x13\x1d\x10\x0c\x05\x02\x01\x0d\x0e\x1a\x01\xb5\ +\x63\x71\x08\x27\x3d\x56\x3d\x3d\x56\xfb\xf7\xa2\x32\x28\x27\x38\ +\x02\x94\x17\x22\x18\x10\x08\x04\x01\x38\x2a\x1d\x2b\x24\x13\x03\ +\xc0\x36\x7f\x87\x6d\x46\x64\xa5\x5a\x3a\x4c\x22\x0b\x81\x5d\x02\ +\x02\x0b\x21\x03\x75\x31\x24\x2b\x4b\x35\x26\x0a\xbe\x51\x1b\x91\ +\x91\x1d\x1e\x8d\x54\x03\x24\x31\x30\x22\x3b\x50\x67\x8b\xc4\x4a\ +\x4c\x34\xab\x5c\x40\x51\x5c\x40\x4e\x7c\xd7\x27\x74\xa3\x21\x02\ +\x0e\x0f\x20\x15\x4e\x83\xa6\xe9\x86\x4d\x7a\x4c\x31\x14\x48\x43\ +\x3f\x4c\x10\x22\x41\x2b\x34\x57\x2c\x3d\x50\x1b\x5d\x77\x15\x1d\ +\x26\x63\x52\x15\x3f\x57\x1a\x0d\x7e\x99\x2f\x27\xea\x6c\xb0\xb5\ +\x4f\x47\x1e\x27\x41\x2d\x21\x08\x0f\x0b\x17\x13\xfe\xdc\x71\xfd\ +\xa7\x56\x3d\x3d\x56\x3d\x01\x3d\x9a\x53\x22\x2b\x6a\x52\x3f\x01\ +\x1b\x0c\x22\x22\x34\x29\x47\x30\x2c\x58\xa2\x15\x34\x62\x47\xb5\ +\x00\x00\x00\x00\x02\x00\xb4\x00\xef\x05\x2e\x05\x67\x00\x07\x00\ +\xc7\x00\x00\x00\x14\x16\x32\x36\x34\x26\x22\x37\x14\x16\x33\x32\ +\x37\x36\x35\x34\x26\x35\x34\x37\x36\x33\x32\x16\x15\x14\x07\x06\ +\x23\x22\x26\x23\x22\x07\x06\x15\x14\x16\x33\x32\x3e\x03\x33\x32\ +\x16\x14\x06\x23\x22\x2e\x03\x23\x22\x06\x15\x14\x17\x16\x33\x32\ +\x36\x33\x32\x17\x16\x15\x14\x06\x23\x22\x27\x26\x35\x34\x36\x35\ +\x34\x27\x26\x23\x22\x06\x15\x14\x1e\x03\x15\x14\x06\x22\x26\x35\ +\x34\x3e\x03\x35\x34\x26\x23\x22\x07\x06\x15\x14\x16\x15\x14\x07\ +\x06\x23\x22\x26\x35\x34\x37\x36\x33\x32\x16\x33\x32\x37\x36\x35\ +\x34\x26\x23\x22\x0e\x03\x23\x22\x26\x34\x36\x33\x32\x1e\x03\x33\ +\x32\x36\x35\x34\x27\x26\x23\x22\x06\x23\x22\x27\x26\x35\x34\x36\ +\x33\x32\x17\x16\x15\x14\x06\x15\x14\x17\x16\x33\x32\x36\x35\x34\ +\x2e\x03\x35\x34\x36\x32\x16\x15\x14\x0e\x03\x02\x83\x41\x5a\x40\ +\x40\x5a\x3f\x55\x22\x12\x08\x1b\x05\x27\x25\x2e\x35\x5e\x25\x26\ +\x36\x0d\x31\x0b\x28\x17\x0e\x2f\x17\x1a\x28\x1b\x1d\x31\x22\x3b\ +\x40\x40\x3b\x22\x32\x1e\x1b\x27\x19\x14\x2c\x08\x1b\x28\x0b\x2f\ +\x0d\x33\x27\x25\x5d\x36\x2e\x24\x28\x06\x1c\x10\x18\x24\x45\x18\ +\x23\x23\x18\x4f\x74\x4f\x19\x23\x23\x18\x45\x23\x19\x0f\x1d\x06\ +\x27\x24\x2e\x37\x5d\x25\x27\x34\x0c\x30\x0b\x28\x1b\x08\x2d\x13\ +\x19\x27\x1b\x1f\x32\x22\x3a\x40\x40\x3a\x22\x32\x1d\x1b\x27\x1b\ +\x17\x2e\x0d\x18\x27\x0b\x31\x0e\x35\x27\x24\x5d\x36\x2e\x25\x27\ +\x06\x1c\x08\x12\x22\x54\x18\x23\x23\x18\x4f\x73\x50\x19\x23\x22\ +\x19\x03\x58\x5a\x40\x40\x5a\x40\x87\x13\x2d\x08\x1b\x28\x0b\x30\ +\x0c\x34\x27\x25\x5d\x37\x2e\x24\x27\x06\x1d\x0f\x19\x23\x45\x18\ +\x23\x23\x19\x4f\x74\x4f\x19\x23\x23\x18\x54\x22\x12\x08\x1c\x06\ +\x27\x25\x2e\x36\x5d\x24\x27\x35\x0e\x31\x0b\x27\x18\x0d\x2e\x17\ +\x19\x27\x1b\x1e\x33\x22\x3a\x40\x40\x3a\x22\x32\x1d\x1b\x27\x1b\ +\x17\x2e\x0d\x18\x27\x0b\x31\x0e\x35\x27\x24\x5d\x36\x2e\x25\x27\ +\x06\x1c\x08\x12\x22\x54\x18\x23\x23\x19\x4f\x74\x4f\x19\x23\x23\ +\x18\x45\x23\x19\x0f\x1d\x06\x27\x24\x2e\x37\x5d\x25\x27\x34\x0c\ +\x30\x0b\x28\x1b\x08\x2d\x13\x1a\x28\x1b\x1d\x32\x22\x3a\x3f\x3f\ +\x3a\x22\x33\x1e\x1b\x27\x00\x00\x01\x00\xc8\x01\x5e\x0c\xd6\x04\ +\x77\x00\x0a\x00\x00\x09\x01\x21\x15\x21\x09\x01\x21\x35\x21\x01\ +\x06\xe6\x01\xa1\x04\x4f\xfb\x89\xfe\x70\xfe\x70\xfb\x89\x04\x4f\ +\x01\xa2\x04\x77\xfd\x2d\x46\x02\xb5\xfd\x4b\x46\x02\xd3\x00\x00\ +\x01\x00\xc8\x00\xe3\x05\x6c\x05\x9b\x00\x60\x00\x00\x01\x32\x3e\ +\x03\x33\x32\x16\x33\x32\x36\x35\x34\x26\x35\x34\x33\x32\x16\x15\ +\x14\x16\x15\x14\x23\x22\x26\x23\x22\x15\x14\x16\x15\x14\x06\x23\ +\x22\x26\x23\x22\x15\x14\x16\x15\x14\x06\x23\x22\x26\x23\x22\x15\ +\x14\x16\x15\x14\x06\x23\x22\x26\x23\x22\x0e\x03\x15\x14\x23\x22\ +\x35\x34\x3e\x02\x33\x32\x16\x33\x32\x3e\x03\x33\x32\x16\x33\x32\ +\x3e\x03\x33\x32\x16\x03\xc8\x28\x21\x06\x0c\x3b\x3b\x0c\x30\x0b\ +\x22\x26\x01\x15\x18\x12\x06\x85\x0b\x2a\x09\x5d\x07\x3a\x4c\x0b\ +\x2b\x09\x5c\x08\x39\x4c\x0c\x2b\x0a\x5b\x07\x3a\x4c\x0b\x2b\x09\ +\x18\x22\x10\x09\x01\x15\x2a\x19\x2b\x29\x17\x0d\x31\x0b\x28\x21\ +\x06\x0c\x3b\x3b\x0c\x30\x0b\x29\x22\x05\x0b\x3c\x3b\x0d\x31\x03\ +\xf9\x3b\x54\x53\x3b\x03\x22\x2c\x07\x1a\x07\x12\x21\x24\x0d\x37\ +\x0d\x65\x02\x64\x0f\x3b\x0e\x26\x39\x01\x64\x0f\x3b\x0e\x26\x39\ +\x02\x64\x0f\x3b\x0e\x26\x39\x01\x0d\x11\x1e\x13\x11\x12\x40\x33\ +\x46\x21\x0d\x04\x3b\x53\x54\x3b\x03\x3b\x53\x54\x3b\x04\x00\x00\ +\x01\x00\xbe\x00\xed\x05\x76\x05\x91\x00\x60\x00\x00\x01\x14\x1e\ +\x03\x15\x14\x06\x15\x14\x16\x33\x32\x36\x33\x32\x15\x14\x06\x23\ +\x22\x06\x23\x22\x35\x34\x36\x35\x34\x23\x22\x06\x23\x22\x26\x35\ +\x34\x36\x35\x34\x23\x22\x06\x23\x22\x26\x35\x34\x36\x35\x34\x23\ +\x22\x06\x23\x22\x26\x35\x34\x36\x35\x34\x2e\x03\x23\x22\x35\x34\ +\x33\x32\x1e\x02\x15\x14\x06\x15\x14\x1e\x03\x15\x14\x06\x15\x14\ +\x1e\x03\x15\x14\x06\x03\xd4\x3b\x53\x54\x3b\x03\x22\x2c\x06\x1c\ +\x06\x12\x21\x24\x0d\x37\x0d\x65\x02\x64\x0f\x3b\x0e\x26\x39\x01\ +\x64\x0f\x3b\x0e\x26\x39\x02\x64\x0f\x3b\x0e\x26\x39\x01\x0d\x11\ +\x1e\x13\x11\x12\x40\x33\x46\x21\x0d\x04\x3b\x54\x53\x3b\x03\x3b\ +\x54\x53\x3b\x04\x02\x91\x28\x21\x06\x0c\x3b\x3b\x0c\x30\x0b\x22\ +\x26\x01\x15\x18\x12\x06\x85\x0b\x2a\x09\x5d\x07\x3a\x4c\x0b\x2b\ +\x09\x5c\x08\x39\x4c\x0c\x2b\x0a\x5b\x07\x3a\x4c\x0b\x2b\x09\x18\ +\x22\x10\x09\x01\x15\x2a\x19\x2b\x29\x17\x0d\x31\x0b\x28\x21\x06\ +\x0c\x3b\x3b\x0c\x30\x0b\x29\x22\x05\x0b\x3c\x3b\x0d\x31\x00\x00\ +\x02\x00\xb4\x02\x34\x04\x04\x04\x2e\x00\x11\x00\x1d\x00\x00\x00\ +\x32\x1e\x03\x17\x07\x2e\x01\x22\x06\x07\x27\x3e\x03\x01\x2e\x03\ +\x22\x0e\x02\x07\x36\x20\x02\x17\x8a\x81\x59\x4f\x2a\x10\x34\x3d\ +\xc6\xe2\xc6\x3d\x34\x10\x2a\x4f\x59\x02\x06\x1c\x35\x4f\x66\x74\ +\x66\x4f\x35\x1c\x9a\x01\x4c\x04\x2e\x49\x67\x88\x66\x30\x2c\x35\ +\x3e\x3e\x35\x2c\x30\x66\x88\x67\xfe\xc2\x3c\x5a\x5f\x34\x34\x5f\ +\x5a\x3c\x5e\x00\x05\x00\xb4\x01\xf1\x03\x38\x04\x75\x00\x17\x00\ +\x1c\x00\x21\x00\x26\x00\x2b\x00\x00\x01\x2e\x01\x27\x23\x35\x33\ +\x3e\x01\x37\x35\x33\x15\x1e\x01\x17\x33\x15\x23\x0e\x01\x07\x15\ +\x23\x11\x0e\x01\x07\x3b\x01\x2e\x01\x27\x1d\x01\x3e\x01\x37\x23\ +\x07\x35\x23\x1e\x01\x01\xd3\x5c\x83\x0c\x34\x34\x0c\x83\x5c\x46\ +\x5c\x83\x0c\x34\x34\x0c\x83\x5c\x46\x3f\x5c\x0b\xa6\xec\x0b\x5c\ +\x3f\x3f\x5c\x0b\xa6\x46\xa6\x0b\x5c\x02\x25\x0c\x83\x5c\x46\x5c\ +\x83\x0c\x34\x34\x0c\x83\x5c\x46\x5c\x83\x0c\x34\x02\x0b\x0b\x5c\ +\x3f\x3f\x5c\x0b\xa6\xec\x0b\x5c\x3f\xa6\xa6\x3f\x5c\x00\x00\x00\ +\x07\x00\xb4\x01\x87\x04\x0c\x04\xdf\x00\x07\x00\x0f\x00\x27\x00\ +\x2c\x00\x31\x00\x36\x00\x3b\x00\x00\x12\x10\x16\x20\x36\x10\x26\ +\x20\x00\x10\x36\x20\x16\x10\x06\x20\x37\x2e\x01\x27\x23\x35\x33\ +\x3e\x01\x37\x35\x33\x15\x1e\x01\x17\x33\x15\x23\x0e\x01\x07\x15\ +\x23\x11\x0e\x01\x07\x3b\x01\x2e\x01\x27\x1d\x01\x3e\x01\x37\x23\ +\x07\x35\x23\x1e\x01\xf8\xd3\x01\x2a\xd3\xd3\xfe\xd6\xfe\xe9\xfb\ +\x01\x62\xfb\xfb\xfe\x9e\x8e\x5b\x84\x0c\x34\x34\x0c\x84\x5b\x46\ +\x5b\x84\x0c\x34\x34\x0c\x84\x5b\x46\x3f\x5c\x0b\xa6\xec\x0b\x5c\ +\x3f\x3f\x5c\x0b\xa6\x46\xa6\x0b\x5c\x03\xc8\xfe\xd6\xd3\xd3\x01\ +\x2a\xd3\xfd\xe7\x01\x62\xfb\xfb\xfe\x9e\xfb\x9e\x0c\x83\x5c\x46\ +\x5c\x83\x0c\x34\x34\x0c\x83\x5c\x46\x5c\x83\x0c\x34\x02\x0b\x0b\ +\x5c\x3f\x3f\x5c\x0b\xa6\xec\x0b\x5c\x3f\xa6\xa6\x3f\x5c\x00\x00\ +\x02\x00\xc9\xff\xc8\x05\x5a\x06\x66\x00\x0b\x00\x0f\x00\x00\x13\ +\x33\x15\x21\x11\x33\x11\x23\x35\x21\x15\x23\x13\x15\x21\x35\xc9\ +\x46\x04\x05\x46\x46\xfb\xfb\x46\x46\x04\x05\x01\xf4\x79\x04\xeb\ +\xf9\x62\x79\x79\x01\x3c\x4c\x4c\x00\x00\x00\x00\x02\x00\xc9\xff\ +\xc8\x03\x66\x06\x66\x00\x0b\x00\x0f\x00\x00\x13\x33\x15\x21\x11\ +\x33\x11\x23\x35\x21\x15\x23\x13\x15\x21\x35\xc9\x46\x02\x11\x46\ +\x46\xfd\xef\x46\x46\x02\x11\x01\xf4\x79\x04\xeb\xf9\x62\x79\x79\ +\x01\x3c\x4c\x4c\x00\x00\x00\x00\x02\x00\xc9\xff\xc8\x03\x66\x01\ +\xf4\x00\x0b\x00\x0f\x00\x00\x13\x33\x15\x21\x35\x33\x11\x23\x35\ +\x21\x15\x23\x13\x15\x21\x35\xc9\x46\x02\x11\x46\x46\xfd\xef\x46\ +\x46\x02\x11\x01\xf4\x79\x79\xfd\xd4\x79\x79\x01\x3c\x4c\x4c\x00\ +\x02\x00\xc9\x00\x00\x02\x85\x01\xbc\x00\x03\x00\x07\x00\x00\x01\ +\x07\x17\x37\x27\x17\x07\x27\x01\xa7\x7e\x7e\x7e\x7e\xde\xde\xde\ +\x01\x5c\x7e\x7e\x7e\xde\xde\xde\xde\x00\x00\x00\x01\x00\xc9\x00\ +\x00\x02\x85\x01\xbc\x00\x03\x00\x00\x01\x17\x07\x27\x01\xa7\xde\ +\xde\xde\x01\xbc\xde\xde\xde\x00\x02\x00\xc9\x00\x00\x02\x85\x06\ +\x66\x00\x03\x00\x0a\x00\x00\x01\x07\x17\x37\x27\x17\x07\x27\x37\ +\x11\x33\x01\xa7\x7e\x7e\x7e\x5b\xbb\xde\xde\xbb\x46\x01\x5c\x7e\ +\x7e\x7e\xbb\xbb\xde\xde\xbb\x04\xcd\x00\x00\x00\x01\x00\xc9\x00\ +\x00\x02\x85\x06\x66\x00\x06\x00\x00\x01\x17\x07\x27\x37\x11\x33\ +\x01\xca\xbb\xde\xde\xbb\x46\x01\x99\xbb\xde\xde\xbb\x04\xcd\x00\ +\x02\x00\xc9\x00\x00\x03\x95\x06\x66\x00\x22\x00\x26\x00\x00\x01\ +\x17\x07\x27\x37\x11\x33\x1e\x06\x15\x14\x06\x07\x06\x26\x37\x3e\ +\x04\x35\x34\x2e\x05\x27\x03\x07\x17\x37\x01\xca\xbb\xde\xde\xbb\ +\x46\x0f\x43\x52\x5e\x57\x47\x2b\x51\x67\x0c\x37\x0b\x20\x35\x2b\ +\x1e\x10\x26\x3f\x4c\x51\x46\x37\x0a\x23\x7e\x7e\x7e\x01\x99\xbb\ +\xde\xde\xbb\x04\xcd\x4a\x8b\x6c\x69\x63\x66\x79\x41\x81\xad\x79\ +\x0e\x1b\x0e\x2a\x45\x45\x44\x53\x30\x37\x5c\x43\x3c\x3a\x3d\x52\ +\x30\xfc\x29\x7e\x7e\x7e\x00\x00\x01\x00\xc9\x00\x00\x03\x95\x06\ +\x66\x00\x22\x00\x00\x01\x17\x07\x27\x37\x11\x33\x1e\x06\x15\x14\ +\x06\x07\x06\x26\x37\x3e\x04\x35\x34\x2e\x05\x27\x01\xca\xbb\xde\ +\xde\xbb\x46\x0f\x43\x52\x5e\x57\x47\x2b\x51\x67\x0c\x37\x0b\x20\ +\x35\x2b\x1e\x10\x26\x3f\x4c\x51\x46\x37\x0a\x01\x99\xbb\xde\xde\ +\xbb\x04\xcd\x4a\x8b\x6c\x69\x63\x66\x79\x41\x81\xad\x79\x0e\x1b\ +\x0e\x2a\x45\x45\x44\x53\x30\x37\x5c\x43\x3c\x3a\x3d\x52\x30\x00\ +\x03\x00\xc9\x00\x00\x03\x95\x06\x66\x00\x23\x00\x27\x00\x36\x00\ +\x00\x01\x17\x07\x27\x37\x11\x33\x1e\x06\x15\x14\x07\x16\x15\x14\ +\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x03\x27\x03\x07\x17\x37\ +\x03\x15\x1e\x04\x17\x36\x34\x2e\x04\x01\xca\xbb\xde\xde\xbb\x46\ +\x0c\x40\x52\x5e\x5a\x49\x2c\x21\x21\x4d\x6b\x0f\x37\x0e\x31\x38\ +\x31\x14\x49\x6b\x6f\x5a\x0c\x23\x7e\x7e\x7e\x5b\x0c\x40\x57\x59\ +\x63\x1f\x0b\x34\x50\x61\x56\x44\x01\x99\xbb\xde\xde\xbb\x04\xcd\ +\x36\x61\x4c\x4b\x4e\x55\x72\x41\x74\x46\x3f\x4f\x87\x93\x4e\x0b\ +\x1b\x0b\x27\x35\x4a\x59\x3d\x48\x5f\x32\x2d\x4c\x39\xfd\x48\x7e\ +\x7e\x7e\x04\xb0\xa2\x36\x5c\x47\x3c\x4a\x24\x2c\x7a\x5e\x39\x3a\ +\x30\x4e\x00\x00\x02\x00\xc9\x00\x00\x03\x95\x06\x66\x00\x23\x00\ +\x32\x00\x00\x01\x17\x07\x27\x37\x11\x33\x1e\x06\x15\x14\x07\x16\ +\x15\x14\x06\x07\x06\x26\x37\x3e\x03\x35\x34\x2e\x03\x27\x11\x15\ +\x1e\x04\x17\x36\x34\x2e\x04\x01\xca\xbb\xde\xde\xbb\x46\x0c\x40\ +\x52\x5e\x5a\x49\x2c\x21\x21\x4d\x6b\x0f\x37\x0e\x31\x38\x31\x14\ +\x49\x6b\x6f\x5a\x0c\x0c\x40\x57\x59\x63\x1f\x0b\x34\x50\x61\x56\ +\x44\x01\x99\xbb\xde\xde\xbb\x04\xcd\x36\x61\x4c\x4b\x4e\x55\x72\ +\x41\x74\x46\x3f\x4f\x87\x93\x4e\x0b\x1b\x0b\x27\x35\x4a\x59\x3d\ +\x48\x5f\x32\x2d\x4c\x39\x01\x7a\xa2\x36\x5c\x47\x3c\x4a\x24\x2c\ +\x7a\x5e\x39\x3a\x30\x4e\x00\x00\x01\x00\xc8\x01\x5c\x01\x0e\x06\ +\x98\x00\x03\x00\x00\x13\x11\x33\x11\xc8\x46\x01\x5c\x05\x3c\xfa\ +\xc4\x00\x00\x00\x01\x00\xc8\x01\x5c\x01\x0e\x05\x0a\x00\x03\x00\ +\x00\x13\x11\x33\x11\xc8\x46\x01\x5c\x03\xae\xfc\x52\x00\x00\x00\ +\x01\x00\xc8\x01\x5c\x01\x0e\x03\x7c\x00\x03\x00\x00\x13\x11\x33\ +\x11\xc8\x46\x01\x5c\x02\x20\xfd\xe0\x00\x00\x00\x01\x00\xc8\x02\ +\x6c\x01\x0e\x03\x7c\x00\x03\x00\x00\x13\x11\x33\x11\xc8\x46\x02\ +\x6c\x01\x10\xfe\xf0\x00\x00\x00\x01\x00\xc8\x01\x5c\x01\x0e\x02\ +\x6c\x00\x03\x00\x00\x13\x11\x33\x11\xc8\x46\x01\x5c\x01\x10\xfe\ +\xf0\x00\x00\x00\x01\x00\xc8\x01\x5c\x02\x0f\x02\xe6\x00\x05\x00\ +\x00\x01\x11\x23\x11\x21\x15\x01\x0e\x46\x01\x47\x02\xa0\xfe\xbc\ +\x01\x8a\x46\x00\x03\x00\xb4\x01\xdf\x03\x5c\x04\x87\x00\x07\x00\ +\x0f\x00\x17\x00\x00\x00\x34\x36\x32\x16\x14\x06\x22\x26\x14\x16\ +\x32\x36\x34\x26\x22\x02\x10\x36\x20\x16\x10\x06\x20\x01\xb8\x2f\ +\x42\x2f\x2f\x42\xeb\x9d\xde\x9d\x9d\xde\xe5\xc7\x01\x1a\xc7\xc7\ +\xfe\xe6\x03\x12\x42\x2f\x2f\x42\x2f\xbf\xde\x9d\x9d\xde\x9d\xfe\ +\x67\x01\x1a\xc7\xc7\xfe\xe6\xc7\x00\x00\x00\x00\x02\x00\xb4\x01\ +\xdf\x03\x5c\x04\x87\x00\x07\x00\x0f\x00\x00\x12\x14\x16\x32\x36\ +\x34\x26\x22\x02\x10\x36\x20\x16\x10\x06\x20\xfc\x9d\xde\x9d\x9d\ +\xde\xe5\xc7\x01\x1a\xc7\xc7\xfe\xe6\x03\xa2\xde\x9d\x9d\xde\x9d\ +\xfe\x67\x01\x1a\xc7\xc7\xfe\xe6\xc7\x00\x00\x00\x03\x00\xb4\x01\ +\x2b\x03\x5c\x05\x3b\x00\x0f\x00\x15\x00\x1b\x00\x00\x01\x15\x23\ +\x35\x2e\x01\x10\x36\x37\x35\x33\x15\x1e\x01\x10\x06\x03\x11\x3e\ +\x01\x34\x26\x27\x0e\x01\x14\x16\x17\x02\x2b\x46\x81\xb0\xb0\x81\ +\x46\x81\xb0\xb0\x81\x63\x86\x86\xa9\x63\x86\x86\x63\x01\xe1\xb6\ +\xb6\x0d\xc2\x01\x06\xc2\x0d\xb6\xb6\x0d\xc2\xfe\xfa\xc2\x02\x4f\ +\xfd\xec\x0d\x97\xcc\x97\x0d\x0d\x97\xcc\x97\x0d\x00\x00\x00\x00\ +\x02\x00\xb4\x01\xdf\x02\xf9\x04\x87\x00\x07\x00\x1b\x00\x00\x00\ +\x34\x36\x32\x16\x14\x06\x22\x04\x10\x36\x33\x32\x16\x17\x07\x26\ +\x22\x06\x14\x16\x32\x37\x17\x0e\x01\x23\x22\x01\xb8\x2f\x42\x2f\ +\x2f\x42\xfe\xcd\xc7\x8d\x45\x7c\x2f\x32\x4f\xde\x9d\x9d\xde\x4f\ +\x32\x2f\x7c\x45\x8d\x03\x12\x42\x2f\x2f\x42\x2f\x3d\x01\x1a\xc7\ +\x35\x2f\x33\x4f\x9d\xde\x9d\x4f\x33\x2f\x35\x00\x01\x00\xb4\x01\ +\xdf\x02\xf9\x04\x87\x00\x13\x00\x00\x12\x10\x36\x33\x32\x16\x17\ +\x07\x26\x22\x06\x14\x16\x32\x37\x17\x0e\x01\x23\x22\xb4\xc7\x8d\ +\x45\x7c\x2f\x32\x4f\xde\x9d\x9d\xde\x4f\x32\x2f\x7c\x45\x8d\x02\ +\xa6\x01\x1a\xc7\x35\x2f\x33\x4f\x9d\xde\x9d\x4f\x33\x2f\x35\x00\ +\x01\x00\xb4\x01\xdf\x02\xf8\x04\x87\x00\x13\x00\x00\x00\x10\x06\ +\x23\x22\x27\x37\x16\x33\x32\x36\x34\x26\x23\x22\x07\x27\x36\x33\ +\x32\x02\xf8\xc7\x8d\x8c\x64\x33\x4d\x70\x6f\x9d\x9d\x6f\x70\x4d\ +\x33\x64\x8c\x8d\x03\xc0\xfe\xe6\xc7\x64\x33\x4f\x9d\xde\x9d\x4f\ +\x33\x64\x00\x00\x02\x00\xb4\x01\x2b\x02\xf9\x05\x3b\x00\x15\x00\ +\x1b\x00\x00\x01\x33\x15\x16\x17\x07\x26\x27\x11\x36\x37\x17\x06\ +\x07\x15\x23\x35\x2e\x01\x10\x36\x37\x19\x01\x0e\x01\x14\x16\x01\ +\xe5\x46\x78\x55\x32\x41\x5a\x5a\x41\x32\x55\x78\x46\x81\xb0\xb0\ +\x81\x63\x86\x86\x05\x3b\xb6\x0c\x56\x33\x41\x0c\xfd\xec\x0c\x41\ +\x33\x56\x0c\xb6\xb6\x0d\xc2\x01\x06\xc2\x0d\xfd\xa4\x02\x14\x0d\ +\x97\xcc\x97\x00\x02\x00\xb4\x01\x2b\x02\xf8\x05\x3b\x00\x15\x00\ +\x1b\x00\x00\x01\x15\x1e\x01\x10\x06\x07\x15\x23\x35\x26\x27\x37\ +\x16\x17\x11\x06\x07\x27\x36\x37\x35\x13\x3e\x01\x34\x26\x27\x01\ +\xc7\x81\xb0\xb0\x81\x46\x77\x56\x33\x41\x59\x59\x41\x33\x56\x77\ +\x46\x63\x86\x86\x63\x05\x3b\xb6\x0d\xc1\xfe\xf8\xc1\x0d\xb6\xb6\ +\x0c\x56\x33\x41\x0c\x02\x14\x0c\x41\x33\x56\x0c\xb6\xfc\xee\x0d\ +\x97\xcc\x97\x0d\x00\x00\x00\x00\x02\x00\xb5\xff\xec\x03\x42\x01\ +\xd0\x00\x1b\x00\x1f\x00\x00\x13\x37\x17\x37\x27\x37\x17\x37\x17\ +\x07\x17\x37\x17\x07\x17\x07\x27\x07\x17\x07\x27\x07\x27\x37\x27\ +\x07\x27\x3f\x01\x07\x17\x37\xb5\x2f\xc3\x25\x6e\x2f\x6f\x6e\x2f\ +\x6f\x26\xc3\x2f\xc3\xc3\x2f\xc3\x26\x6f\x2f\x6e\x6f\x2f\x6e\x25\ +\xc3\x2f\xc3\x84\x26\x26\x25\x01\xa1\x2f\xc3\x25\x6f\x2f\x6e\x6e\ +\x2f\x6f\x25\xc3\x2f\xc3\xc3\x2f\xc3\x25\x6f\x2f\x6e\x6e\x2f\x6f\ +\x25\xc3\x2f\xc3\x26\x26\x26\x26\x00\x00\x00\x00\x01\x00\xc9\x02\ +\xaf\x01\xef\x05\x45\x00\x07\x00\x00\x01\x21\x11\x21\x11\x23\x15\ +\x33\x01\xef\xfe\xda\x01\x26\xe0\xe0\x02\xaf\x02\x96\xfe\xf8\x86\ +\x00\x00\x00\x00\x02\x00\xc9\x02\x46\x03\x7f\x05\x45\x00\x07\x00\ +\x0d\x00\x00\x01\x21\x11\x21\x11\x23\x15\x33\x05\x23\x11\x21\x11\ +\x23\x03\x7f\xfe\xda\x01\x26\xe0\xe0\xfe\x2a\xe0\x01\x26\x46\x02\ +\xaf\x02\x96\xfe\xf8\x86\x50\x01\x08\xfd\xd7\x00\x02\x00\xc9\x00\ +\xf5\x01\xef\x04\xb4\x00\x03\x00\x09\x00\x00\x01\x27\x15\x17\x03\ +\x17\x11\x25\x11\x33\x01\xa9\x9a\x9a\x9a\xe0\xfe\xda\x46\x01\xe9\ +\x27\x9c\x27\x01\x08\x38\xfe\xd8\x4b\x03\x74\x00\x01\x00\xc9\x00\ +\x8b\x01\xef\x03\xb7\x00\x05\x00\x00\x01\x23\x11\x21\x11\x23\x01\ +\xa9\xe0\x01\x26\x46\x02\xaf\x01\x08\xfc\xd4\x00\x01\x00\xca\x00\ +\xef\x01\xf0\x03\x85\x00\x07\x00\x00\x37\x11\x33\x35\x23\x11\x21\ +\x11\xca\xe0\xe0\x01\x26\xef\x01\x08\x86\x01\x08\xfd\x6a\x00\x00\ +\x02\x00\xc9\x00\x59\x02\xf7\x03\xe9\x00\x03\x00\x09\x00\x00\x01\ +\x21\x11\x21\x03\x11\x23\x11\x21\x11\x01\xef\x01\x08\xfe\xf8\xe0\ +\x46\x01\x26\x02\xe1\xfe\xf8\x01\x08\xfd\x78\x03\x90\xfe\xf8\x00\ +\x01\x00\xc9\x00\x4b\x02\xf7\x03\x85\x00\x0b\x00\x00\x01\x15\x21\ +\x11\x21\x35\x33\x35\x23\x11\x21\x11\x01\xd1\xfe\xf8\x01\x08\xe0\ +\xe0\x01\x26\x01\x21\xd6\x01\x08\xd6\x54\x01\x08\xfd\x9c\x00\x00\ +\x03\x00\xc9\x00\x7b\x04\x9f\x04\x6f\x00\x03\x00\x07\x00\x0d\x00\ +\x00\x01\x17\x07\x27\x03\x17\x07\x2f\x01\x23\x11\x21\x11\x23\x03\ +\xee\xb1\xb1\xb2\x43\xb2\xb2\xb2\x9e\xe0\x01\x26\x46\x02\xfa\xb2\ +\xb2\xb2\x01\x9d\xb2\xb2\xb2\x34\x01\x08\xfc\x0c\x00\x00\x00\x00\ +\x03\x00\xc9\x01\xd9\x03\xe1\x03\xe9\x00\x03\x00\x07\x00\x0b\x00\ +\x00\x01\x21\x11\x29\x02\x11\x21\x01\x21\x11\x21\x01\xd1\x01\x08\ +\xfe\xf8\x01\x08\x01\x08\xfe\xf8\xfd\xf0\x01\x08\xfe\xf8\x03\xe9\ +\xfe\xf8\xfe\xf8\x01\x08\xfe\xf8\x00\x00\x00\x00\x01\x00\xc9\x00\ +\x4b\x05\x46\x04\x70\x00\x14\x00\x00\x01\x11\x23\x11\x37\x16\x00\ +\x1e\x01\x17\x11\x23\x11\x21\x11\x23\x2e\x04\x01\x0f\x46\x46\xa2\ +\x01\x12\xf4\xd3\x76\xe0\x01\x26\x46\x65\xe0\xf1\xd4\xb5\x02\xfb\ +\xfd\x5e\x04\x16\x01\xd9\xfe\xd7\xb3\x58\x10\x01\x5c\x01\x08\xfc\ +\x94\x10\x48\x79\x99\xd2\x00\x00\x01\x00\xc9\xff\x93\x07\x24\x04\ +\x70\x00\x18\x00\x00\x01\x11\x21\x11\x33\x11\x21\x11\x23\x11\x23\ +\x2e\x04\x27\x11\x23\x11\x37\x16\x00\x1e\x01\x05\x00\x01\x44\xe0\ +\xfe\xda\xb8\x46\x65\xe0\xf1\xd4\xb5\x32\x46\x46\xa2\x01\x12\xf4\ +\xd3\x01\x53\x01\x2a\xfe\x1e\xfe\xf8\x01\xe2\xfe\xd6\x10\x48\x79\ +\x99\xd2\x74\xfd\x5e\x04\x16\x01\xd9\xfe\xd7\xb3\x58\x00\x00\x00\ +\x02\x00\xc9\x00\x59\x04\x7d\x04\x6f\x00\x07\x00\x11\x00\x00\x13\ +\x35\x33\x35\x23\x11\x21\x11\x13\x11\x23\x11\x21\x11\x33\x11\x21\ +\x11\xc9\xe0\xe0\x01\x26\xb0\x46\x01\x44\xe0\xfe\xda\x01\xd9\xe4\ +\x46\x01\x08\xfd\xce\x01\x8e\xfc\xf2\x04\x16\xfe\x72\xfe\xf8\x01\ +\x8e\x00\x00\x00\x01\x00\xca\x00\x4b\x06\x27\x04\x70\x00\x16\x00\ +\x00\x01\x37\x16\x00\x1e\x01\x17\x11\x23\x11\x21\x11\x23\x2e\x04\ +\x27\x11\x21\x11\x33\x01\xaa\x46\xa2\x01\x12\xf4\xd3\x76\xe0\x01\ +\x26\x46\x65\xe0\xf1\xd4\xb5\x32\xfe\xda\xe0\x04\x6f\x01\xd9\xfe\ +\xd7\xb3\x58\x10\x01\x5c\x01\x08\xfc\x94\x10\x48\x79\x99\xd2\x74\ +\xfd\x50\x01\x08\x00\x00\x00\x00\x03\x00\xc9\x00\xf2\x04\x9f\x04\ +\x11\x00\x03\x00\x07\x00\x0f\x00\x00\x01\x17\x07\x27\x03\x17\x07\ +\x27\x05\x11\x33\x35\x23\x11\x21\x11\x03\xee\xb1\xb1\xb2\x43\xb2\ +\xb2\xb2\xfe\x82\xe0\xe0\x01\x26\x02\x56\xb2\xb2\xb2\x01\x9b\xb2\ +\xb2\xb2\xd2\x01\x08\x46\x01\x08\xfd\xaa\x00\x00\x01\x00\xc7\x01\ +\x46\x04\x37\x05\x20\x00\x1d\x00\x00\x01\x32\x36\x37\x36\x16\x07\ +\x0e\x03\x22\x24\x27\x11\x23\x11\x07\x06\x26\x3f\x01\x11\x33\x11\ +\x3e\x01\x33\x32\x04\x03\x6c\x0d\x28\x62\x05\x33\x05\x4b\x92\x42\ +\x34\x18\xfe\xf8\x1f\x55\x54\x05\x32\x04\x87\x55\x42\x33\x0e\x16\ +\x01\x28\x03\x42\x24\x62\x05\x39\x05\x4b\x9a\x48\x2e\xd5\x13\xfe\ +\x2a\x01\xae\x56\x05\x39\x05\x8a\x01\xbf\xfe\x9b\x47\x30\xf0\x00\ +\x01\x00\xc7\x01\x46\x03\xa5\x05\x20\x00\x16\x00\x00\x01\x11\x33\ +\x11\x3e\x01\x33\x32\x00\x15\x14\x06\x23\x22\x24\x27\x11\x23\x11\ +\x07\x06\x26\x37\x01\x4f\x55\x42\x33\x0e\x1b\x01\x63\xc0\x0e\x0c\ +\xfe\xf8\x1f\x55\x54\x05\x32\x04\x03\x61\x01\xbf\xfe\x9b\x47\x30\ +\xfe\xdc\x0e\x0f\xbd\xd5\x13\xfe\x2a\x01\xae\x56\x05\x39\x05\x00\ +\x00\x00\x14\x00\xf6\x00\x01\x00\x00\x00\x00\x00\x00\x00\x58\x00\ +\xb2\x00\x01\x00\x00\x00\x00\x00\x01\x00\x07\x01\x1b\x00\x01\x00\ +\x00\x00\x00\x00\x02\x00\x06\x01\x31\x00\x01\x00\x00\x00\x00\x00\ +\x03\x00\x1f\x01\x78\x00\x01\x00\x00\x00\x00\x00\x04\x00\x07\x01\ +\xa8\x00\x01\x00\x00\x00\x00\x00\x05\x00\x0c\x01\xca\x00\x01\x00\ +\x00\x00\x00\x00\x06\x00\x07\x01\xe7\x00\x01\x00\x00\x00\x00\x00\ +\x09\x00\x0a\x02\x05\x00\x01\x00\x00\x00\x00\x00\x0d\x11\x25\x24\ +\x5c\x00\x01\x00\x00\x00\x00\x00\x0e\x00\x1a\x35\xb8\x00\x03\x00\ +\x01\x04\x09\x00\x00\x00\xb0\x00\x00\x00\x03\x00\x01\x04\x09\x00\ +\x01\x00\x0e\x01\x0b\x00\x03\x00\x01\x04\x09\x00\x02\x00\x0c\x01\ +\x23\x00\x03\x00\x01\x04\x09\x00\x03\x00\x3e\x01\x38\x00\x03\x00\ +\x01\x04\x09\x00\x04\x00\x0e\x01\x98\x00\x03\x00\x01\x04\x09\x00\ +\x05\x00\x18\x01\xb0\x00\x03\x00\x01\x04\x09\x00\x06\x00\x0e\x01\ +\xd7\x00\x03\x00\x01\x04\x09\x00\x09\x00\x14\x01\xef\x00\x03\x00\ +\x01\x04\x09\x00\x0d\x22\x4a\x02\x10\x00\x03\x00\x01\x04\x09\x00\ +\x0e\x00\x34\x35\x82\x00\x43\x00\x72\x00\x65\x00\x61\x00\x74\x00\ +\x65\x00\x64\x00\x20\x00\x62\x00\x79\x00\x20\x00\x42\x00\x65\x00\ +\x6e\x00\x20\x00\x4c\x00\x61\x00\x65\x00\x6e\x00\x65\x00\x6e\x00\ +\x20\x00\x28\x00\x62\x00\x65\x00\x6e\x00\x6c\x00\x61\x00\x65\x00\ +\x6e\x00\x65\x00\x6e\x00\x40\x00\x67\x00\x6d\x00\x61\x00\x69\x00\ +\x6c\x00\x2e\x00\x63\x00\x6f\x00\x6d\x00\x29\x00\x20\x00\x77\x00\ +\x69\x00\x74\x00\x68\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\x74\x00\ +\x46\x00\x6f\x00\x72\x00\x67\x00\x65\x00\x20\x00\x31\x00\x2e\x00\ +\x30\x00\x20\x00\x28\x00\x68\x00\x74\x00\x74\x00\x70\x00\x3a\x00\ +\x2f\x00\x2f\x00\x66\x00\x6f\x00\x6e\x00\x74\x00\x66\x00\x6f\x00\ +\x72\x00\x67\x00\x65\x00\x2e\x00\x73\x00\x66\x00\x2e\x00\x6e\x00\ +\x65\x00\x74\x00\x29\x00\x00\x43\x72\x65\x61\x74\x65\x64\x20\x62\ +\x79\x20\x42\x65\x6e\x20\x4c\x61\x65\x6e\x65\x6e\x20\x28\x62\x65\ +\x6e\x6c\x61\x65\x6e\x65\x6e\x40\x67\x6d\x61\x69\x6c\x2e\x63\x6f\ +\x6d\x29\x20\x77\x69\x74\x68\x20\x46\x6f\x6e\x74\x46\x6f\x72\x67\ +\x65\x20\x31\x2e\x30\x20\x28\x68\x74\x74\x70\x3a\x2f\x2f\x66\x6f\ +\x6e\x74\x66\x6f\x72\x67\x65\x2e\x73\x66\x2e\x6e\x65\x74\x29\x00\ +\x00\x45\x00\x75\x00\x74\x00\x65\x00\x72\x00\x70\x00\x65\x00\x00\ +\x45\x75\x74\x65\x72\x70\x65\x00\x00\x4d\x00\x65\x00\x64\x00\x69\ +\x00\x75\x00\x6d\x00\x00\x4d\x65\x64\x69\x75\x6d\x00\x00\x46\x00\ +\x6f\x00\x6e\x00\x74\x00\x46\x00\x6f\x00\x72\x00\x67\x00\x65\x00\ +\x20\x00\x3a\x00\x20\x00\x45\x00\x75\x00\x74\x00\x65\x00\x72\x00\ +\x70\x00\x65\x00\x20\x00\x3a\x00\x20\x00\x31\x00\x34\x00\x2d\x00\ +\x37\x00\x2d\x00\x32\x00\x30\x00\x30\x00\x38\x00\x00\x46\x6f\x6e\ +\x74\x46\x6f\x72\x67\x65\x20\x3a\x20\x45\x75\x74\x65\x72\x70\x65\ +\x20\x3a\x20\x31\x34\x2d\x37\x2d\x32\x30\x30\x38\x00\x00\x45\x00\ +\x75\x00\x74\x00\x65\x00\x72\x00\x70\x00\x65\x00\x00\x45\x75\x74\ +\x65\x72\x70\x65\x00\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\ +\x6f\x00\x6e\x00\x20\x00\x31\x00\x2e\x00\x31\x00\x20\x00\x00\x56\ +\x65\x72\x73\x69\x6f\x6e\x20\x31\x2e\x31\x20\x00\x00\x45\x00\x75\ +\x00\x74\x00\x65\x00\x72\x00\x70\x00\x65\x00\x00\x45\x75\x74\x65\ +\x72\x70\x65\x00\x00\x42\x00\x65\x00\x6e\x00\x20\x00\x4c\x00\x61\ +\x00\x65\x00\x6e\x00\x65\x00\x6e\x00\x00\x42\x65\x6e\x20\x4c\x61\ +\x65\x6e\x65\x6e\x00\x00\x43\x00\x6f\x00\x70\x00\x79\x00\x72\x00\ +\x69\x00\x67\x00\x68\x00\x74\x00\x20\x00\x28\x00\x63\x00\x29\x00\ +\x20\x00\x32\x00\x30\x00\x30\x00\x38\x00\x2c\x00\x20\x00\x42\x00\ +\x65\x00\x6e\x00\x20\x00\x4c\x00\x61\x00\x65\x00\x6e\x00\x65\x00\ +\x6e\x00\x20\x00\x28\x00\x62\x00\x65\x00\x6e\x00\x6c\x00\x61\x00\ +\x65\x00\x6e\x00\x65\x00\x6e\x00\x40\x00\x67\x00\x6d\x00\x61\x00\ +\x69\x00\x6c\x00\x2e\x00\x63\x00\x6f\x00\x6d\x00\x29\x00\x2c\x00\ +\x0a\x00\x77\x00\x69\x00\x74\x00\x68\x00\x20\x00\x52\x00\x65\x00\ +\x73\x00\x65\x00\x72\x00\x76\x00\x65\x00\x64\x00\x20\x00\x46\x00\ +\x6f\x00\x6e\x00\x74\x00\x20\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\ +\x20\x00\x45\x00\x75\x00\x74\x00\x65\x00\x72\x00\x70\x00\x65\x00\ +\x2e\x00\x0a\x00\x0a\x00\x54\x00\x68\x00\x69\x00\x73\x00\x20\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\x6f\x00\x66\x00\ +\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x20\x00\x69\x00\x73\x00\ +\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\x65\x00\ +\x64\x00\x20\x00\x75\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x20\x00\ +\x74\x00\x68\x00\x65\x00\x20\x00\x53\x00\x49\x00\x4c\x00\x20\x00\ +\x4f\x00\x70\x00\x65\x00\x6e\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\ +\x74\x00\x20\x00\x4c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\ +\x65\x00\x2c\x00\x20\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\ +\x6f\x00\x6e\x00\x20\x00\x31\x00\x2e\x00\x31\x00\x2e\x00\x0a\x00\ +\x54\x00\x68\x00\x69\x00\x73\x00\x20\x00\x6c\x00\x69\x00\x63\x00\ +\x65\x00\x6e\x00\x73\x00\x65\x00\x20\x00\x69\x00\x73\x00\x20\x00\ +\x63\x00\x6f\x00\x70\x00\x69\x00\x65\x00\x64\x00\x20\x00\x62\x00\ +\x65\x00\x6c\x00\x6f\x00\x77\x00\x2c\x00\x20\x00\x61\x00\x6e\x00\ +\x64\x00\x20\x00\x69\x00\x73\x00\x20\x00\x61\x00\x6c\x00\x73\x00\ +\x6f\x00\x20\x00\x61\x00\x76\x00\x61\x00\x69\x00\x6c\x00\x61\x00\ +\x62\x00\x6c\x00\x65\x00\x20\x00\x77\x00\x69\x00\x74\x00\x68\x00\ +\x20\x00\x61\x00\x20\x00\x46\x00\x41\x00\x51\x00\x20\x00\x61\x00\ +\x74\x00\x3a\x00\x0a\x00\x68\x00\x74\x00\x74\x00\x70\x00\x3a\x00\ +\x2f\x00\x2f\x00\x73\x00\x63\x00\x72\x00\x69\x00\x70\x00\x74\x00\ +\x73\x00\x2e\x00\x73\x00\x69\x00\x6c\x00\x2e\x00\x6f\x00\x72\x00\ +\x67\x00\x2f\x00\x4f\x00\x46\x00\x4c\x00\x0a\x00\x0a\x00\x0a\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x0a\x00\x53\x00\x49\x00\x4c\x00\x20\x00\ +\x4f\x00\x50\x00\x45\x00\x4e\x00\x20\x00\x46\x00\x4f\x00\x4e\x00\ +\x54\x00\x20\x00\x4c\x00\x49\x00\x43\x00\x45\x00\x4e\x00\x53\x00\ +\x45\x00\x20\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\x6f\x00\ +\x6e\x00\x20\x00\x31\x00\x2e\x00\x31\x00\x20\x00\x2d\x00\x20\x00\ +\x32\x00\x36\x00\x20\x00\x46\x00\x65\x00\x62\x00\x72\x00\x75\x00\ +\x61\x00\x72\x00\x79\x00\x20\x00\x32\x00\x30\x00\x30\x00\x37\x00\ +\x0a\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x2d\x00\ +\x2d\x00\x2d\x00\x2d\x00\x2d\x00\x0a\x00\x0a\x00\x50\x00\x52\x00\ +\x45\x00\x41\x00\x4d\x00\x42\x00\x4c\x00\x45\x00\x0a\x00\x54\x00\ +\x68\x00\x65\x00\x20\x00\x67\x00\x6f\x00\x61\x00\x6c\x00\x73\x00\ +\x20\x00\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\ +\x4f\x00\x70\x00\x65\x00\x6e\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\ +\x74\x00\x20\x00\x4c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\ +\x65\x00\x20\x00\x28\x00\x4f\x00\x46\x00\x4c\x00\x29\x00\x20\x00\ +\x61\x00\x72\x00\x65\x00\x20\x00\x74\x00\x6f\x00\x20\x00\x73\x00\ +\x74\x00\x69\x00\x6d\x00\x75\x00\x6c\x00\x61\x00\x74\x00\x65\x00\ +\x20\x00\x77\x00\x6f\x00\x72\x00\x6c\x00\x64\x00\x77\x00\x69\x00\ +\x64\x00\x65\x00\x0a\x00\x64\x00\x65\x00\x76\x00\x65\x00\x6c\x00\ +\x6f\x00\x70\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x20\x00\x6f\x00\ +\x66\x00\x20\x00\x63\x00\x6f\x00\x6c\x00\x6c\x00\x61\x00\x62\x00\ +\x6f\x00\x72\x00\x61\x00\x74\x00\x69\x00\x76\x00\x65\x00\x20\x00\ +\x66\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x70\x00\x72\x00\x6f\x00\ +\x6a\x00\x65\x00\x63\x00\x74\x00\x73\x00\x2c\x00\x20\x00\x74\x00\ +\x6f\x00\x20\x00\x73\x00\x75\x00\x70\x00\x70\x00\x6f\x00\x72\x00\ +\x74\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x66\x00\x6f\x00\ +\x6e\x00\x74\x00\x20\x00\x63\x00\x72\x00\x65\x00\x61\x00\x74\x00\ +\x69\x00\x6f\x00\x6e\x00\x0a\x00\x65\x00\x66\x00\x66\x00\x6f\x00\ +\x72\x00\x74\x00\x73\x00\x20\x00\x6f\x00\x66\x00\x20\x00\x61\x00\ +\x63\x00\x61\x00\x64\x00\x65\x00\x6d\x00\x69\x00\x63\x00\x20\x00\ +\x61\x00\x6e\x00\x64\x00\x20\x00\x6c\x00\x69\x00\x6e\x00\x67\x00\ +\x75\x00\x69\x00\x73\x00\x74\x00\x69\x00\x63\x00\x20\x00\x63\x00\ +\x6f\x00\x6d\x00\x6d\x00\x75\x00\x6e\x00\x69\x00\x74\x00\x69\x00\ +\x65\x00\x73\x00\x2c\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x20\x00\ +\x74\x00\x6f\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x69\x00\ +\x64\x00\x65\x00\x20\x00\x61\x00\x20\x00\x66\x00\x72\x00\x65\x00\ +\x65\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x0a\x00\x6f\x00\x70\x00\ +\x65\x00\x6e\x00\x20\x00\x66\x00\x72\x00\x61\x00\x6d\x00\x65\x00\ +\x77\x00\x6f\x00\x72\x00\x6b\x00\x20\x00\x69\x00\x6e\x00\x20\x00\ +\x77\x00\x68\x00\x69\x00\x63\x00\x68\x00\x20\x00\x66\x00\x6f\x00\ +\x6e\x00\x74\x00\x73\x00\x20\x00\x6d\x00\x61\x00\x79\x00\x20\x00\ +\x62\x00\x65\x00\x20\x00\x73\x00\x68\x00\x61\x00\x72\x00\x65\x00\ +\x64\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x20\x00\x69\x00\x6d\x00\ +\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x64\x00\x20\x00\x69\x00\ +\x6e\x00\x20\x00\x70\x00\x61\x00\x72\x00\x74\x00\x6e\x00\x65\x00\ +\x72\x00\x73\x00\x68\x00\x69\x00\x70\x00\x0a\x00\x77\x00\x69\x00\ +\x74\x00\x68\x00\x20\x00\x6f\x00\x74\x00\x68\x00\x65\x00\x72\x00\ +\x73\x00\x2e\x00\x0a\x00\x0a\x00\x54\x00\x68\x00\x65\x00\x20\x00\ +\x4f\x00\x46\x00\x4c\x00\x20\x00\x61\x00\x6c\x00\x6c\x00\x6f\x00\ +\x77\x00\x73\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x6c\x00\ +\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\x65\x00\x64\x00\x20\x00\ +\x66\x00\x6f\x00\x6e\x00\x74\x00\x73\x00\x20\x00\x74\x00\x6f\x00\ +\x20\x00\x62\x00\x65\x00\x20\x00\x75\x00\x73\x00\x65\x00\x64\x00\ +\x2c\x00\x20\x00\x73\x00\x74\x00\x75\x00\x64\x00\x69\x00\x65\x00\ +\x64\x00\x2c\x00\x20\x00\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\ +\x69\x00\x65\x00\x64\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x0a\x00\ +\x72\x00\x65\x00\x64\x00\x69\x00\x73\x00\x74\x00\x72\x00\x69\x00\ +\x62\x00\x75\x00\x74\x00\x65\x00\x64\x00\x20\x00\x66\x00\x72\x00\ +\x65\x00\x65\x00\x6c\x00\x79\x00\x20\x00\x61\x00\x73\x00\x20\x00\ +\x6c\x00\x6f\x00\x6e\x00\x67\x00\x20\x00\x61\x00\x73\x00\x20\x00\ +\x74\x00\x68\x00\x65\x00\x79\x00\x20\x00\x61\x00\x72\x00\x65\x00\ +\x20\x00\x6e\x00\x6f\x00\x74\x00\x20\x00\x73\x00\x6f\x00\x6c\x00\ +\x64\x00\x20\x00\x62\x00\x79\x00\x20\x00\x74\x00\x68\x00\x65\x00\ +\x6d\x00\x73\x00\x65\x00\x6c\x00\x76\x00\x65\x00\x73\x00\x2e\x00\ +\x20\x00\x54\x00\x68\x00\x65\x00\x0a\x00\x66\x00\x6f\x00\x6e\x00\ +\x74\x00\x73\x00\x2c\x00\x20\x00\x69\x00\x6e\x00\x63\x00\x6c\x00\ +\x75\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x20\x00\x61\x00\x6e\x00\ +\x79\x00\x20\x00\x64\x00\x65\x00\x72\x00\x69\x00\x76\x00\x61\x00\ +\x74\x00\x69\x00\x76\x00\x65\x00\x20\x00\x77\x00\x6f\x00\x72\x00\ +\x6b\x00\x73\x00\x2c\x00\x20\x00\x63\x00\x61\x00\x6e\x00\x20\x00\ +\x62\x00\x65\x00\x20\x00\x62\x00\x75\x00\x6e\x00\x64\x00\x6c\x00\ +\x65\x00\x64\x00\x2c\x00\x20\x00\x65\x00\x6d\x00\x62\x00\x65\x00\ +\x64\x00\x64\x00\x65\x00\x64\x00\x2c\x00\x20\x00\x0a\x00\x72\x00\ +\x65\x00\x64\x00\x69\x00\x73\x00\x74\x00\x72\x00\x69\x00\x62\x00\ +\x75\x00\x74\x00\x65\x00\x64\x00\x20\x00\x61\x00\x6e\x00\x64\x00\ +\x2f\x00\x6f\x00\x72\x00\x20\x00\x73\x00\x6f\x00\x6c\x00\x64\x00\ +\x20\x00\x77\x00\x69\x00\x74\x00\x68\x00\x20\x00\x61\x00\x6e\x00\ +\x79\x00\x20\x00\x73\x00\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\ +\x72\x00\x65\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x69\x00\ +\x64\x00\x65\x00\x64\x00\x20\x00\x74\x00\x68\x00\x61\x00\x74\x00\ +\x20\x00\x61\x00\x6e\x00\x79\x00\x20\x00\x72\x00\x65\x00\x73\x00\ +\x65\x00\x72\x00\x76\x00\x65\x00\x64\x00\x0a\x00\x6e\x00\x61\x00\ +\x6d\x00\x65\x00\x73\x00\x20\x00\x61\x00\x72\x00\x65\x00\x20\x00\ +\x6e\x00\x6f\x00\x74\x00\x20\x00\x75\x00\x73\x00\x65\x00\x64\x00\ +\x20\x00\x62\x00\x79\x00\x20\x00\x64\x00\x65\x00\x72\x00\x69\x00\ +\x76\x00\x61\x00\x74\x00\x69\x00\x76\x00\x65\x00\x20\x00\x77\x00\ +\x6f\x00\x72\x00\x6b\x00\x73\x00\x2e\x00\x20\x00\x54\x00\x68\x00\ +\x65\x00\x20\x00\x66\x00\x6f\x00\x6e\x00\x74\x00\x73\x00\x20\x00\ +\x61\x00\x6e\x00\x64\x00\x20\x00\x64\x00\x65\x00\x72\x00\x69\x00\ +\x76\x00\x61\x00\x74\x00\x69\x00\x76\x00\x65\x00\x73\x00\x2c\x00\ +\x0a\x00\x68\x00\x6f\x00\x77\x00\x65\x00\x76\x00\x65\x00\x72\x00\ +\x2c\x00\x20\x00\x63\x00\x61\x00\x6e\x00\x6e\x00\x6f\x00\x74\x00\ +\x20\x00\x62\x00\x65\x00\x20\x00\x72\x00\x65\x00\x6c\x00\x65\x00\ +\x61\x00\x73\x00\x65\x00\x64\x00\x20\x00\x75\x00\x6e\x00\x64\x00\ +\x65\x00\x72\x00\x20\x00\x61\x00\x6e\x00\x79\x00\x20\x00\x6f\x00\ +\x74\x00\x68\x00\x65\x00\x72\x00\x20\x00\x74\x00\x79\x00\x70\x00\ +\x65\x00\x20\x00\x6f\x00\x66\x00\x20\x00\x6c\x00\x69\x00\x63\x00\ +\x65\x00\x6e\x00\x73\x00\x65\x00\x2e\x00\x20\x00\x54\x00\x68\x00\ +\x65\x00\x0a\x00\x72\x00\x65\x00\x71\x00\x75\x00\x69\x00\x72\x00\ +\x65\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x20\x00\x66\x00\x6f\x00\ +\x72\x00\x20\x00\x66\x00\x6f\x00\x6e\x00\x74\x00\x73\x00\x20\x00\ +\x74\x00\x6f\x00\x20\x00\x72\x00\x65\x00\x6d\x00\x61\x00\x69\x00\ +\x6e\x00\x20\x00\x75\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x20\x00\ +\x74\x00\x68\x00\x69\x00\x73\x00\x20\x00\x6c\x00\x69\x00\x63\x00\ +\x65\x00\x6e\x00\x73\x00\x65\x00\x20\x00\x64\x00\x6f\x00\x65\x00\ +\x73\x00\x20\x00\x6e\x00\x6f\x00\x74\x00\x20\x00\x61\x00\x70\x00\ +\x70\x00\x6c\x00\x79\x00\x0a\x00\x74\x00\x6f\x00\x20\x00\x61\x00\ +\x6e\x00\x79\x00\x20\x00\x64\x00\x6f\x00\x63\x00\x75\x00\x6d\x00\ +\x65\x00\x6e\x00\x74\x00\x20\x00\x63\x00\x72\x00\x65\x00\x61\x00\ +\x74\x00\x65\x00\x64\x00\x20\x00\x75\x00\x73\x00\x69\x00\x6e\x00\ +\x67\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x66\x00\x6f\x00\ +\x6e\x00\x74\x00\x73\x00\x20\x00\x6f\x00\x72\x00\x20\x00\x74\x00\ +\x68\x00\x65\x00\x69\x00\x72\x00\x20\x00\x64\x00\x65\x00\x72\x00\ +\x69\x00\x76\x00\x61\x00\x74\x00\x69\x00\x76\x00\x65\x00\x73\x00\ +\x2e\x00\x0a\x00\x0a\x00\x44\x00\x45\x00\x46\x00\x49\x00\x4e\x00\ +\x49\x00\x54\x00\x49\x00\x4f\x00\x4e\x00\x53\x00\x0a\x00\x22\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\x6f\x00\x66\x00\ +\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x22\x00\x20\x00\x72\x00\ +\x65\x00\x66\x00\x65\x00\x72\x00\x73\x00\x20\x00\x74\x00\x6f\x00\ +\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x73\x00\x65\x00\x74\x00\ +\x20\x00\x6f\x00\x66\x00\x20\x00\x66\x00\x69\x00\x6c\x00\x65\x00\ +\x73\x00\x20\x00\x72\x00\x65\x00\x6c\x00\x65\x00\x61\x00\x73\x00\ +\x65\x00\x64\x00\x20\x00\x62\x00\x79\x00\x20\x00\x74\x00\x68\x00\ +\x65\x00\x20\x00\x43\x00\x6f\x00\x70\x00\x79\x00\x72\x00\x69\x00\ +\x67\x00\x68\x00\x74\x00\x0a\x00\x48\x00\x6f\x00\x6c\x00\x64\x00\ +\x65\x00\x72\x00\x28\x00\x73\x00\x29\x00\x20\x00\x75\x00\x6e\x00\ +\x64\x00\x65\x00\x72\x00\x20\x00\x74\x00\x68\x00\x69\x00\x73\x00\ +\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\x65\x00\ +\x20\x00\x61\x00\x6e\x00\x64\x00\x20\x00\x63\x00\x6c\x00\x65\x00\ +\x61\x00\x72\x00\x6c\x00\x79\x00\x20\x00\x6d\x00\x61\x00\x72\x00\ +\x6b\x00\x65\x00\x64\x00\x20\x00\x61\x00\x73\x00\x20\x00\x73\x00\ +\x75\x00\x63\x00\x68\x00\x2e\x00\x20\x00\x54\x00\x68\x00\x69\x00\ +\x73\x00\x20\x00\x6d\x00\x61\x00\x79\x00\x0a\x00\x69\x00\x6e\x00\ +\x63\x00\x6c\x00\x75\x00\x64\x00\x65\x00\x20\x00\x73\x00\x6f\x00\ +\x75\x00\x72\x00\x63\x00\x65\x00\x20\x00\x66\x00\x69\x00\x6c\x00\ +\x65\x00\x73\x00\x2c\x00\x20\x00\x62\x00\x75\x00\x69\x00\x6c\x00\ +\x64\x00\x20\x00\x73\x00\x63\x00\x72\x00\x69\x00\x70\x00\x74\x00\ +\x73\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x20\x00\x64\x00\x6f\x00\ +\x63\x00\x75\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x74\x00\ +\x69\x00\x6f\x00\x6e\x00\x2e\x00\x0a\x00\x0a\x00\x22\x00\x52\x00\ +\x65\x00\x73\x00\x65\x00\x72\x00\x76\x00\x65\x00\x64\x00\x20\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x4e\x00\x61\x00\x6d\x00\ +\x65\x00\x22\x00\x20\x00\x72\x00\x65\x00\x66\x00\x65\x00\x72\x00\ +\x73\x00\x20\x00\x74\x00\x6f\x00\x20\x00\x61\x00\x6e\x00\x79\x00\ +\x20\x00\x6e\x00\x61\x00\x6d\x00\x65\x00\x73\x00\x20\x00\x73\x00\ +\x70\x00\x65\x00\x63\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\ +\x20\x00\x61\x00\x73\x00\x20\x00\x73\x00\x75\x00\x63\x00\x68\x00\ +\x20\x00\x61\x00\x66\x00\x74\x00\x65\x00\x72\x00\x20\x00\x74\x00\ +\x68\x00\x65\x00\x0a\x00\x63\x00\x6f\x00\x70\x00\x79\x00\x72\x00\ +\x69\x00\x67\x00\x68\x00\x74\x00\x20\x00\x73\x00\x74\x00\x61\x00\ +\x74\x00\x65\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x28\x00\x73\x00\ +\x29\x00\x2e\x00\x0a\x00\x0a\x00\x22\x00\x4f\x00\x72\x00\x69\x00\ +\x67\x00\x69\x00\x6e\x00\x61\x00\x6c\x00\x20\x00\x56\x00\x65\x00\ +\x72\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x22\x00\x20\x00\x72\x00\ +\x65\x00\x66\x00\x65\x00\x72\x00\x73\x00\x20\x00\x74\x00\x6f\x00\ +\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x63\x00\x6f\x00\x6c\x00\ +\x6c\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\ +\x6f\x00\x66\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\ +\x53\x00\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\ +\x20\x00\x63\x00\x6f\x00\x6d\x00\x70\x00\x6f\x00\x6e\x00\x65\x00\ +\x6e\x00\x74\x00\x73\x00\x20\x00\x61\x00\x73\x00\x0a\x00\x64\x00\ +\x69\x00\x73\x00\x74\x00\x72\x00\x69\x00\x62\x00\x75\x00\x74\x00\ +\x65\x00\x64\x00\x20\x00\x62\x00\x79\x00\x20\x00\x74\x00\x68\x00\ +\x65\x00\x20\x00\x43\x00\x6f\x00\x70\x00\x79\x00\x72\x00\x69\x00\ +\x67\x00\x68\x00\x74\x00\x20\x00\x48\x00\x6f\x00\x6c\x00\x64\x00\ +\x65\x00\x72\x00\x28\x00\x73\x00\x29\x00\x2e\x00\x0a\x00\x0a\x00\ +\x22\x00\x4d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\ +\x64\x00\x20\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\x6f\x00\ +\x6e\x00\x22\x00\x20\x00\x72\x00\x65\x00\x66\x00\x65\x00\x72\x00\ +\x73\x00\x20\x00\x74\x00\x6f\x00\x20\x00\x61\x00\x6e\x00\x79\x00\ +\x20\x00\x64\x00\x65\x00\x72\x00\x69\x00\x76\x00\x61\x00\x74\x00\ +\x69\x00\x76\x00\x65\x00\x20\x00\x6d\x00\x61\x00\x64\x00\x65\x00\ +\x20\x00\x62\x00\x79\x00\x20\x00\x61\x00\x64\x00\x64\x00\x69\x00\ +\x6e\x00\x67\x00\x20\x00\x74\x00\x6f\x00\x2c\x00\x20\x00\x64\x00\ +\x65\x00\x6c\x00\x65\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x2c\x00\ +\x0a\x00\x6f\x00\x72\x00\x20\x00\x73\x00\x75\x00\x62\x00\x73\x00\ +\x74\x00\x69\x00\x74\x00\x75\x00\x74\x00\x69\x00\x6e\x00\x67\x00\ +\x20\x00\x2d\x00\x2d\x00\x20\x00\x69\x00\x6e\x00\x20\x00\x70\x00\ +\x61\x00\x72\x00\x74\x00\x20\x00\x6f\x00\x72\x00\x20\x00\x69\x00\ +\x6e\x00\x20\x00\x77\x00\x68\x00\x6f\x00\x6c\x00\x65\x00\x20\x00\ +\x2d\x00\x2d\x00\x20\x00\x61\x00\x6e\x00\x79\x00\x20\x00\x6f\x00\ +\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x63\x00\x6f\x00\ +\x6d\x00\x70\x00\x6f\x00\x6e\x00\x65\x00\x6e\x00\x74\x00\x73\x00\ +\x20\x00\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x0a\x00\ +\x4f\x00\x72\x00\x69\x00\x67\x00\x69\x00\x6e\x00\x61\x00\x6c\x00\ +\x20\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\ +\x2c\x00\x20\x00\x62\x00\x79\x00\x20\x00\x63\x00\x68\x00\x61\x00\ +\x6e\x00\x67\x00\x69\x00\x6e\x00\x67\x00\x20\x00\x66\x00\x6f\x00\ +\x72\x00\x6d\x00\x61\x00\x74\x00\x73\x00\x20\x00\x6f\x00\x72\x00\ +\x20\x00\x62\x00\x79\x00\x20\x00\x70\x00\x6f\x00\x72\x00\x74\x00\ +\x69\x00\x6e\x00\x67\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\x6f\x00\x66\x00\ +\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x20\x00\x74\x00\x6f\x00\ +\x20\x00\x61\x00\x0a\x00\x6e\x00\x65\x00\x77\x00\x20\x00\x65\x00\ +\x6e\x00\x76\x00\x69\x00\x72\x00\x6f\x00\x6e\x00\x6d\x00\x65\x00\ +\x6e\x00\x74\x00\x2e\x00\x0a\x00\x0a\x00\x22\x00\x41\x00\x75\x00\ +\x74\x00\x68\x00\x6f\x00\x72\x00\x22\x00\x20\x00\x72\x00\x65\x00\ +\x66\x00\x65\x00\x72\x00\x73\x00\x20\x00\x74\x00\x6f\x00\x20\x00\ +\x61\x00\x6e\x00\x79\x00\x20\x00\x64\x00\x65\x00\x73\x00\x69\x00\ +\x67\x00\x6e\x00\x65\x00\x72\x00\x2c\x00\x20\x00\x65\x00\x6e\x00\ +\x67\x00\x69\x00\x6e\x00\x65\x00\x65\x00\x72\x00\x2c\x00\x20\x00\ +\x70\x00\x72\x00\x6f\x00\x67\x00\x72\x00\x61\x00\x6d\x00\x6d\x00\ +\x65\x00\x72\x00\x2c\x00\x20\x00\x74\x00\x65\x00\x63\x00\x68\x00\ +\x6e\x00\x69\x00\x63\x00\x61\x00\x6c\x00\x0a\x00\x77\x00\x72\x00\ +\x69\x00\x74\x00\x65\x00\x72\x00\x20\x00\x6f\x00\x72\x00\x20\x00\ +\x6f\x00\x74\x00\x68\x00\x65\x00\x72\x00\x20\x00\x70\x00\x65\x00\ +\x72\x00\x73\x00\x6f\x00\x6e\x00\x20\x00\x77\x00\x68\x00\x6f\x00\ +\x20\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x69\x00\x62\x00\ +\x75\x00\x74\x00\x65\x00\x64\x00\x20\x00\x74\x00\x6f\x00\x20\x00\ +\x74\x00\x68\x00\x65\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\x74\x00\ +\x20\x00\x53\x00\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\x72\x00\ +\x65\x00\x2e\x00\x0a\x00\x0a\x00\x50\x00\x45\x00\x52\x00\x4d\x00\ +\x49\x00\x53\x00\x53\x00\x49\x00\x4f\x00\x4e\x00\x20\x00\x26\x00\ +\x20\x00\x43\x00\x4f\x00\x4e\x00\x44\x00\x49\x00\x54\x00\x49\x00\ +\x4f\x00\x4e\x00\x53\x00\x0a\x00\x50\x00\x65\x00\x72\x00\x6d\x00\ +\x69\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x69\x00\ +\x73\x00\x20\x00\x68\x00\x65\x00\x72\x00\x65\x00\x62\x00\x79\x00\ +\x20\x00\x67\x00\x72\x00\x61\x00\x6e\x00\x74\x00\x65\x00\x64\x00\ +\x2c\x00\x20\x00\x66\x00\x72\x00\x65\x00\x65\x00\x20\x00\x6f\x00\ +\x66\x00\x20\x00\x63\x00\x68\x00\x61\x00\x72\x00\x67\x00\x65\x00\ +\x2c\x00\x20\x00\x74\x00\x6f\x00\x20\x00\x61\x00\x6e\x00\x79\x00\ +\x20\x00\x70\x00\x65\x00\x72\x00\x73\x00\x6f\x00\x6e\x00\x20\x00\ +\x6f\x00\x62\x00\x74\x00\x61\x00\x69\x00\x6e\x00\x69\x00\x6e\x00\ +\x67\x00\x0a\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x70\x00\x79\x00\ +\x20\x00\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\x6f\x00\x66\x00\ +\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x2c\x00\x20\x00\x74\x00\ +\x6f\x00\x20\x00\x75\x00\x73\x00\x65\x00\x2c\x00\x20\x00\x73\x00\ +\x74\x00\x75\x00\x64\x00\x79\x00\x2c\x00\x20\x00\x63\x00\x6f\x00\ +\x70\x00\x79\x00\x2c\x00\x20\x00\x6d\x00\x65\x00\x72\x00\x67\x00\ +\x65\x00\x2c\x00\x20\x00\x65\x00\x6d\x00\x62\x00\x65\x00\x64\x00\ +\x2c\x00\x20\x00\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x79\x00\ +\x2c\x00\x0a\x00\x72\x00\x65\x00\x64\x00\x69\x00\x73\x00\x74\x00\ +\x72\x00\x69\x00\x62\x00\x75\x00\x74\x00\x65\x00\x2c\x00\x20\x00\ +\x61\x00\x6e\x00\x64\x00\x20\x00\x73\x00\x65\x00\x6c\x00\x6c\x00\ +\x20\x00\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\ +\x64\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x20\x00\x75\x00\x6e\x00\ +\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\ +\x20\x00\x63\x00\x6f\x00\x70\x00\x69\x00\x65\x00\x73\x00\x20\x00\ +\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x46\x00\ +\x6f\x00\x6e\x00\x74\x00\x0a\x00\x53\x00\x6f\x00\x66\x00\x74\x00\ +\x77\x00\x61\x00\x72\x00\x65\x00\x2c\x00\x20\x00\x73\x00\x75\x00\ +\x62\x00\x6a\x00\x65\x00\x63\x00\x74\x00\x20\x00\x74\x00\x6f\x00\ +\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x66\x00\x6f\x00\x6c\x00\ +\x6c\x00\x6f\x00\x77\x00\x69\x00\x6e\x00\x67\x00\x20\x00\x63\x00\ +\x6f\x00\x6e\x00\x64\x00\x69\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\ +\x73\x00\x3a\x00\x0a\x00\x0a\x00\x31\x00\x29\x00\x20\x00\x4e\x00\ +\x65\x00\x69\x00\x74\x00\x68\x00\x65\x00\x72\x00\x20\x00\x74\x00\ +\x68\x00\x65\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\ +\x53\x00\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\ +\x20\x00\x6e\x00\x6f\x00\x72\x00\x20\x00\x61\x00\x6e\x00\x79\x00\ +\x20\x00\x6f\x00\x66\x00\x20\x00\x69\x00\x74\x00\x73\x00\x20\x00\ +\x69\x00\x6e\x00\x64\x00\x69\x00\x76\x00\x69\x00\x64\x00\x75\x00\ +\x61\x00\x6c\x00\x20\x00\x63\x00\x6f\x00\x6d\x00\x70\x00\x6f\x00\ +\x6e\x00\x65\x00\x6e\x00\x74\x00\x73\x00\x2c\x00\x0a\x00\x69\x00\ +\x6e\x00\x20\x00\x4f\x00\x72\x00\x69\x00\x67\x00\x69\x00\x6e\x00\ +\x61\x00\x6c\x00\x20\x00\x6f\x00\x72\x00\x20\x00\x4d\x00\x6f\x00\ +\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\x20\x00\x56\x00\ +\x65\x00\x72\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x73\x00\x2c\x00\ +\x20\x00\x6d\x00\x61\x00\x79\x00\x20\x00\x62\x00\x65\x00\x20\x00\ +\x73\x00\x6f\x00\x6c\x00\x64\x00\x20\x00\x62\x00\x79\x00\x20\x00\ +\x69\x00\x74\x00\x73\x00\x65\x00\x6c\x00\x66\x00\x2e\x00\x0a\x00\ +\x0a\x00\x32\x00\x29\x00\x20\x00\x4f\x00\x72\x00\x69\x00\x67\x00\ +\x69\x00\x6e\x00\x61\x00\x6c\x00\x20\x00\x6f\x00\x72\x00\x20\x00\ +\x4d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\ +\x20\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\ +\x73\x00\x20\x00\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\ +\x20\x00\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\x6f\x00\ +\x66\x00\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x20\x00\x6d\x00\ +\x61\x00\x79\x00\x20\x00\x62\x00\x65\x00\x20\x00\x62\x00\x75\x00\ +\x6e\x00\x64\x00\x6c\x00\x65\x00\x64\x00\x2c\x00\x0a\x00\x72\x00\ +\x65\x00\x64\x00\x69\x00\x73\x00\x74\x00\x72\x00\x69\x00\x62\x00\ +\x75\x00\x74\x00\x65\x00\x64\x00\x20\x00\x61\x00\x6e\x00\x64\x00\ +\x2f\x00\x6f\x00\x72\x00\x20\x00\x73\x00\x6f\x00\x6c\x00\x64\x00\ +\x20\x00\x77\x00\x69\x00\x74\x00\x68\x00\x20\x00\x61\x00\x6e\x00\ +\x79\x00\x20\x00\x73\x00\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\ +\x72\x00\x65\x00\x2c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\ +\x69\x00\x64\x00\x65\x00\x64\x00\x20\x00\x74\x00\x68\x00\x61\x00\ +\x74\x00\x20\x00\x65\x00\x61\x00\x63\x00\x68\x00\x20\x00\x63\x00\ +\x6f\x00\x70\x00\x79\x00\x0a\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\ +\x61\x00\x69\x00\x6e\x00\x73\x00\x20\x00\x74\x00\x68\x00\x65\x00\ +\x20\x00\x61\x00\x62\x00\x6f\x00\x76\x00\x65\x00\x20\x00\x63\x00\ +\x6f\x00\x70\x00\x79\x00\x72\x00\x69\x00\x67\x00\x68\x00\x74\x00\ +\x20\x00\x6e\x00\x6f\x00\x74\x00\x69\x00\x63\x00\x65\x00\x20\x00\ +\x61\x00\x6e\x00\x64\x00\x20\x00\x74\x00\x68\x00\x69\x00\x73\x00\ +\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\x65\x00\ +\x2e\x00\x20\x00\x54\x00\x68\x00\x65\x00\x73\x00\x65\x00\x20\x00\ +\x63\x00\x61\x00\x6e\x00\x20\x00\x62\x00\x65\x00\x0a\x00\x69\x00\ +\x6e\x00\x63\x00\x6c\x00\x75\x00\x64\x00\x65\x00\x64\x00\x20\x00\ +\x65\x00\x69\x00\x74\x00\x68\x00\x65\x00\x72\x00\x20\x00\x61\x00\ +\x73\x00\x20\x00\x73\x00\x74\x00\x61\x00\x6e\x00\x64\x00\x2d\x00\ +\x61\x00\x6c\x00\x6f\x00\x6e\x00\x65\x00\x20\x00\x74\x00\x65\x00\ +\x78\x00\x74\x00\x20\x00\x66\x00\x69\x00\x6c\x00\x65\x00\x73\x00\ +\x2c\x00\x20\x00\x68\x00\x75\x00\x6d\x00\x61\x00\x6e\x00\x2d\x00\ +\x72\x00\x65\x00\x61\x00\x64\x00\x61\x00\x62\x00\x6c\x00\x65\x00\ +\x20\x00\x68\x00\x65\x00\x61\x00\x64\x00\x65\x00\x72\x00\x73\x00\ +\x20\x00\x6f\x00\x72\x00\x0a\x00\x69\x00\x6e\x00\x20\x00\x74\x00\ +\x68\x00\x65\x00\x20\x00\x61\x00\x70\x00\x70\x00\x72\x00\x6f\x00\ +\x70\x00\x72\x00\x69\x00\x61\x00\x74\x00\x65\x00\x20\x00\x6d\x00\ +\x61\x00\x63\x00\x68\x00\x69\x00\x6e\x00\x65\x00\x2d\x00\x72\x00\ +\x65\x00\x61\x00\x64\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x20\x00\ +\x6d\x00\x65\x00\x74\x00\x61\x00\x64\x00\x61\x00\x74\x00\x61\x00\ +\x20\x00\x66\x00\x69\x00\x65\x00\x6c\x00\x64\x00\x73\x00\x20\x00\ +\x77\x00\x69\x00\x74\x00\x68\x00\x69\x00\x6e\x00\x20\x00\x74\x00\ +\x65\x00\x78\x00\x74\x00\x20\x00\x6f\x00\x72\x00\x0a\x00\x62\x00\ +\x69\x00\x6e\x00\x61\x00\x72\x00\x79\x00\x20\x00\x66\x00\x69\x00\ +\x6c\x00\x65\x00\x73\x00\x20\x00\x61\x00\x73\x00\x20\x00\x6c\x00\ +\x6f\x00\x6e\x00\x67\x00\x20\x00\x61\x00\x73\x00\x20\x00\x74\x00\ +\x68\x00\x6f\x00\x73\x00\x65\x00\x20\x00\x66\x00\x69\x00\x65\x00\ +\x6c\x00\x64\x00\x73\x00\x20\x00\x63\x00\x61\x00\x6e\x00\x20\x00\ +\x62\x00\x65\x00\x20\x00\x65\x00\x61\x00\x73\x00\x69\x00\x6c\x00\ +\x79\x00\x20\x00\x76\x00\x69\x00\x65\x00\x77\x00\x65\x00\x64\x00\ +\x20\x00\x62\x00\x79\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\ +\x75\x00\x73\x00\x65\x00\x72\x00\x2e\x00\x0a\x00\x0a\x00\x33\x00\ +\x29\x00\x20\x00\x4e\x00\x6f\x00\x20\x00\x4d\x00\x6f\x00\x64\x00\ +\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\x20\x00\x56\x00\x65\x00\ +\x72\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x6f\x00\x66\x00\ +\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\ +\x74\x00\x20\x00\x53\x00\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\ +\x72\x00\x65\x00\x20\x00\x6d\x00\x61\x00\x79\x00\x20\x00\x75\x00\ +\x73\x00\x65\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x52\x00\ +\x65\x00\x73\x00\x65\x00\x72\x00\x76\x00\x65\x00\x64\x00\x20\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x0a\x00\x4e\x00\x61\x00\x6d\x00\ +\x65\x00\x28\x00\x73\x00\x29\x00\x20\x00\x75\x00\x6e\x00\x6c\x00\ +\x65\x00\x73\x00\x73\x00\x20\x00\x65\x00\x78\x00\x70\x00\x6c\x00\ +\x69\x00\x63\x00\x69\x00\x74\x00\x20\x00\x77\x00\x72\x00\x69\x00\ +\x74\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x70\x00\x65\x00\x72\x00\ +\x6d\x00\x69\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\ +\x69\x00\x73\x00\x20\x00\x67\x00\x72\x00\x61\x00\x6e\x00\x74\x00\ +\x65\x00\x64\x00\x20\x00\x62\x00\x79\x00\x20\x00\x74\x00\x68\x00\ +\x65\x00\x20\x00\x63\x00\x6f\x00\x72\x00\x72\x00\x65\x00\x73\x00\ +\x70\x00\x6f\x00\x6e\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x0a\x00\ +\x43\x00\x6f\x00\x70\x00\x79\x00\x72\x00\x69\x00\x67\x00\x68\x00\ +\x74\x00\x20\x00\x48\x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\ +\x2e\x00\x20\x00\x54\x00\x68\x00\x69\x00\x73\x00\x20\x00\x72\x00\ +\x65\x00\x73\x00\x74\x00\x72\x00\x69\x00\x63\x00\x74\x00\x69\x00\ +\x6f\x00\x6e\x00\x20\x00\x6f\x00\x6e\x00\x6c\x00\x79\x00\x20\x00\ +\x61\x00\x70\x00\x70\x00\x6c\x00\x69\x00\x65\x00\x73\x00\x20\x00\ +\x74\x00\x6f\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x70\x00\ +\x72\x00\x69\x00\x6d\x00\x61\x00\x72\x00\x79\x00\x20\x00\x66\x00\ +\x6f\x00\x6e\x00\x74\x00\x20\x00\x6e\x00\x61\x00\x6d\x00\x65\x00\ +\x20\x00\x61\x00\x73\x00\x0a\x00\x70\x00\x72\x00\x65\x00\x73\x00\ +\x65\x00\x6e\x00\x74\x00\x65\x00\x64\x00\x20\x00\x74\x00\x6f\x00\ +\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x75\x00\x73\x00\x65\x00\ +\x72\x00\x73\x00\x2e\x00\x0a\x00\x0a\x00\x34\x00\x29\x00\x20\x00\ +\x54\x00\x68\x00\x65\x00\x20\x00\x6e\x00\x61\x00\x6d\x00\x65\x00\ +\x28\x00\x73\x00\x29\x00\x20\x00\x6f\x00\x66\x00\x20\x00\x74\x00\ +\x68\x00\x65\x00\x20\x00\x43\x00\x6f\x00\x70\x00\x79\x00\x72\x00\ +\x69\x00\x67\x00\x68\x00\x74\x00\x20\x00\x48\x00\x6f\x00\x6c\x00\ +\x64\x00\x65\x00\x72\x00\x28\x00\x73\x00\x29\x00\x20\x00\x6f\x00\ +\x72\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x41\x00\x75\x00\ +\x74\x00\x68\x00\x6f\x00\x72\x00\x28\x00\x73\x00\x29\x00\x20\x00\ +\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x46\x00\ +\x6f\x00\x6e\x00\x74\x00\x0a\x00\x53\x00\x6f\x00\x66\x00\x74\x00\ +\x77\x00\x61\x00\x72\x00\x65\x00\x20\x00\x73\x00\x68\x00\x61\x00\ +\x6c\x00\x6c\x00\x20\x00\x6e\x00\x6f\x00\x74\x00\x20\x00\x62\x00\ +\x65\x00\x20\x00\x75\x00\x73\x00\x65\x00\x64\x00\x20\x00\x74\x00\ +\x6f\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x6d\x00\x6f\x00\x74\x00\ +\x65\x00\x2c\x00\x20\x00\x65\x00\x6e\x00\x64\x00\x6f\x00\x72\x00\ +\x73\x00\x65\x00\x20\x00\x6f\x00\x72\x00\x20\x00\x61\x00\x64\x00\ +\x76\x00\x65\x00\x72\x00\x74\x00\x69\x00\x73\x00\x65\x00\x20\x00\ +\x61\x00\x6e\x00\x79\x00\x0a\x00\x4d\x00\x6f\x00\x64\x00\x69\x00\ +\x66\x00\x69\x00\x65\x00\x64\x00\x20\x00\x56\x00\x65\x00\x72\x00\ +\x73\x00\x69\x00\x6f\x00\x6e\x00\x2c\x00\x20\x00\x65\x00\x78\x00\ +\x63\x00\x65\x00\x70\x00\x74\x00\x20\x00\x74\x00\x6f\x00\x20\x00\ +\x61\x00\x63\x00\x6b\x00\x6e\x00\x6f\x00\x77\x00\x6c\x00\x65\x00\ +\x64\x00\x67\x00\x65\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\ +\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x69\x00\x62\x00\x75\x00\ +\x74\x00\x69\x00\x6f\x00\x6e\x00\x28\x00\x73\x00\x29\x00\x20\x00\ +\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x0a\x00\x43\x00\ +\x6f\x00\x70\x00\x79\x00\x72\x00\x69\x00\x67\x00\x68\x00\x74\x00\ +\x20\x00\x48\x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\x28\x00\ +\x73\x00\x29\x00\x20\x00\x61\x00\x6e\x00\x64\x00\x20\x00\x74\x00\ +\x68\x00\x65\x00\x20\x00\x41\x00\x75\x00\x74\x00\x68\x00\x6f\x00\ +\x72\x00\x28\x00\x73\x00\x29\x00\x20\x00\x6f\x00\x72\x00\x20\x00\ +\x77\x00\x69\x00\x74\x00\x68\x00\x20\x00\x74\x00\x68\x00\x65\x00\ +\x69\x00\x72\x00\x20\x00\x65\x00\x78\x00\x70\x00\x6c\x00\x69\x00\ +\x63\x00\x69\x00\x74\x00\x20\x00\x77\x00\x72\x00\x69\x00\x74\x00\ +\x74\x00\x65\x00\x6e\x00\x0a\x00\x70\x00\x65\x00\x72\x00\x6d\x00\ +\x69\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x0a\x00\ +\x0a\x00\x35\x00\x29\x00\x20\x00\x54\x00\x68\x00\x65\x00\x20\x00\ +\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\x6f\x00\x66\x00\ +\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x2c\x00\x20\x00\x6d\x00\ +\x6f\x00\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\x20\x00\ +\x6f\x00\x72\x00\x20\x00\x75\x00\x6e\x00\x6d\x00\x6f\x00\x64\x00\ +\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\x2c\x00\x20\x00\x69\x00\ +\x6e\x00\x20\x00\x70\x00\x61\x00\x72\x00\x74\x00\x20\x00\x6f\x00\ +\x72\x00\x20\x00\x69\x00\x6e\x00\x20\x00\x77\x00\x68\x00\x6f\x00\ +\x6c\x00\x65\x00\x2c\x00\x0a\x00\x6d\x00\x75\x00\x73\x00\x74\x00\ +\x20\x00\x62\x00\x65\x00\x20\x00\x64\x00\x69\x00\x73\x00\x74\x00\ +\x72\x00\x69\x00\x62\x00\x75\x00\x74\x00\x65\x00\x64\x00\x20\x00\ +\x65\x00\x6e\x00\x74\x00\x69\x00\x72\x00\x65\x00\x6c\x00\x79\x00\ +\x20\x00\x75\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x20\x00\x74\x00\ +\x68\x00\x69\x00\x73\x00\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\ +\x6e\x00\x73\x00\x65\x00\x2c\x00\x20\x00\x61\x00\x6e\x00\x64\x00\ +\x20\x00\x6d\x00\x75\x00\x73\x00\x74\x00\x20\x00\x6e\x00\x6f\x00\ +\x74\x00\x20\x00\x62\x00\x65\x00\x0a\x00\x64\x00\x69\x00\x73\x00\ +\x74\x00\x72\x00\x69\x00\x62\x00\x75\x00\x74\x00\x65\x00\x64\x00\ +\x20\x00\x75\x00\x6e\x00\x64\x00\x65\x00\x72\x00\x20\x00\x61\x00\ +\x6e\x00\x79\x00\x20\x00\x6f\x00\x74\x00\x68\x00\x65\x00\x72\x00\ +\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\x65\x00\ +\x2e\x00\x20\x00\x54\x00\x68\x00\x65\x00\x20\x00\x72\x00\x65\x00\ +\x71\x00\x75\x00\x69\x00\x72\x00\x65\x00\x6d\x00\x65\x00\x6e\x00\ +\x74\x00\x20\x00\x66\x00\x6f\x00\x72\x00\x20\x00\x66\x00\x6f\x00\ +\x6e\x00\x74\x00\x73\x00\x20\x00\x74\x00\x6f\x00\x0a\x00\x72\x00\ +\x65\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\x20\x00\x75\x00\x6e\x00\ +\x64\x00\x65\x00\x72\x00\x20\x00\x74\x00\x68\x00\x69\x00\x73\x00\ +\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\x73\x00\x65\x00\ +\x20\x00\x64\x00\x6f\x00\x65\x00\x73\x00\x20\x00\x6e\x00\x6f\x00\ +\x74\x00\x20\x00\x61\x00\x70\x00\x70\x00\x6c\x00\x79\x00\x20\x00\ +\x74\x00\x6f\x00\x20\x00\x61\x00\x6e\x00\x79\x00\x20\x00\x64\x00\ +\x6f\x00\x63\x00\x75\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x20\x00\ +\x63\x00\x72\x00\x65\x00\x61\x00\x74\x00\x65\x00\x64\x00\x0a\x00\ +\x75\x00\x73\x00\x69\x00\x6e\x00\x67\x00\x20\x00\x74\x00\x68\x00\ +\x65\x00\x20\x00\x46\x00\x6f\x00\x6e\x00\x74\x00\x20\x00\x53\x00\ +\x6f\x00\x66\x00\x74\x00\x77\x00\x61\x00\x72\x00\x65\x00\x2e\x00\ +\x0a\x00\x0a\x00\x54\x00\x45\x00\x52\x00\x4d\x00\x49\x00\x4e\x00\ +\x41\x00\x54\x00\x49\x00\x4f\x00\x4e\x00\x0a\x00\x54\x00\x68\x00\ +\x69\x00\x73\x00\x20\x00\x6c\x00\x69\x00\x63\x00\x65\x00\x6e\x00\ +\x73\x00\x65\x00\x20\x00\x62\x00\x65\x00\x63\x00\x6f\x00\x6d\x00\ +\x65\x00\x73\x00\x20\x00\x6e\x00\x75\x00\x6c\x00\x6c\x00\x20\x00\ +\x61\x00\x6e\x00\x64\x00\x20\x00\x76\x00\x6f\x00\x69\x00\x64\x00\ +\x20\x00\x69\x00\x66\x00\x20\x00\x61\x00\x6e\x00\x79\x00\x20\x00\ +\x6f\x00\x66\x00\x20\x00\x74\x00\x68\x00\x65\x00\x20\x00\x61\x00\ +\x62\x00\x6f\x00\x76\x00\x65\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\ +\x64\x00\x69\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\x00\x20\x00\ +\x61\x00\x72\x00\x65\x00\x0a\x00\x6e\x00\x6f\x00\x74\x00\x20\x00\ +\x6d\x00\x65\x00\x74\x00\x2e\x00\x0a\x00\x0a\x00\x44\x00\x49\x00\ +\x53\x00\x43\x00\x4c\x00\x41\x00\x49\x00\x4d\x00\x45\x00\x52\x00\ +\x0a\x00\x54\x00\x48\x00\x45\x00\x20\x00\x46\x00\x4f\x00\x4e\x00\ +\x54\x00\x20\x00\x53\x00\x4f\x00\x46\x00\x54\x00\x57\x00\x41\x00\ +\x52\x00\x45\x00\x20\x00\x49\x00\x53\x00\x20\x00\x50\x00\x52\x00\ +\x4f\x00\x56\x00\x49\x00\x44\x00\x45\x00\x44\x00\x20\x00\x22\x00\ +\x41\x00\x53\x00\x20\x00\x49\x00\x53\x00\x22\x00\x2c\x00\x20\x00\ +\x57\x00\x49\x00\x54\x00\x48\x00\x4f\x00\x55\x00\x54\x00\x20\x00\ +\x57\x00\x41\x00\x52\x00\x52\x00\x41\x00\x4e\x00\x54\x00\x59\x00\ +\x20\x00\x4f\x00\x46\x00\x20\x00\x41\x00\x4e\x00\x59\x00\x20\x00\ +\x4b\x00\x49\x00\x4e\x00\x44\x00\x2c\x00\x0a\x00\x45\x00\x58\x00\ +\x50\x00\x52\x00\x45\x00\x53\x00\x53\x00\x20\x00\x4f\x00\x52\x00\ +\x20\x00\x49\x00\x4d\x00\x50\x00\x4c\x00\x49\x00\x45\x00\x44\x00\ +\x2c\x00\x20\x00\x49\x00\x4e\x00\x43\x00\x4c\x00\x55\x00\x44\x00\ +\x49\x00\x4e\x00\x47\x00\x20\x00\x42\x00\x55\x00\x54\x00\x20\x00\ +\x4e\x00\x4f\x00\x54\x00\x20\x00\x4c\x00\x49\x00\x4d\x00\x49\x00\ +\x54\x00\x45\x00\x44\x00\x20\x00\x54\x00\x4f\x00\x20\x00\x41\x00\ +\x4e\x00\x59\x00\x20\x00\x57\x00\x41\x00\x52\x00\x52\x00\x41\x00\ +\x4e\x00\x54\x00\x49\x00\x45\x00\x53\x00\x20\x00\x4f\x00\x46\x00\ +\x0a\x00\x4d\x00\x45\x00\x52\x00\x43\x00\x48\x00\x41\x00\x4e\x00\ +\x54\x00\x41\x00\x42\x00\x49\x00\x4c\x00\x49\x00\x54\x00\x59\x00\ +\x2c\x00\x20\x00\x46\x00\x49\x00\x54\x00\x4e\x00\x45\x00\x53\x00\ +\x53\x00\x20\x00\x46\x00\x4f\x00\x52\x00\x20\x00\x41\x00\x20\x00\ +\x50\x00\x41\x00\x52\x00\x54\x00\x49\x00\x43\x00\x55\x00\x4c\x00\ +\x41\x00\x52\x00\x20\x00\x50\x00\x55\x00\x52\x00\x50\x00\x4f\x00\ +\x53\x00\x45\x00\x20\x00\x41\x00\x4e\x00\x44\x00\x20\x00\x4e\x00\ +\x4f\x00\x4e\x00\x49\x00\x4e\x00\x46\x00\x52\x00\x49\x00\x4e\x00\ +\x47\x00\x45\x00\x4d\x00\x45\x00\x4e\x00\x54\x00\x0a\x00\x4f\x00\ +\x46\x00\x20\x00\x43\x00\x4f\x00\x50\x00\x59\x00\x52\x00\x49\x00\ +\x47\x00\x48\x00\x54\x00\x2c\x00\x20\x00\x50\x00\x41\x00\x54\x00\ +\x45\x00\x4e\x00\x54\x00\x2c\x00\x20\x00\x54\x00\x52\x00\x41\x00\ +\x44\x00\x45\x00\x4d\x00\x41\x00\x52\x00\x4b\x00\x2c\x00\x20\x00\ +\x4f\x00\x52\x00\x20\x00\x4f\x00\x54\x00\x48\x00\x45\x00\x52\x00\ +\x20\x00\x52\x00\x49\x00\x47\x00\x48\x00\x54\x00\x2e\x00\x20\x00\ +\x49\x00\x4e\x00\x20\x00\x4e\x00\x4f\x00\x20\x00\x45\x00\x56\x00\ +\x45\x00\x4e\x00\x54\x00\x20\x00\x53\x00\x48\x00\x41\x00\x4c\x00\ +\x4c\x00\x20\x00\x54\x00\x48\x00\x45\x00\x0a\x00\x43\x00\x4f\x00\ +\x50\x00\x59\x00\x52\x00\x49\x00\x47\x00\x48\x00\x54\x00\x20\x00\ +\x48\x00\x4f\x00\x4c\x00\x44\x00\x45\x00\x52\x00\x20\x00\x42\x00\ +\x45\x00\x20\x00\x4c\x00\x49\x00\x41\x00\x42\x00\x4c\x00\x45\x00\ +\x20\x00\x46\x00\x4f\x00\x52\x00\x20\x00\x41\x00\x4e\x00\x59\x00\ +\x20\x00\x43\x00\x4c\x00\x41\x00\x49\x00\x4d\x00\x2c\x00\x20\x00\ +\x44\x00\x41\x00\x4d\x00\x41\x00\x47\x00\x45\x00\x53\x00\x20\x00\ +\x4f\x00\x52\x00\x20\x00\x4f\x00\x54\x00\x48\x00\x45\x00\x52\x00\ +\x20\x00\x4c\x00\x49\x00\x41\x00\x42\x00\x49\x00\x4c\x00\x49\x00\ +\x54\x00\x59\x00\x2c\x00\x0a\x00\x49\x00\x4e\x00\x43\x00\x4c\x00\ +\x55\x00\x44\x00\x49\x00\x4e\x00\x47\x00\x20\x00\x41\x00\x4e\x00\ +\x59\x00\x20\x00\x47\x00\x45\x00\x4e\x00\x45\x00\x52\x00\x41\x00\ +\x4c\x00\x2c\x00\x20\x00\x53\x00\x50\x00\x45\x00\x43\x00\x49\x00\ +\x41\x00\x4c\x00\x2c\x00\x20\x00\x49\x00\x4e\x00\x44\x00\x49\x00\ +\x52\x00\x45\x00\x43\x00\x54\x00\x2c\x00\x20\x00\x49\x00\x4e\x00\ +\x43\x00\x49\x00\x44\x00\x45\x00\x4e\x00\x54\x00\x41\x00\x4c\x00\ +\x2c\x00\x20\x00\x4f\x00\x52\x00\x20\x00\x43\x00\x4f\x00\x4e\x00\ +\x53\x00\x45\x00\x51\x00\x55\x00\x45\x00\x4e\x00\x54\x00\x49\x00\ +\x41\x00\x4c\x00\x0a\x00\x44\x00\x41\x00\x4d\x00\x41\x00\x47\x00\ +\x45\x00\x53\x00\x2c\x00\x20\x00\x57\x00\x48\x00\x45\x00\x54\x00\ +\x48\x00\x45\x00\x52\x00\x20\x00\x49\x00\x4e\x00\x20\x00\x41\x00\ +\x4e\x00\x20\x00\x41\x00\x43\x00\x54\x00\x49\x00\x4f\x00\x4e\x00\ +\x20\x00\x4f\x00\x46\x00\x20\x00\x43\x00\x4f\x00\x4e\x00\x54\x00\ +\x52\x00\x41\x00\x43\x00\x54\x00\x2c\x00\x20\x00\x54\x00\x4f\x00\ +\x52\x00\x54\x00\x20\x00\x4f\x00\x52\x00\x20\x00\x4f\x00\x54\x00\ +\x48\x00\x45\x00\x52\x00\x57\x00\x49\x00\x53\x00\x45\x00\x2c\x00\ +\x20\x00\x41\x00\x52\x00\x49\x00\x53\x00\x49\x00\x4e\x00\x47\x00\ +\x0a\x00\x46\x00\x52\x00\x4f\x00\x4d\x00\x2c\x00\x20\x00\x4f\x00\ +\x55\x00\x54\x00\x20\x00\x4f\x00\x46\x00\x20\x00\x54\x00\x48\x00\ +\x45\x00\x20\x00\x55\x00\x53\x00\x45\x00\x20\x00\x4f\x00\x52\x00\ +\x20\x00\x49\x00\x4e\x00\x41\x00\x42\x00\x49\x00\x4c\x00\x49\x00\ +\x54\x00\x59\x00\x20\x00\x54\x00\x4f\x00\x20\x00\x55\x00\x53\x00\ +\x45\x00\x20\x00\x54\x00\x48\x00\x45\x00\x20\x00\x46\x00\x4f\x00\ +\x4e\x00\x54\x00\x20\x00\x53\x00\x4f\x00\x46\x00\x54\x00\x57\x00\ +\x41\x00\x52\x00\x45\x00\x20\x00\x4f\x00\x52\x00\x20\x00\x46\x00\ +\x52\x00\x4f\x00\x4d\x00\x0a\x00\x4f\x00\x54\x00\x48\x00\x45\x00\ +\x52\x00\x20\x00\x44\x00\x45\x00\x41\x00\x4c\x00\x49\x00\x4e\x00\ +\x47\x00\x53\x00\x20\x00\x49\x00\x4e\x00\x20\x00\x54\x00\x48\x00\ +\x45\x00\x20\x00\x46\x00\x4f\x00\x4e\x00\x54\x00\x20\x00\x53\x00\ +\x4f\x00\x46\x00\x54\x00\x57\x00\x41\x00\x52\x00\x45\x00\x2e\x00\ +\x00\x43\x6f\x70\x79\x72\x69\x67\x68\x74\x20\x28\x63\x29\x20\x32\ +\x30\x30\x38\x2c\x20\x42\x65\x6e\x20\x4c\x61\x65\x6e\x65\x6e\x20\ +\x28\x62\x65\x6e\x6c\x61\x65\x6e\x65\x6e\x40\x67\x6d\x61\x69\x6c\ +\x2e\x63\x6f\x6d\x29\x2c\x0a\x77\x69\x74\x68\x20\x52\x65\x73\x65\ +\x72\x76\x65\x64\x20\x46\x6f\x6e\x74\x20\x4e\x61\x6d\x65\x20\x45\ +\x75\x74\x65\x72\x70\x65\x2e\x0a\x0a\x54\x68\x69\x73\x20\x46\x6f\ +\x6e\x74\x20\x53\x6f\x66\x74\x77\x61\x72\x65\x20\x69\x73\x20\x6c\ +\x69\x63\x65\x6e\x73\x65\x64\x20\x75\x6e\x64\x65\x72\x20\x74\x68\ +\x65\x20\x53\x49\x4c\x20\x4f\x70\x65\x6e\x20\x46\x6f\x6e\x74\x20\ +\x4c\x69\x63\x65\x6e\x73\x65\x2c\x20\x56\x65\x72\x73\x69\x6f\x6e\ +\x20\x31\x2e\x31\x2e\x0a\x54\x68\x69\x73\x20\x6c\x69\x63\x65\x6e\ +\x73\x65\x20\x69\x73\x20\x63\x6f\x70\x69\x65\x64\x20\x62\x65\x6c\ +\x6f\x77\x2c\x20\x61\x6e\x64\x20\x69\x73\x20\x61\x6c\x73\x6f\x20\ +\x61\x76\x61\x69\x6c\x61\x62\x6c\x65\x20\x77\x69\x74\x68\x20\x61\ +\x20\x46\x41\x51\x20\x61\x74\x3a\x0a\x68\x74\x74\x70\x3a\x2f\x2f\ +\x73\x63\x72\x69\x70\x74\x73\x2e\x73\x69\x6c\x2e\x6f\x72\x67\x2f\ +\x4f\x46\x4c\x0a\x0a\x0a\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x0a\x53\x49\x4c\x20\x4f\x50\x45\x4e\x20\x46\x4f\x4e\x54\x20\ +\x4c\x49\x43\x45\x4e\x53\x45\x20\x56\x65\x72\x73\x69\x6f\x6e\x20\ +\x31\x2e\x31\x20\x2d\x20\x32\x36\x20\x46\x65\x62\x72\x75\x61\x72\ +\x79\x20\x32\x30\x30\x37\x0a\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\ +\x2d\x2d\x0a\x0a\x50\x52\x45\x41\x4d\x42\x4c\x45\x0a\x54\x68\x65\ +\x20\x67\x6f\x61\x6c\x73\x20\x6f\x66\x20\x74\x68\x65\x20\x4f\x70\ +\x65\x6e\x20\x46\x6f\x6e\x74\x20\x4c\x69\x63\x65\x6e\x73\x65\x20\ +\x28\x4f\x46\x4c\x29\x20\x61\x72\x65\x20\x74\x6f\x20\x73\x74\x69\ +\x6d\x75\x6c\x61\x74\x65\x20\x77\x6f\x72\x6c\x64\x77\x69\x64\x65\ +\x0a\x64\x65\x76\x65\x6c\x6f\x70\x6d\x65\x6e\x74\x20\x6f\x66\x20\ +\x63\x6f\x6c\x6c\x61\x62\x6f\x72\x61\x74\x69\x76\x65\x20\x66\x6f\ +\x6e\x74\x20\x70\x72\x6f\x6a\x65\x63\x74\x73\x2c\x20\x74\x6f\x20\ +\x73\x75\x70\x70\x6f\x72\x74\x20\x74\x68\x65\x20\x66\x6f\x6e\x74\ +\x20\x63\x72\x65\x61\x74\x69\x6f\x6e\x0a\x65\x66\x66\x6f\x72\x74\ +\x73\x20\x6f\x66\x20\x61\x63\x61\x64\x65\x6d\x69\x63\x20\x61\x6e\ +\x64\x20\x6c\x69\x6e\x67\x75\x69\x73\x74\x69\x63\x20\x63\x6f\x6d\ +\x6d\x75\x6e\x69\x74\x69\x65\x73\x2c\x20\x61\x6e\x64\x20\x74\x6f\ +\x20\x70\x72\x6f\x76\x69\x64\x65\x20\x61\x20\x66\x72\x65\x65\x20\ +\x61\x6e\x64\x0a\x6f\x70\x65\x6e\x20\x66\x72\x61\x6d\x65\x77\x6f\ +\x72\x6b\x20\x69\x6e\x20\x77\x68\x69\x63\x68\x20\x66\x6f\x6e\x74\ +\x73\x20\x6d\x61\x79\x20\x62\x65\x20\x73\x68\x61\x72\x65\x64\x20\ +\x61\x6e\x64\x20\x69\x6d\x70\x72\x6f\x76\x65\x64\x20\x69\x6e\x20\ +\x70\x61\x72\x74\x6e\x65\x72\x73\x68\x69\x70\x0a\x77\x69\x74\x68\ +\x20\x6f\x74\x68\x65\x72\x73\x2e\x0a\x0a\x54\x68\x65\x20\x4f\x46\ +\x4c\x20\x61\x6c\x6c\x6f\x77\x73\x20\x74\x68\x65\x20\x6c\x69\x63\ +\x65\x6e\x73\x65\x64\x20\x66\x6f\x6e\x74\x73\x20\x74\x6f\x20\x62\ +\x65\x20\x75\x73\x65\x64\x2c\x20\x73\x74\x75\x64\x69\x65\x64\x2c\ +\x20\x6d\x6f\x64\x69\x66\x69\x65\x64\x20\x61\x6e\x64\x0a\x72\x65\ +\x64\x69\x73\x74\x72\x69\x62\x75\x74\x65\x64\x20\x66\x72\x65\x65\ +\x6c\x79\x20\x61\x73\x20\x6c\x6f\x6e\x67\x20\x61\x73\x20\x74\x68\ +\x65\x79\x20\x61\x72\x65\x20\x6e\x6f\x74\x20\x73\x6f\x6c\x64\x20\ +\x62\x79\x20\x74\x68\x65\x6d\x73\x65\x6c\x76\x65\x73\x2e\x20\x54\ +\x68\x65\x0a\x66\x6f\x6e\x74\x73\x2c\x20\x69\x6e\x63\x6c\x75\x64\ +\x69\x6e\x67\x20\x61\x6e\x79\x20\x64\x65\x72\x69\x76\x61\x74\x69\ +\x76\x65\x20\x77\x6f\x72\x6b\x73\x2c\x20\x63\x61\x6e\x20\x62\x65\ +\x20\x62\x75\x6e\x64\x6c\x65\x64\x2c\x20\x65\x6d\x62\x65\x64\x64\ +\x65\x64\x2c\x20\x0a\x72\x65\x64\x69\x73\x74\x72\x69\x62\x75\x74\ +\x65\x64\x20\x61\x6e\x64\x2f\x6f\x72\x20\x73\x6f\x6c\x64\x20\x77\ +\x69\x74\x68\x20\x61\x6e\x79\x20\x73\x6f\x66\x74\x77\x61\x72\x65\ +\x20\x70\x72\x6f\x76\x69\x64\x65\x64\x20\x74\x68\x61\x74\x20\x61\ +\x6e\x79\x20\x72\x65\x73\x65\x72\x76\x65\x64\x0a\x6e\x61\x6d\x65\ +\x73\x20\x61\x72\x65\x20\x6e\x6f\x74\x20\x75\x73\x65\x64\x20\x62\ +\x79\x20\x64\x65\x72\x69\x76\x61\x74\x69\x76\x65\x20\x77\x6f\x72\ +\x6b\x73\x2e\x20\x54\x68\x65\x20\x66\x6f\x6e\x74\x73\x20\x61\x6e\ +\x64\x20\x64\x65\x72\x69\x76\x61\x74\x69\x76\x65\x73\x2c\x0a\x68\ +\x6f\x77\x65\x76\x65\x72\x2c\x20\x63\x61\x6e\x6e\x6f\x74\x20\x62\ +\x65\x20\x72\x65\x6c\x65\x61\x73\x65\x64\x20\x75\x6e\x64\x65\x72\ +\x20\x61\x6e\x79\x20\x6f\x74\x68\x65\x72\x20\x74\x79\x70\x65\x20\ +\x6f\x66\x20\x6c\x69\x63\x65\x6e\x73\x65\x2e\x20\x54\x68\x65\x0a\ +\x72\x65\x71\x75\x69\x72\x65\x6d\x65\x6e\x74\x20\x66\x6f\x72\x20\ +\x66\x6f\x6e\x74\x73\x20\x74\x6f\x20\x72\x65\x6d\x61\x69\x6e\x20\ +\x75\x6e\x64\x65\x72\x20\x74\x68\x69\x73\x20\x6c\x69\x63\x65\x6e\ +\x73\x65\x20\x64\x6f\x65\x73\x20\x6e\x6f\x74\x20\x61\x70\x70\x6c\ +\x79\x0a\x74\x6f\x20\x61\x6e\x79\x20\x64\x6f\x63\x75\x6d\x65\x6e\ +\x74\x20\x63\x72\x65\x61\x74\x65\x64\x20\x75\x73\x69\x6e\x67\x20\ +\x74\x68\x65\x20\x66\x6f\x6e\x74\x73\x20\x6f\x72\x20\x74\x68\x65\ +\x69\x72\x20\x64\x65\x72\x69\x76\x61\x74\x69\x76\x65\x73\x2e\x0a\ +\x0a\x44\x45\x46\x49\x4e\x49\x54\x49\x4f\x4e\x53\x0a\x22\x46\x6f\ +\x6e\x74\x20\x53\x6f\x66\x74\x77\x61\x72\x65\x22\x20\x72\x65\x66\ +\x65\x72\x73\x20\x74\x6f\x20\x74\x68\x65\x20\x73\x65\x74\x20\x6f\ +\x66\x20\x66\x69\x6c\x65\x73\x20\x72\x65\x6c\x65\x61\x73\x65\x64\ +\x20\x62\x79\x20\x74\x68\x65\x20\x43\x6f\x70\x79\x72\x69\x67\x68\ +\x74\x0a\x48\x6f\x6c\x64\x65\x72\x28\x73\x29\x20\x75\x6e\x64\x65\ +\x72\x20\x74\x68\x69\x73\x20\x6c\x69\x63\x65\x6e\x73\x65\x20\x61\ +\x6e\x64\x20\x63\x6c\x65\x61\x72\x6c\x79\x20\x6d\x61\x72\x6b\x65\ +\x64\x20\x61\x73\x20\x73\x75\x63\x68\x2e\x20\x54\x68\x69\x73\x20\ +\x6d\x61\x79\x0a\x69\x6e\x63\x6c\x75\x64\x65\x20\x73\x6f\x75\x72\ +\x63\x65\x20\x66\x69\x6c\x65\x73\x2c\x20\x62\x75\x69\x6c\x64\x20\ +\x73\x63\x72\x69\x70\x74\x73\x20\x61\x6e\x64\x20\x64\x6f\x63\x75\ +\x6d\x65\x6e\x74\x61\x74\x69\x6f\x6e\x2e\x0a\x0a\x22\x52\x65\x73\ +\x65\x72\x76\x65\x64\x20\x46\x6f\x6e\x74\x20\x4e\x61\x6d\x65\x22\ +\x20\x72\x65\x66\x65\x72\x73\x20\x74\x6f\x20\x61\x6e\x79\x20\x6e\ +\x61\x6d\x65\x73\x20\x73\x70\x65\x63\x69\x66\x69\x65\x64\x20\x61\ +\x73\x20\x73\x75\x63\x68\x20\x61\x66\x74\x65\x72\x20\x74\x68\x65\ +\x0a\x63\x6f\x70\x79\x72\x69\x67\x68\x74\x20\x73\x74\x61\x74\x65\ +\x6d\x65\x6e\x74\x28\x73\x29\x2e\x0a\x0a\x22\x4f\x72\x69\x67\x69\ +\x6e\x61\x6c\x20\x56\x65\x72\x73\x69\x6f\x6e\x22\x20\x72\x65\x66\ +\x65\x72\x73\x20\x74\x6f\x20\x74\x68\x65\x20\x63\x6f\x6c\x6c\x65\ +\x63\x74\x69\x6f\x6e\x20\x6f\x66\x20\x46\x6f\x6e\x74\x20\x53\x6f\ +\x66\x74\x77\x61\x72\x65\x20\x63\x6f\x6d\x70\x6f\x6e\x65\x6e\x74\ +\x73\x20\x61\x73\x0a\x64\x69\x73\x74\x72\x69\x62\x75\x74\x65\x64\ +\x20\x62\x79\x20\x74\x68\x65\x20\x43\x6f\x70\x79\x72\x69\x67\x68\ +\x74\x20\x48\x6f\x6c\x64\x65\x72\x28\x73\x29\x2e\x0a\x0a\x22\x4d\ +\x6f\x64\x69\x66\x69\x65\x64\x20\x56\x65\x72\x73\x69\x6f\x6e\x22\ +\x20\x72\x65\x66\x65\x72\x73\x20\x74\x6f\x20\x61\x6e\x79\x20\x64\ +\x65\x72\x69\x76\x61\x74\x69\x76\x65\x20\x6d\x61\x64\x65\x20\x62\ +\x79\x20\x61\x64\x64\x69\x6e\x67\x20\x74\x6f\x2c\x20\x64\x65\x6c\ +\x65\x74\x69\x6e\x67\x2c\x0a\x6f\x72\x20\x73\x75\x62\x73\x74\x69\ +\x74\x75\x74\x69\x6e\x67\x20\x2d\x2d\x20\x69\x6e\x20\x70\x61\x72\ +\x74\x20\x6f\x72\x20\x69\x6e\x20\x77\x68\x6f\x6c\x65\x20\x2d\x2d\ +\x20\x61\x6e\x79\x20\x6f\x66\x20\x74\x68\x65\x20\x63\x6f\x6d\x70\ +\x6f\x6e\x65\x6e\x74\x73\x20\x6f\x66\x20\x74\x68\x65\x0a\x4f\x72\ +\x69\x67\x69\x6e\x61\x6c\x20\x56\x65\x72\x73\x69\x6f\x6e\x2c\x20\ +\x62\x79\x20\x63\x68\x61\x6e\x67\x69\x6e\x67\x20\x66\x6f\x72\x6d\ +\x61\x74\x73\x20\x6f\x72\x20\x62\x79\x20\x70\x6f\x72\x74\x69\x6e\ +\x67\x20\x74\x68\x65\x20\x46\x6f\x6e\x74\x20\x53\x6f\x66\x74\x77\ +\x61\x72\x65\x20\x74\x6f\x20\x61\x0a\x6e\x65\x77\x20\x65\x6e\x76\ +\x69\x72\x6f\x6e\x6d\x65\x6e\x74\x2e\x0a\x0a\x22\x41\x75\x74\x68\ +\x6f\x72\x22\x20\x72\x65\x66\x65\x72\x73\x20\x74\x6f\x20\x61\x6e\ +\x79\x20\x64\x65\x73\x69\x67\x6e\x65\x72\x2c\x20\x65\x6e\x67\x69\ +\x6e\x65\x65\x72\x2c\x20\x70\x72\x6f\x67\x72\x61\x6d\x6d\x65\x72\ +\x2c\x20\x74\x65\x63\x68\x6e\x69\x63\x61\x6c\x0a\x77\x72\x69\x74\ +\x65\x72\x20\x6f\x72\x20\x6f\x74\x68\x65\x72\x20\x70\x65\x72\x73\ +\x6f\x6e\x20\x77\x68\x6f\x20\x63\x6f\x6e\x74\x72\x69\x62\x75\x74\ +\x65\x64\x20\x74\x6f\x20\x74\x68\x65\x20\x46\x6f\x6e\x74\x20\x53\ +\x6f\x66\x74\x77\x61\x72\x65\x2e\x0a\x0a\x50\x45\x52\x4d\x49\x53\ +\x53\x49\x4f\x4e\x20\x26\x20\x43\x4f\x4e\x44\x49\x54\x49\x4f\x4e\ +\x53\x0a\x50\x65\x72\x6d\x69\x73\x73\x69\x6f\x6e\x20\x69\x73\x20\ +\x68\x65\x72\x65\x62\x79\x20\x67\x72\x61\x6e\x74\x65\x64\x2c\x20\ +\x66\x72\x65\x65\x20\x6f\x66\x20\x63\x68\x61\x72\x67\x65\x2c\x20\ +\x74\x6f\x20\x61\x6e\x79\x20\x70\x65\x72\x73\x6f\x6e\x20\x6f\x62\ +\x74\x61\x69\x6e\x69\x6e\x67\x0a\x61\x20\x63\x6f\x70\x79\x20\x6f\ +\x66\x20\x74\x68\x65\x20\x46\x6f\x6e\x74\x20\x53\x6f\x66\x74\x77\ +\x61\x72\x65\x2c\x20\x74\x6f\x20\x75\x73\x65\x2c\x20\x73\x74\x75\ +\x64\x79\x2c\x20\x63\x6f\x70\x79\x2c\x20\x6d\x65\x72\x67\x65\x2c\ +\x20\x65\x6d\x62\x65\x64\x2c\x20\x6d\x6f\x64\x69\x66\x79\x2c\x0a\ +\x72\x65\x64\x69\x73\x74\x72\x69\x62\x75\x74\x65\x2c\x20\x61\x6e\ +\x64\x20\x73\x65\x6c\x6c\x20\x6d\x6f\x64\x69\x66\x69\x65\x64\x20\ +\x61\x6e\x64\x20\x75\x6e\x6d\x6f\x64\x69\x66\x69\x65\x64\x20\x63\ +\x6f\x70\x69\x65\x73\x20\x6f\x66\x20\x74\x68\x65\x20\x46\x6f\x6e\ +\x74\x0a\x53\x6f\x66\x74\x77\x61\x72\x65\x2c\x20\x73\x75\x62\x6a\ +\x65\x63\x74\x20\x74\x6f\x20\x74\x68\x65\x20\x66\x6f\x6c\x6c\x6f\ +\x77\x69\x6e\x67\x20\x63\x6f\x6e\x64\x69\x74\x69\x6f\x6e\x73\x3a\ +\x0a\x0a\x31\x29\x20\x4e\x65\x69\x74\x68\x65\x72\x20\x74\x68\x65\ +\x20\x46\x6f\x6e\x74\x20\x53\x6f\x66\x74\x77\x61\x72\x65\x20\x6e\ +\x6f\x72\x20\x61\x6e\x79\x20\x6f\x66\x20\x69\x74\x73\x20\x69\x6e\ +\x64\x69\x76\x69\x64\x75\x61\x6c\x20\x63\x6f\x6d\x70\x6f\x6e\x65\ +\x6e\x74\x73\x2c\x0a\x69\x6e\x20\x4f\x72\x69\x67\x69\x6e\x61\x6c\ +\x20\x6f\x72\x20\x4d\x6f\x64\x69\x66\x69\x65\x64\x20\x56\x65\x72\ +\x73\x69\x6f\x6e\x73\x2c\x20\x6d\x61\x79\x20\x62\x65\x20\x73\x6f\ +\x6c\x64\x20\x62\x79\x20\x69\x74\x73\x65\x6c\x66\x2e\x0a\x0a\x32\ +\x29\x20\x4f\x72\x69\x67\x69\x6e\x61\x6c\x20\x6f\x72\x20\x4d\x6f\ +\x64\x69\x66\x69\x65\x64\x20\x56\x65\x72\x73\x69\x6f\x6e\x73\x20\ +\x6f\x66\x20\x74\x68\x65\x20\x46\x6f\x6e\x74\x20\x53\x6f\x66\x74\ +\x77\x61\x72\x65\x20\x6d\x61\x79\x20\x62\x65\x20\x62\x75\x6e\x64\ +\x6c\x65\x64\x2c\x0a\x72\x65\x64\x69\x73\x74\x72\x69\x62\x75\x74\ +\x65\x64\x20\x61\x6e\x64\x2f\x6f\x72\x20\x73\x6f\x6c\x64\x20\x77\ +\x69\x74\x68\x20\x61\x6e\x79\x20\x73\x6f\x66\x74\x77\x61\x72\x65\ +\x2c\x20\x70\x72\x6f\x76\x69\x64\x65\x64\x20\x74\x68\x61\x74\x20\ +\x65\x61\x63\x68\x20\x63\x6f\x70\x79\x0a\x63\x6f\x6e\x74\x61\x69\ +\x6e\x73\x20\x74\x68\x65\x20\x61\x62\x6f\x76\x65\x20\x63\x6f\x70\ +\x79\x72\x69\x67\x68\x74\x20\x6e\x6f\x74\x69\x63\x65\x20\x61\x6e\ +\x64\x20\x74\x68\x69\x73\x20\x6c\x69\x63\x65\x6e\x73\x65\x2e\x20\ +\x54\x68\x65\x73\x65\x20\x63\x61\x6e\x20\x62\x65\x0a\x69\x6e\x63\ +\x6c\x75\x64\x65\x64\x20\x65\x69\x74\x68\x65\x72\x20\x61\x73\x20\ +\x73\x74\x61\x6e\x64\x2d\x61\x6c\x6f\x6e\x65\x20\x74\x65\x78\x74\ +\x20\x66\x69\x6c\x65\x73\x2c\x20\x68\x75\x6d\x61\x6e\x2d\x72\x65\ +\x61\x64\x61\x62\x6c\x65\x20\x68\x65\x61\x64\x65\x72\x73\x20\x6f\ +\x72\x0a\x69\x6e\x20\x74\x68\x65\x20\x61\x70\x70\x72\x6f\x70\x72\ +\x69\x61\x74\x65\x20\x6d\x61\x63\x68\x69\x6e\x65\x2d\x72\x65\x61\ +\x64\x61\x62\x6c\x65\x20\x6d\x65\x74\x61\x64\x61\x74\x61\x20\x66\ +\x69\x65\x6c\x64\x73\x20\x77\x69\x74\x68\x69\x6e\x20\x74\x65\x78\ +\x74\x20\x6f\x72\x0a\x62\x69\x6e\x61\x72\x79\x20\x66\x69\x6c\x65\ +\x73\x20\x61\x73\x20\x6c\x6f\x6e\x67\x20\x61\x73\x20\x74\x68\x6f\ +\x73\x65\x20\x66\x69\x65\x6c\x64\x73\x20\x63\x61\x6e\x20\x62\x65\ +\x20\x65\x61\x73\x69\x6c\x79\x20\x76\x69\x65\x77\x65\x64\x20\x62\ +\x79\x20\x74\x68\x65\x20\x75\x73\x65\x72\x2e\x0a\x0a\x33\x29\x20\ +\x4e\x6f\x20\x4d\x6f\x64\x69\x66\x69\x65\x64\x20\x56\x65\x72\x73\ +\x69\x6f\x6e\x20\x6f\x66\x20\x74\x68\x65\x20\x46\x6f\x6e\x74\x20\ +\x53\x6f\x66\x74\x77\x61\x72\x65\x20\x6d\x61\x79\x20\x75\x73\x65\ +\x20\x74\x68\x65\x20\x52\x65\x73\x65\x72\x76\x65\x64\x20\x46\x6f\ +\x6e\x74\x0a\x4e\x61\x6d\x65\x28\x73\x29\x20\x75\x6e\x6c\x65\x73\ +\x73\x20\x65\x78\x70\x6c\x69\x63\x69\x74\x20\x77\x72\x69\x74\x74\ +\x65\x6e\x20\x70\x65\x72\x6d\x69\x73\x73\x69\x6f\x6e\x20\x69\x73\ +\x20\x67\x72\x61\x6e\x74\x65\x64\x20\x62\x79\x20\x74\x68\x65\x20\ +\x63\x6f\x72\x72\x65\x73\x70\x6f\x6e\x64\x69\x6e\x67\x0a\x43\x6f\ +\x70\x79\x72\x69\x67\x68\x74\x20\x48\x6f\x6c\x64\x65\x72\x2e\x20\ +\x54\x68\x69\x73\x20\x72\x65\x73\x74\x72\x69\x63\x74\x69\x6f\x6e\ +\x20\x6f\x6e\x6c\x79\x20\x61\x70\x70\x6c\x69\x65\x73\x20\x74\x6f\ +\x20\x74\x68\x65\x20\x70\x72\x69\x6d\x61\x72\x79\x20\x66\x6f\x6e\ +\x74\x20\x6e\x61\x6d\x65\x20\x61\x73\x0a\x70\x72\x65\x73\x65\x6e\ +\x74\x65\x64\x20\x74\x6f\x20\x74\x68\x65\x20\x75\x73\x65\x72\x73\ +\x2e\x0a\x0a\x34\x29\x20\x54\x68\x65\x20\x6e\x61\x6d\x65\x28\x73\ +\x29\x20\x6f\x66\x20\x74\x68\x65\x20\x43\x6f\x70\x79\x72\x69\x67\ +\x68\x74\x20\x48\x6f\x6c\x64\x65\x72\x28\x73\x29\x20\x6f\x72\x20\ +\x74\x68\x65\x20\x41\x75\x74\x68\x6f\x72\x28\x73\x29\x20\x6f\x66\ +\x20\x74\x68\x65\x20\x46\x6f\x6e\x74\x0a\x53\x6f\x66\x74\x77\x61\ +\x72\x65\x20\x73\x68\x61\x6c\x6c\x20\x6e\x6f\x74\x20\x62\x65\x20\ +\x75\x73\x65\x64\x20\x74\x6f\x20\x70\x72\x6f\x6d\x6f\x74\x65\x2c\ +\x20\x65\x6e\x64\x6f\x72\x73\x65\x20\x6f\x72\x20\x61\x64\x76\x65\ +\x72\x74\x69\x73\x65\x20\x61\x6e\x79\x0a\x4d\x6f\x64\x69\x66\x69\ +\x65\x64\x20\x56\x65\x72\x73\x69\x6f\x6e\x2c\x20\x65\x78\x63\x65\ +\x70\x74\x20\x74\x6f\x20\x61\x63\x6b\x6e\x6f\x77\x6c\x65\x64\x67\ +\x65\x20\x74\x68\x65\x20\x63\x6f\x6e\x74\x72\x69\x62\x75\x74\x69\ +\x6f\x6e\x28\x73\x29\x20\x6f\x66\x20\x74\x68\x65\x0a\x43\x6f\x70\ +\x79\x72\x69\x67\x68\x74\x20\x48\x6f\x6c\x64\x65\x72\x28\x73\x29\ +\x20\x61\x6e\x64\x20\x74\x68\x65\x20\x41\x75\x74\x68\x6f\x72\x28\ +\x73\x29\x20\x6f\x72\x20\x77\x69\x74\x68\x20\x74\x68\x65\x69\x72\ +\x20\x65\x78\x70\x6c\x69\x63\x69\x74\x20\x77\x72\x69\x74\x74\x65\ +\x6e\x0a\x70\x65\x72\x6d\x69\x73\x73\x69\x6f\x6e\x2e\x0a\x0a\x35\ +\x29\x20\x54\x68\x65\x20\x46\x6f\x6e\x74\x20\x53\x6f\x66\x74\x77\ +\x61\x72\x65\x2c\x20\x6d\x6f\x64\x69\x66\x69\x65\x64\x20\x6f\x72\ +\x20\x75\x6e\x6d\x6f\x64\x69\x66\x69\x65\x64\x2c\x20\x69\x6e\x20\ +\x70\x61\x72\x74\x20\x6f\x72\x20\x69\x6e\x20\x77\x68\x6f\x6c\x65\ +\x2c\x0a\x6d\x75\x73\x74\x20\x62\x65\x20\x64\x69\x73\x74\x72\x69\ +\x62\x75\x74\x65\x64\x20\x65\x6e\x74\x69\x72\x65\x6c\x79\x20\x75\ +\x6e\x64\x65\x72\x20\x74\x68\x69\x73\x20\x6c\x69\x63\x65\x6e\x73\ +\x65\x2c\x20\x61\x6e\x64\x20\x6d\x75\x73\x74\x20\x6e\x6f\x74\x20\ +\x62\x65\x0a\x64\x69\x73\x74\x72\x69\x62\x75\x74\x65\x64\x20\x75\ +\x6e\x64\x65\x72\x20\x61\x6e\x79\x20\x6f\x74\x68\x65\x72\x20\x6c\ +\x69\x63\x65\x6e\x73\x65\x2e\x20\x54\x68\x65\x20\x72\x65\x71\x75\ +\x69\x72\x65\x6d\x65\x6e\x74\x20\x66\x6f\x72\x20\x66\x6f\x6e\x74\ +\x73\x20\x74\x6f\x0a\x72\x65\x6d\x61\x69\x6e\x20\x75\x6e\x64\x65\ +\x72\x20\x74\x68\x69\x73\x20\x6c\x69\x63\x65\x6e\x73\x65\x20\x64\ +\x6f\x65\x73\x20\x6e\x6f\x74\x20\x61\x70\x70\x6c\x79\x20\x74\x6f\ +\x20\x61\x6e\x79\x20\x64\x6f\x63\x75\x6d\x65\x6e\x74\x20\x63\x72\ +\x65\x61\x74\x65\x64\x0a\x75\x73\x69\x6e\x67\x20\x74\x68\x65\x20\ +\x46\x6f\x6e\x74\x20\x53\x6f\x66\x74\x77\x61\x72\x65\x2e\x0a\x0a\ +\x54\x45\x52\x4d\x49\x4e\x41\x54\x49\x4f\x4e\x0a\x54\x68\x69\x73\ +\x20\x6c\x69\x63\x65\x6e\x73\x65\x20\x62\x65\x63\x6f\x6d\x65\x73\ +\x20\x6e\x75\x6c\x6c\x20\x61\x6e\x64\x20\x76\x6f\x69\x64\x20\x69\ +\x66\x20\x61\x6e\x79\x20\x6f\x66\x20\x74\x68\x65\x20\x61\x62\x6f\ +\x76\x65\x20\x63\x6f\x6e\x64\x69\x74\x69\x6f\x6e\x73\x20\x61\x72\ +\x65\x0a\x6e\x6f\x74\x20\x6d\x65\x74\x2e\x0a\x0a\x44\x49\x53\x43\ +\x4c\x41\x49\x4d\x45\x52\x0a\x54\x48\x45\x20\x46\x4f\x4e\x54\x20\ +\x53\x4f\x46\x54\x57\x41\x52\x45\x20\x49\x53\x20\x50\x52\x4f\x56\ +\x49\x44\x45\x44\x20\x22\x41\x53\x20\x49\x53\x22\x2c\x20\x57\x49\ +\x54\x48\x4f\x55\x54\x20\x57\x41\x52\x52\x41\x4e\x54\x59\x20\x4f\ +\x46\x20\x41\x4e\x59\x20\x4b\x49\x4e\x44\x2c\x0a\x45\x58\x50\x52\ +\x45\x53\x53\x20\x4f\x52\x20\x49\x4d\x50\x4c\x49\x45\x44\x2c\x20\ +\x49\x4e\x43\x4c\x55\x44\x49\x4e\x47\x20\x42\x55\x54\x20\x4e\x4f\ +\x54\x20\x4c\x49\x4d\x49\x54\x45\x44\x20\x54\x4f\x20\x41\x4e\x59\ +\x20\x57\x41\x52\x52\x41\x4e\x54\x49\x45\x53\x20\x4f\x46\x0a\x4d\ +\x45\x52\x43\x48\x41\x4e\x54\x41\x42\x49\x4c\x49\x54\x59\x2c\x20\ +\x46\x49\x54\x4e\x45\x53\x53\x20\x46\x4f\x52\x20\x41\x20\x50\x41\ +\x52\x54\x49\x43\x55\x4c\x41\x52\x20\x50\x55\x52\x50\x4f\x53\x45\ +\x20\x41\x4e\x44\x20\x4e\x4f\x4e\x49\x4e\x46\x52\x49\x4e\x47\x45\ +\x4d\x45\x4e\x54\x0a\x4f\x46\x20\x43\x4f\x50\x59\x52\x49\x47\x48\ +\x54\x2c\x20\x50\x41\x54\x45\x4e\x54\x2c\x20\x54\x52\x41\x44\x45\ +\x4d\x41\x52\x4b\x2c\x20\x4f\x52\x20\x4f\x54\x48\x45\x52\x20\x52\ +\x49\x47\x48\x54\x2e\x20\x49\x4e\x20\x4e\x4f\x20\x45\x56\x45\x4e\ +\x54\x20\x53\x48\x41\x4c\x4c\x20\x54\x48\x45\x0a\x43\x4f\x50\x59\ +\x52\x49\x47\x48\x54\x20\x48\x4f\x4c\x44\x45\x52\x20\x42\x45\x20\ +\x4c\x49\x41\x42\x4c\x45\x20\x46\x4f\x52\x20\x41\x4e\x59\x20\x43\ +\x4c\x41\x49\x4d\x2c\x20\x44\x41\x4d\x41\x47\x45\x53\x20\x4f\x52\ +\x20\x4f\x54\x48\x45\x52\x20\x4c\x49\x41\x42\x49\x4c\x49\x54\x59\ +\x2c\x0a\x49\x4e\x43\x4c\x55\x44\x49\x4e\x47\x20\x41\x4e\x59\x20\ +\x47\x45\x4e\x45\x52\x41\x4c\x2c\x20\x53\x50\x45\x43\x49\x41\x4c\ +\x2c\x20\x49\x4e\x44\x49\x52\x45\x43\x54\x2c\x20\x49\x4e\x43\x49\ +\x44\x45\x4e\x54\x41\x4c\x2c\x20\x4f\x52\x20\x43\x4f\x4e\x53\x45\ +\x51\x55\x45\x4e\x54\x49\x41\x4c\x0a\x44\x41\x4d\x41\x47\x45\x53\ +\x2c\x20\x57\x48\x45\x54\x48\x45\x52\x20\x49\x4e\x20\x41\x4e\x20\ +\x41\x43\x54\x49\x4f\x4e\x20\x4f\x46\x20\x43\x4f\x4e\x54\x52\x41\ +\x43\x54\x2c\x20\x54\x4f\x52\x54\x20\x4f\x52\x20\x4f\x54\x48\x45\ +\x52\x57\x49\x53\x45\x2c\x20\x41\x52\x49\x53\x49\x4e\x47\x0a\x46\ +\x52\x4f\x4d\x2c\x20\x4f\x55\x54\x20\x4f\x46\x20\x54\x48\x45\x20\ +\x55\x53\x45\x20\x4f\x52\x20\x49\x4e\x41\x42\x49\x4c\x49\x54\x59\ +\x20\x54\x4f\x20\x55\x53\x45\x20\x54\x48\x45\x20\x46\x4f\x4e\x54\ +\x20\x53\x4f\x46\x54\x57\x41\x52\x45\x20\x4f\x52\x20\x46\x52\x4f\ +\x4d\x0a\x4f\x54\x48\x45\x52\x20\x44\x45\x41\x4c\x49\x4e\x47\x53\ +\x20\x49\x4e\x20\x54\x48\x45\x20\x46\x4f\x4e\x54\x20\x53\x4f\x46\ +\x54\x57\x41\x52\x45\x2e\x00\x00\x68\x00\x74\x00\x74\x00\x70\x00\ +\x3a\x00\x2f\x00\x2f\x00\x73\x00\x63\x00\x72\x00\x69\x00\x70\x00\ +\x74\x00\x73\x00\x2e\x00\x73\x00\x69\x00\x6c\x00\x2e\x00\x6f\x00\ +\x72\x00\x67\x00\x2f\x00\x6f\x00\x66\x00\x6c\x00\x00\x68\x74\x74\ +\x70\x3a\x2f\x2f\x73\x63\x72\x69\x70\x74\x73\x2e\x73\x69\x6c\x2e\ +\x6f\x72\x67\x2f\x6f\x66\x6c\x00\x00\x00\x00\x00\x02\x00\x00\x00\ +\x00\x00\x00\xff\x83\x00\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x14\x00\x00\x00\ +\x01\x00\x02\x00\x03\x01\x02\x01\x03\x01\x04\x01\x05\x01\x06\x01\ +\x07\x01\x08\x01\x09\x01\x0a\x01\x0b\x01\x0c\x01\x0d\x01\x0e\x01\ +\x0f\x01\x10\x01\x11\x01\x12\x01\x13\x01\x14\x01\x15\x01\x16\x01\ +\x17\x01\x18\x01\x19\x01\x1a\x01\x1b\x01\x1c\x01\x1d\x01\x1e\x01\ +\x1f\x01\x20\x01\x21\x01\x22\x01\x23\x01\x24\x01\x25\x01\x26\x01\ +\x27\x01\x28\x01\x29\x01\x2a\x01\x2b\x01\x2c\x01\x2d\x01\x2e\x01\ +\x2f\x01\x30\x01\x31\x01\x32\x01\x33\x01\x34\x01\x35\x01\x36\x01\ +\x37\x01\x38\x01\x39\x01\x3a\x01\x3b\x01\x3c\x01\x3d\x01\x3e\x01\ +\x3f\x01\x40\x01\x41\x01\x42\x01\x43\x01\x44\x01\x45\x01\x46\x01\ +\x47\x01\x48\x01\x49\x01\x4a\x01\x4b\x01\x4c\x01\x4d\x01\x4e\x01\ +\x4f\x01\x50\x01\x51\x01\x52\x01\x53\x01\x54\x01\x55\x01\x56\x01\ +\x57\x01\x58\x01\x59\x01\x5a\x01\x5b\x01\x5c\x01\x5d\x01\x5e\x01\ +\x5f\x01\x60\x01\x61\x01\x62\x01\x63\x01\x64\x01\x65\x01\x66\x01\ +\x67\x01\x68\x01\x69\x01\x6a\x01\x6b\x01\x6c\x01\x6d\x01\x6e\x01\ +\x6f\x01\x70\x01\x71\x01\x72\x01\x73\x01\x74\x01\x75\x01\x76\x01\ +\x77\x01\x78\x01\x79\x01\x7a\x01\x7b\x01\x7c\x01\x7d\x01\x7e\x01\ +\x7f\x01\x80\x01\x81\x01\x82\x01\x83\x01\x84\x01\x85\x01\x86\x01\ +\x87\x01\x88\x01\x89\x01\x8a\x01\x8b\x01\x8c\x01\x8d\x01\x8e\x01\ +\x8f\x01\x90\x01\x91\x01\x92\x01\x93\x01\x94\x01\x95\x01\x96\x01\ +\x97\x01\x98\x01\x99\x01\x9a\x01\x9b\x01\x9c\x01\x9d\x01\x9e\x01\ +\x9f\x01\xa0\x01\xa1\x01\xa2\x01\xa3\x01\xa4\x01\xa5\x01\xa6\x01\ +\xa7\x01\xa8\x01\xa9\x01\xaa\x01\xab\x01\xac\x01\xad\x01\xae\x01\ +\xaf\x01\xb0\x01\xb1\x01\xb2\x01\xb3\x01\xb4\x01\xb5\x01\xb6\x01\ +\xb7\x01\xb8\x01\xb9\x01\xba\x01\xbb\x01\xbc\x01\xbd\x01\xbe\x01\ +\xbf\x01\xc0\x01\xc1\x01\xc2\x01\xc3\x01\xc4\x01\xc5\x01\xc6\x01\ +\xc7\x01\xc8\x01\xc9\x01\xca\x01\xcb\x01\xcc\x01\xcd\x01\xce\x01\ +\xcf\x01\xd0\x01\xd1\x01\xd2\x01\xd3\x01\xd4\x01\xd5\x01\xd6\x01\ +\xd7\x01\xd8\x01\xd9\x01\xda\x01\xdb\x01\xdc\x01\xdd\x01\xde\x01\ +\xdf\x01\xe0\x01\xe1\x01\xe2\x01\xe3\x01\xe4\x01\xe5\x01\xe6\x01\ +\xe7\x01\xe8\x01\xe9\x01\xea\x01\xeb\x01\xec\x01\xed\x01\xee\x01\ +\xef\x01\xf0\x01\xf1\x01\xf2\x01\xf3\x01\xf4\x01\xf5\x01\xf6\x01\ +\xf7\x01\xf8\x01\xf9\x01\xfa\x01\xfb\x01\xfc\x01\xfd\x01\xfe\x01\ +\xff\x02\x00\x02\x01\x02\x02\x02\x03\x02\x04\x02\x05\x02\x06\x02\ +\x07\x02\x08\x02\x09\x02\x0a\x02\x0b\x02\x0c\x02\x0d\x02\x0e\x02\ +\x0f\x02\x10\x02\x11\x07\x75\x6e\x69\x32\x36\x36\x44\x07\x75\x6e\ +\x69\x32\x36\x36\x45\x07\x75\x6e\x69\x32\x36\x36\x46\x0d\x75\x31\ +\x44\x31\x38\x46\x5f\x75\x31\x44\x31\x38\x46\x14\x75\x31\x44\x31\ +\x38\x46\x5f\x75\x31\x44\x31\x38\x46\x5f\x75\x31\x44\x31\x38\x46\ +\x0d\x75\x31\x44\x31\x39\x31\x5f\x75\x31\x44\x31\x39\x31\x14\x75\ +\x31\x44\x31\x39\x31\x5f\x75\x31\x44\x31\x39\x31\x5f\x75\x31\x44\ +\x31\x39\x31\x0d\x75\x31\x44\x31\x39\x30\x5f\x75\x31\x44\x31\x38\ +\x46\x0d\x75\x31\x44\x31\x39\x30\x5f\x75\x31\x44\x31\x39\x31\x0d\ +\x75\x31\x44\x31\x38\x44\x5f\x75\x31\x44\x31\x39\x31\x0d\x75\x31\ +\x44\x31\x39\x31\x5f\x75\x31\x44\x31\x38\x45\x14\x75\x31\x44\x31\ +\x38\x44\x5f\x75\x31\x44\x31\x39\x31\x5f\x75\x31\x44\x31\x38\x45\ +\x15\x6d\x75\x6c\x74\x69\x70\x6c\x65\x5f\x6d\x65\x61\x73\x75\x72\ +\x65\x5f\x72\x65\x73\x74\x0d\x75\x31\x44\x31\x30\x37\x5f\x75\x31\ +\x44\x31\x30\x36\x0a\x75\x31\x44\x31\x32\x32\x2e\x6f\x6c\x64\x0a\ +\x75\x31\x44\x31\x32\x33\x2e\x6f\x6c\x64\x0a\x75\x31\x44\x31\x32\ +\x34\x2e\x6f\x6c\x64\x08\x74\x61\x62\x5f\x73\x69\x67\x6e\x0a\x75\ +\x31\x44\x31\x32\x31\x2e\x6f\x6c\x64\x07\x75\x6e\x69\x45\x30\x31\ +\x30\x16\x63\x72\x65\x73\x63\x65\x6e\x64\x6f\x5f\x66\x72\x6f\x6d\ +\x5f\x73\x69\x6c\x65\x6e\x63\x65\x05\x73\x77\x65\x6c\x6c\x0c\x6d\ +\x75\x74\x65\x5f\x73\x74\x72\x69\x6e\x67\x73\x0a\x75\x31\x44\x31\ +\x42\x30\x2e\x61\x6c\x74\x07\x75\x6e\x69\x45\x30\x31\x38\x0f\x75\ +\x6e\x69\x32\x36\x36\x45\x5f\x75\x6e\x69\x32\x36\x36\x45\x0f\x75\ +\x6e\x69\x32\x36\x36\x45\x5f\x75\x6e\x69\x32\x36\x36\x44\x0f\x75\ +\x6e\x69\x32\x36\x36\x45\x5f\x75\x6e\x69\x32\x36\x36\x46\x0a\x68\ +\x61\x6c\x66\x5f\x73\x68\x61\x72\x70\x10\x73\x68\x61\x72\x70\x5f\ +\x61\x6e\x64\x5f\x61\x5f\x68\x61\x6c\x66\x09\x68\x61\x6c\x66\x5f\ +\x66\x6c\x61\x74\x0f\x66\x6c\x61\x74\x5f\x61\x6e\x64\x5f\x61\x5f\ +\x68\x61\x6c\x66\x07\x75\x6e\x69\x45\x30\x32\x30\x07\x75\x6e\x69\ +\x45\x30\x32\x31\x07\x75\x6e\x69\x45\x30\x32\x35\x07\x75\x6e\x69\ +\x45\x30\x32\x38\x07\x75\x6e\x69\x45\x30\x33\x32\x07\x75\x6e\x69\ +\x45\x30\x33\x33\x07\x75\x6e\x69\x45\x30\x34\x32\x07\x75\x6e\x69\ +\x45\x30\x34\x41\x07\x75\x6e\x69\x45\x30\x35\x30\x07\x75\x6e\x69\ +\x45\x30\x35\x31\x07\x75\x6e\x69\x45\x30\x35\x35\x07\x75\x6e\x69\ +\x45\x30\x35\x43\x07\x75\x6e\x69\x45\x30\x35\x44\x07\x75\x6e\x69\ +\x45\x30\x35\x46\x07\x75\x6e\x69\x45\x30\x36\x31\x07\x75\x6e\x69\ +\x45\x30\x36\x32\x07\x75\x6e\x69\x45\x30\x36\x35\x07\x75\x6e\x69\ +\x45\x30\x36\x39\x06\x75\x31\x44\x31\x30\x30\x06\x75\x31\x44\x31\ +\x30\x31\x06\x75\x31\x44\x31\x30\x32\x06\x75\x31\x44\x31\x30\x33\ +\x06\x75\x31\x44\x31\x30\x34\x06\x75\x31\x44\x31\x30\x35\x06\x75\ +\x31\x44\x31\x30\x36\x06\x75\x31\x44\x31\x30\x37\x06\x75\x31\x44\ +\x31\x30\x38\x06\x75\x31\x44\x31\x30\x39\x06\x75\x31\x44\x31\x30\ +\x41\x06\x75\x31\x44\x31\x30\x42\x06\x75\x31\x44\x31\x30\x43\x06\ +\x75\x31\x44\x31\x30\x44\x06\x75\x31\x44\x31\x30\x45\x06\x75\x31\ +\x44\x31\x30\x46\x06\x75\x31\x44\x31\x31\x30\x06\x75\x31\x44\x31\ +\x31\x31\x06\x75\x31\x44\x31\x31\x32\x06\x75\x31\x44\x31\x31\x33\ +\x06\x75\x31\x44\x31\x31\x34\x06\x75\x31\x44\x31\x31\x35\x06\x75\ +\x31\x44\x31\x31\x36\x06\x75\x31\x44\x31\x31\x37\x06\x75\x31\x44\ +\x31\x31\x38\x06\x75\x31\x44\x31\x31\x39\x06\x75\x31\x44\x31\x31\ +\x41\x06\x75\x31\x44\x31\x31\x42\x06\x75\x31\x44\x31\x31\x43\x06\ +\x75\x31\x44\x31\x31\x44\x06\x75\x31\x44\x31\x31\x45\x06\x75\x31\ +\x44\x31\x31\x46\x06\x75\x31\x44\x31\x32\x30\x06\x75\x31\x44\x31\ +\x32\x31\x06\x75\x31\x44\x31\x32\x32\x06\x75\x31\x44\x31\x32\x33\ +\x06\x75\x31\x44\x31\x32\x34\x06\x75\x31\x44\x31\x32\x35\x06\x75\ +\x31\x44\x31\x32\x36\x06\x75\x31\x44\x31\x32\x39\x06\x75\x31\x44\ +\x31\x32\x41\x06\x75\x31\x44\x31\x32\x42\x06\x75\x31\x44\x31\x32\ +\x43\x06\x75\x31\x44\x31\x32\x44\x06\x75\x31\x44\x31\x32\x45\x06\ +\x75\x31\x44\x31\x32\x46\x06\x75\x31\x44\x31\x33\x30\x06\x75\x31\ +\x44\x31\x33\x31\x06\x75\x31\x44\x31\x33\x32\x06\x75\x31\x44\x31\ +\x33\x33\x06\x75\x31\x44\x31\x33\x34\x06\x75\x31\x44\x31\x33\x35\ +\x06\x75\x31\x44\x31\x33\x36\x06\x75\x31\x44\x31\x33\x37\x06\x75\ +\x31\x44\x31\x33\x38\x06\x75\x31\x44\x31\x33\x39\x06\x75\x31\x44\ +\x31\x33\x41\x06\x75\x31\x44\x31\x33\x42\x06\x75\x31\x44\x31\x33\ +\x43\x06\x75\x31\x44\x31\x33\x44\x06\x75\x31\x44\x31\x33\x45\x06\ +\x75\x31\x44\x31\x33\x46\x06\x75\x31\x44\x31\x34\x30\x06\x75\x31\ +\x44\x31\x34\x31\x06\x75\x31\x44\x31\x34\x32\x06\x75\x31\x44\x31\ +\x34\x33\x06\x75\x31\x44\x31\x34\x34\x06\x75\x31\x44\x31\x34\x35\ +\x06\x75\x31\x44\x31\x34\x36\x06\x75\x31\x44\x31\x34\x37\x06\x75\ +\x31\x44\x31\x34\x38\x06\x75\x31\x44\x31\x34\x39\x06\x75\x31\x44\ +\x31\x34\x41\x06\x75\x31\x44\x31\x34\x42\x06\x75\x31\x44\x31\x34\ +\x43\x06\x75\x31\x44\x31\x34\x44\x06\x75\x31\x44\x31\x34\x45\x06\ +\x75\x31\x44\x31\x34\x46\x06\x75\x31\x44\x31\x35\x30\x06\x75\x31\ +\x44\x31\x35\x31\x06\x75\x31\x44\x31\x35\x32\x06\x75\x31\x44\x31\ +\x35\x33\x06\x75\x31\x44\x31\x35\x34\x06\x75\x31\x44\x31\x35\x35\ +\x06\x75\x31\x44\x31\x35\x36\x06\x75\x31\x44\x31\x35\x37\x06\x75\ +\x31\x44\x31\x35\x38\x06\x75\x31\x44\x31\x35\x39\x06\x75\x31\x44\ +\x31\x35\x41\x06\x75\x31\x44\x31\x35\x42\x06\x75\x31\x44\x31\x35\ +\x43\x06\x75\x31\x44\x31\x35\x44\x06\x75\x31\x44\x31\x35\x45\x06\ +\x75\x31\x44\x31\x35\x46\x06\x75\x31\x44\x31\x36\x30\x06\x75\x31\ +\x44\x31\x36\x31\x06\x75\x31\x44\x31\x36\x32\x06\x75\x31\x44\x31\ +\x36\x33\x06\x75\x31\x44\x31\x36\x34\x06\x75\x31\x44\x31\x36\x35\ +\x06\x75\x31\x44\x31\x36\x36\x06\x75\x31\x44\x31\x36\x37\x06\x75\ +\x31\x44\x31\x36\x38\x06\x75\x31\x44\x31\x36\x39\x06\x75\x31\x44\ +\x31\x36\x41\x06\x75\x31\x44\x31\x36\x42\x06\x75\x31\x44\x31\x36\ +\x43\x06\x75\x31\x44\x31\x36\x44\x06\x75\x31\x44\x31\x36\x45\x06\ +\x75\x31\x44\x31\x36\x46\x06\x75\x31\x44\x31\x37\x30\x06\x75\x31\ +\x44\x31\x37\x31\x06\x75\x31\x44\x31\x37\x32\x06\x75\x31\x44\x31\ +\x37\x33\x06\x75\x31\x44\x31\x37\x34\x06\x75\x31\x44\x31\x37\x35\ +\x06\x75\x31\x44\x31\x37\x36\x06\x75\x31\x44\x31\x37\x37\x06\x75\ +\x31\x44\x31\x37\x38\x06\x75\x31\x44\x31\x37\x39\x06\x75\x31\x44\ +\x31\x37\x41\x06\x75\x31\x44\x31\x37\x42\x06\x75\x31\x44\x31\x37\ +\x43\x06\x75\x31\x44\x31\x37\x44\x06\x75\x31\x44\x31\x37\x45\x06\ +\x75\x31\x44\x31\x37\x46\x06\x75\x31\x44\x31\x38\x30\x06\x75\x31\ +\x44\x31\x38\x31\x06\x75\x31\x44\x31\x38\x32\x06\x75\x31\x44\x31\ +\x38\x33\x06\x75\x31\x44\x31\x38\x34\x06\x75\x31\x44\x31\x38\x35\ +\x06\x75\x31\x44\x31\x38\x36\x06\x75\x31\x44\x31\x38\x37\x06\x75\ +\x31\x44\x31\x38\x38\x06\x75\x31\x44\x31\x38\x39\x06\x75\x31\x44\ +\x31\x38\x41\x06\x75\x31\x44\x31\x38\x42\x06\x75\x31\x44\x31\x38\ +\x43\x06\x75\x31\x44\x31\x38\x44\x06\x75\x31\x44\x31\x38\x45\x06\ +\x75\x31\x44\x31\x38\x46\x06\x75\x31\x44\x31\x39\x30\x06\x75\x31\ +\x44\x31\x39\x31\x06\x75\x31\x44\x31\x39\x32\x06\x75\x31\x44\x31\ +\x39\x33\x06\x75\x31\x44\x31\x39\x34\x06\x75\x31\x44\x31\x39\x35\ +\x06\x75\x31\x44\x31\x39\x36\x06\x75\x31\x44\x31\x39\x37\x06\x75\ +\x31\x44\x31\x39\x38\x06\x75\x31\x44\x31\x39\x39\x06\x75\x31\x44\ +\x31\x39\x41\x06\x75\x31\x44\x31\x39\x42\x06\x75\x31\x44\x31\x39\ +\x43\x06\x75\x31\x44\x31\x39\x44\x06\x75\x31\x44\x31\x39\x45\x06\ +\x75\x31\x44\x31\x39\x46\x06\x75\x31\x44\x31\x41\x30\x06\x75\x31\ +\x44\x31\x41\x31\x06\x75\x31\x44\x31\x41\x32\x06\x75\x31\x44\x31\ +\x41\x33\x06\x75\x31\x44\x31\x41\x34\x06\x75\x31\x44\x31\x41\x35\ +\x06\x75\x31\x44\x31\x41\x36\x06\x75\x31\x44\x31\x41\x37\x06\x75\ +\x31\x44\x31\x41\x38\x06\x75\x31\x44\x31\x41\x39\x06\x75\x31\x44\ +\x31\x41\x41\x06\x75\x31\x44\x31\x41\x42\x06\x75\x31\x44\x31\x41\ +\x43\x06\x75\x31\x44\x31\x41\x44\x06\x75\x31\x44\x31\x41\x45\x06\ +\x75\x31\x44\x31\x41\x46\x06\x75\x31\x44\x31\x42\x30\x06\x75\x31\ +\x44\x31\x42\x31\x06\x75\x31\x44\x31\x42\x32\x06\x75\x31\x44\x31\ +\x42\x33\x06\x75\x31\x44\x31\x42\x34\x06\x75\x31\x44\x31\x42\x35\ +\x06\x75\x31\x44\x31\x42\x36\x06\x75\x31\x44\x31\x42\x37\x06\x75\ +\x31\x44\x31\x42\x38\x06\x75\x31\x44\x31\x42\x39\x06\x75\x31\x44\ +\x31\x42\x41\x06\x75\x31\x44\x31\x42\x42\x06\x75\x31\x44\x31\x42\ +\x43\x06\x75\x31\x44\x31\x42\x44\x06\x75\x31\x44\x31\x42\x45\x06\ +\x75\x31\x44\x31\x42\x46\x06\x75\x31\x44\x31\x43\x30\x06\x75\x31\ +\x44\x31\x43\x31\x06\x75\x31\x44\x31\x43\x32\x06\x75\x31\x44\x31\ +\x43\x33\x06\x75\x31\x44\x31\x43\x34\x06\x75\x31\x44\x31\x43\x35\ +\x06\x75\x31\x44\x31\x43\x36\x06\x75\x31\x44\x31\x43\x37\x06\x75\ +\x31\x44\x31\x43\x38\x06\x75\x31\x44\x31\x43\x39\x06\x75\x31\x44\ +\x31\x43\x41\x06\x75\x31\x44\x31\x43\x42\x06\x75\x31\x44\x31\x43\ +\x43\x06\x75\x31\x44\x31\x43\x44\x06\x75\x31\x44\x31\x43\x45\x06\ +\x75\x31\x44\x31\x43\x46\x06\x75\x31\x44\x31\x44\x30\x06\x75\x31\ +\x44\x31\x44\x31\x06\x75\x31\x44\x31\x44\x32\x06\x75\x31\x44\x31\ +\x44\x33\x06\x75\x31\x44\x31\x44\x34\x06\x75\x31\x44\x31\x44\x35\ +\x06\x75\x31\x44\x31\x44\x36\x06\x75\x31\x44\x31\x44\x37\x06\x75\ +\x31\x44\x31\x44\x38\x06\x75\x31\x44\x31\x44\x39\x06\x75\x31\x44\ +\x31\x44\x41\x06\x75\x31\x44\x31\x44\x42\x06\x75\x31\x44\x31\x44\ +\x43\x06\x75\x31\x44\x31\x44\x44\x0d\x75\x31\x44\x31\x41\x30\x5f\ +\x75\x31\x44\x31\x39\x44\x0d\x75\x31\x44\x31\x41\x30\x5f\x75\x31\ +\x44\x31\x39\x43\x00\x00\x00\x00\x00\x00\x01\xff\xff\x00\x02\x00\ +\x01\x00\x00\x00\x0c\x00\x00\x00\x40\x00\x00\x00\x02\x00\x08\x00\ +\x03\x00\x06\x00\x01\x00\x07\x00\x0f\x00\x02\x00\x10\x00\x10\x00\ +\x01\x00\x11\x00\x11\x00\x02\x00\x12\x00\x1c\x00\x01\x00\x1d\x00\ +\x1f\x00\x02\x00\x20\x01\x11\x00\x01\x01\x12\x01\x13\x00\x02\x00\ +\x04\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x5a\x00\ +\x98\x00\x03\x44\x46\x4c\x54\x00\x14\x6c\x61\x74\x6e\x00\x28\x6d\ +\x75\x73\x63\x00\x3c\x00\x04\x00\x00\x00\x00\xff\xff\x00\x05\x00\ +\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x04\x00\x00\x00\x00\xff\ +\xff\x00\x05\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x04\x00\ +\x00\x00\x00\xff\xff\x00\x05\x00\x00\x00\x01\x00\x02\x00\x03\x00\ +\x04\x00\x05\x61\x61\x6c\x74\x00\x20\x63\x63\x6d\x70\x00\x26\x68\ +\x69\x73\x74\x00\x2c\x6c\x69\x67\x61\x00\x32\x73\x73\x30\x31\x00\ +\x38\x00\x00\x00\x01\x00\x01\x00\x00\x00\x01\x00\x04\x00\x00\x00\ +\x01\x00\x02\x00\x00\x00\x01\x00\x03\x00\x00\x00\x01\x00\x00\x00\ +\x05\x00\x0c\x00\x14\x00\x1c\x00\x24\x00\x2c\x00\x01\x00\x00\x00\ +\x01\x00\x28\x00\x01\x00\x00\x00\x01\x00\x2c\x00\x01\x00\x00\x00\ +\x01\x00\x42\x00\x04\x00\x00\x00\x01\x00\x52\x00\x04\x00\x08\x00\ +\x01\x01\x28\x00\x01\x00\x06\xff\x37\x00\x01\x00\x01\x00\xe4\x00\ +\x02\x00\x10\x00\x05\x00\x58\x00\x59\x00\x5a\x00\x57\x00\xe4\x00\ +\x01\x00\x05\x00\x12\x00\x13\x00\x14\x00\x16\x00\x1b\x00\x02\x00\ +\x0e\x00\x04\x00\x16\x00\x12\x00\x13\x00\x14\x00\x02\x00\x01\x00\ +\x57\x00\x5a\x00\x00\x00\x01\x00\xc8\x00\x09\x00\x18\x00\x32\x00\ +\x3c\x00\x46\x00\x50\x00\x5a\x00\x76\x00\x92\x00\xa4\x00\x03\x00\ +\x08\x00\x0e\x00\x14\x00\x1f\x00\x02\x00\x06\x00\x1e\x00\x02\x00\ +\x04\x00\x1d\x00\x02\x00\x05\x00\x01\x00\x04\x00\x08\x00\x02\x00\ +\xc3\x00\x01\x00\x04\x00\x0a\x00\x02\x00\xc5\x00\x01\x00\x04\x00\ +\x0f\x00\x02\x00\xc2\x00\x01\x00\x04\x00\x11\x00\x02\x00\x3c\x00\ +\x03\x00\x08\x00\x10\x00\x16\x00\x0f\x00\x03\x00\xc5\x00\xc2\x00\ +\x0f\x00\x02\x00\x0e\x00\x0d\x00\x02\x00\xc5\x00\x03\x00\x08\x00\ +\x10\x00\x16\x00\x08\x00\x03\x00\xc3\x00\xc3\x00\x08\x00\x02\x00\ +\x07\x00\x07\x00\x02\x00\xc3\x00\x02\x00\x06\x00\x0c\x00\x0c\x00\ +\x02\x00\xc5\x00\x0b\x00\x02\x00\xc3\x00\x04\x00\x0a\x00\x12\x00\ +\x18\x00\x1e\x00\x0a\x00\x03\x00\xc5\x00\xc5\x00\x0e\x00\x02\x00\ +\xc2\x00\x0a\x00\x02\x00\x09\x00\x09\x00\x02\x00\xc5\x00\x01\x00\ +\x09\x00\x05\x00\x07\x00\x09\x00\x0d\x00\x3d\x00\xc1\x00\xc3\x00\ +\xc4\x00\xc5\x00\x01\x00\x1a\x00\x01\x00\x08\x00\x02\x00\x06\x00\ +\x0c\x01\x13\x00\x02\x00\xd0\x01\x12\x00\x02\x00\xd1\x00\x01\x00\ +\x01\x00\xd4\x00\x01\x00\x00\x00\x0a\x00\x42\x00\x50\x00\x03\x44\ +\x46\x4c\x54\x00\x14\x6c\x61\x74\x6e\x00\x20\x6d\x75\x73\x63\x00\ +\x2c\x00\x04\x00\x00\x00\x00\xff\xff\x00\x01\x00\x00\x00\x04\x00\ +\x00\x00\x00\xff\xff\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\xff\ +\xff\x00\x01\x00\x00\x00\x01\x6b\x65\x72\x6e\x00\x08\x00\x00\x00\ +\x01\x00\x00\x00\x01\x00\x04\x00\x02\x00\x00\x00\x01\x00\x08\x00\ +\x01\x00\x7c\x00\x04\x00\x00\x00\x09\x00\x1c\x00\x2e\x00\x40\x00\ +\x2e\x00\x4e\x00\x60\x00\x60\x00\x40\x00\x72\x00\x04\x00\xd0\xfe\ +\x1e\x00\xd1\xfe\x1e\x01\x12\xfe\x1e\x01\x13\xfe\x1e\x00\x04\x00\ +\xd0\xfd\xe9\x00\xd1\xfd\xe9\x01\x12\xfd\xe9\x01\x13\xfd\xe9\x00\ +\x03\x00\xd2\xfe\x14\x00\xd3\xfd\xfc\x00\xd7\xfd\xfc\x00\x04\x00\ +\xd0\xfe\x05\x00\xd1\xfe\x05\x01\x12\xfe\x05\x01\x13\xfe\x05\x00\ +\x04\x00\xd0\xfd\xfb\x00\xd1\xfd\xfb\x01\x12\xfd\xfb\x01\x13\xfd\ +\xfb\x00\x02\x00\xd0\xfd\xe9\x00\xd1\xfd\xe9\x00\x01\x00\x09\x00\ +\xcf\x00\xd0\x00\xd1\x00\xd5\x00\xd6\x00\xd8\x00\xd9\x01\x12\x01\ +\x13\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\xc4\x3e\x8b\xee\x00\ +\x00\x00\x00\xc1\x0c\x00\x54\x00\x00\x00\x00\xc4\xa0\xef\x3b\ +\x00\x00\x07\x7a\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x03\x4b\x7a\x54\x58\x74\x52\x61\x77\x20\x70\x72\x6f\x66\ +\x69\x6c\x65\x20\x74\x79\x70\x65\x20\x65\x78\x69\x66\x00\x00\x78\ +\xda\xed\x97\x5b\x8e\x23\x21\x0c\x45\xff\x59\xc5\x2c\x01\xdb\x18\ +\xc3\x72\xa0\x28\xa4\xd9\xc1\x2c\x7f\x2e\x14\xa9\x3c\x3a\xd3\x4a\ +\x47\xfd\x33\x52\x83\xc2\xc3\x45\xd9\xc6\x07\x9c\xc4\xed\x7f\x7e\ +\x77\xf7\x0b\x85\x72\x8e\x2e\xa8\xa5\x98\x63\xf4\x28\x21\x87\xcc\ +\x05\x83\xe4\x8f\x72\xf4\xe4\xc3\x6c\x67\xe1\xf5\x08\xf3\x3b\xb9\ +\x3b\x1f\x30\x44\x82\x5e\x8e\x69\xdc\xd7\xfa\x02\xb9\x5e\x5f\xb0\ +\xb0\xe4\xf5\x5e\xee\x6c\x5b\x7a\xd2\x52\xb4\x1e\x5c\x14\xca\xb0\ +\x3c\xac\xb5\xe5\xe4\x52\x24\x7c\xc8\x69\xcd\x5d\x5e\x2e\x95\x78\ +\xb3\x9d\xf5\xa9\x8d\x79\xbd\x34\xcb\xe3\x3c\x18\x82\xd1\x14\xfa\ +\x84\x1d\xef\x42\xe2\x67\xbb\x16\x09\xbc\x90\x2c\x05\x7d\x9a\x6d\ +\xe0\x43\x5a\x24\x48\x9c\xad\x7d\x8c\x9f\x3b\x43\xf7\x24\x80\xe7\ +\xe8\x21\x7e\x7e\x5b\x72\xb9\x86\xe3\x50\x74\xd9\x56\x7c\x88\xd3\ +\x92\x93\x3e\x8f\xdf\x8c\xd2\xad\x47\xc4\xa7\x65\xbe\xf5\xa8\x86\ +\xd3\xc4\x87\xf8\xf5\xde\x52\xef\xfb\xb1\xbb\x12\x70\x8e\x72\x88\ +\x6b\x53\x97\xad\xcc\x11\x16\x56\x84\x53\xe6\x6b\x11\xd5\xf0\x51\ +\x8c\x6d\xd6\x8c\x9a\x7c\xf1\x1b\xa8\x35\x6c\xb5\x3a\x5f\x31\xc9\ +\xc4\x88\x78\xa7\x40\x8d\x0a\x75\xda\x67\xbf\xd1\x06\x17\x03\xef\ +\x6c\xe8\x99\x37\x96\x29\x4b\x62\x9c\x79\x9b\x50\xc2\xa8\xd4\xd9\ +\x1c\xf8\x34\xd0\x61\xd9\x40\x4e\x20\xe6\xd3\x17\x9a\x76\xf3\xb0\ +\x07\x63\x09\x96\x1b\x61\x25\x13\x94\x81\xf1\x7d\x75\x8f\x82\x77\ +\xeb\x9d\xa2\xde\xc7\x31\x27\xf2\xe9\x8c\x15\xfc\x9a\x87\x10\x6e\ +\x0c\x72\xa3\xc5\x2a\x00\xa1\xbe\x62\xaa\x33\xbe\xe4\x8e\xce\x3f\ +\x96\x01\x56\x40\x50\x67\x98\x13\x36\x58\x7c\x3d\x54\x54\xa5\xeb\ +\xd9\x92\xc9\x59\xbc\x3a\x2c\x0d\xfe\xb8\x2f\x64\x6d\x29\x40\x88\ +\x60\x5b\xe1\x0c\x09\x08\xf8\x48\xa2\x14\xc9\x1b\xb3\x11\x21\x8e\ +\x09\x7c\x0a\x3c\x67\x1c\xfb\x0a\x02\xa4\x4e\xb9\xc1\x4b\x0e\x22\ +\x11\x70\x12\x0f\xdb\x78\xc7\x68\xae\x65\xe5\x43\x8c\xf4\x02\x10\ +\x8a\x2b\x62\x40\x83\x0b\x04\x58\x21\x68\x88\xb8\x6f\x09\x47\xa8\ +\x38\x15\x0d\xaa\x1a\xd5\x34\x69\xd6\x12\x25\x86\xa8\x31\x46\x8b\ +\x23\x4f\x15\x13\x0b\xa6\x16\xcd\x2c\x59\xb6\x92\x24\x85\xa4\x29\ +\x26\x4b\x29\xe5\x54\x32\x67\x41\x1a\x53\x97\x63\xb6\x9c\x72\xce\ +\xa5\xc0\x68\x09\x05\xba\x0a\xd6\x17\x08\x2a\x57\xa9\xa1\x6a\x8d\ +\xd5\x6a\xaa\xb9\x96\x0d\xc7\x67\x0b\x9b\x6e\x71\xb3\x2d\x6d\x79\ +\x2b\x8d\x9b\x34\xa4\x00\xd7\x62\xb3\x96\x5a\x6e\x65\xa7\x1d\x47\ +\x69\x0f\xbb\xee\x71\xb7\x3d\xed\x79\x2f\x1d\x67\xad\x4b\x0f\x5d\ +\x7b\xec\xd6\x53\xcf\xbd\x9c\xd4\x16\xd5\x7b\x6a\x8f\xe4\x3e\xa7\ +\x46\x8b\x1a\x4f\x50\x63\x9d\x5d\xa9\x41\x6c\x76\x51\x41\x23\x9d\ +\xe8\x60\x06\x62\x1c\x08\xc4\x6d\x10\xc0\x81\xe6\xc1\xcc\x27\x0a\ +\x81\x07\xb9\xc1\xcc\x67\x16\x27\xa2\x0c\x2f\x75\xc0\x69\x34\x88\ +\x81\x60\xd8\x89\xb5\xd3\xc9\xee\x4a\xee\x9f\xdc\x1c\xa2\xfb\x55\ +\x6e\xfc\x8c\x9c\x1b\xe8\xbe\x83\x9c\x1b\xe8\x6e\xc8\x7d\xe4\xf6\ +\x84\x5a\x2b\x33\xdd\xca\x04\x34\x6e\x21\x62\x8a\x0c\x29\xb8\x7e\ +\x58\xb0\xa7\xc2\xa9\x8c\xef\xa5\xb7\x7a\xf7\xee\x8b\x5f\x53\x54\ +\xf7\xb6\x61\xd8\x53\x82\xbf\xbd\x0e\xaf\xc7\x6c\x3c\x7b\x9c\xb9\ +\xcf\x1e\x7e\x36\x4b\xa5\x22\x21\xbf\xe2\x51\xaf\xc8\xce\xa5\xf6\ +\x80\x2f\x78\xf5\xc0\xd9\x1b\x12\x3d\x85\x5b\xe1\x18\x8c\xef\x6c\ +\x34\xee\x32\x78\xaf\x5f\xca\xfc\x48\x6c\xcf\x6d\x2d\xe1\xeb\x2a\ +\xdd\xbb\xbe\xfc\x28\xfa\x51\xf4\xa3\xe8\x47\xd1\xff\xa3\x48\xfb\ +\x9c\xe1\x77\x8b\x21\x49\xbe\x3c\x73\xc7\xd4\xda\xf8\x09\x4f\x47\ +\x32\xd7\xf1\x2f\xec\xe5\x3e\x72\xb3\x4d\xd4\x7d\xd9\xf4\x39\xbb\ +\xdf\x9e\xfb\x8e\xf8\xfc\x77\x8a\xf0\x5f\xa1\xb7\xec\xfe\x02\xcd\ +\x8f\x6b\xf8\x3a\x5d\x42\xce\x00\x00\x01\x84\x69\x43\x43\x50\x49\ +\x43\x43\x20\x70\x72\x6f\x66\x69\x6c\x65\x00\x00\x78\x9c\x7d\x91\ +\x3d\x48\xc3\x40\x1c\xc5\x5f\xd3\x6a\x8b\x54\x04\xed\x20\xe2\x90\ +\xa1\x3a\x59\x10\x15\x71\xd4\x2a\x14\xa1\x42\xa8\x15\x5a\x75\x30\ +\xb9\xf4\x0b\x9a\x34\x24\x29\x2e\x8e\x82\x6b\xc1\xc1\x8f\xc5\xaa\ +\x83\x8b\xb3\xae\x0e\xae\x82\x20\xf8\x01\xe2\xe2\xea\xa4\xe8\x22\ +\x25\xfe\x2f\x29\xb4\x88\xf5\xe0\xb8\x1f\xef\xee\x3d\xee\xde\x01\ +\x42\xbd\xcc\x34\x2b\x30\x0e\x68\xba\x6d\xa6\x12\x71\x31\x93\x5d\ +\x15\x83\xaf\x08\x21\x80\x7e\x74\x63\x46\x66\x96\x31\x27\x49\x49\ +\x74\x1c\x5f\xf7\xf0\xf1\xf5\x2e\xc6\xb3\x3a\x9f\xfb\x73\xf4\xaa\ +\x39\x8b\x01\x3e\x91\x78\x96\x19\xa6\x4d\xbc\x41\x3c\xbd\x69\x1b\ +\x9c\xf7\x89\x23\xac\x28\xab\xc4\xe7\xc4\x63\x26\x5d\x90\xf8\x91\ +\xeb\x8a\xc7\x6f\x9c\x0b\x2e\x0b\x3c\x33\x62\xa6\x53\xf3\xc4\x11\ +\x62\xb1\xd0\xc6\x4a\x1b\xb3\xa2\xa9\x11\x4f\x11\x47\x55\x4d\xa7\ +\x7c\x21\xe3\xb1\xca\x79\x8b\xb3\x56\xae\xb2\xe6\x3d\xf9\x0b\xc3\ +\x39\x7d\x65\x99\xeb\x34\x87\x91\xc0\x22\x96\x20\x41\x84\x82\x2a\ +\x4a\x28\xc3\x46\x8c\x56\x9d\x14\x0b\x29\xda\x8f\x77\xf0\x0f\xb9\ +\x7e\x89\x5c\x0a\xb9\x4a\x60\xe4\x58\x40\x05\x1a\x64\xd7\x0f\xfe\ +\x07\xbf\xbb\xb5\xf2\x93\x13\x5e\x52\x38\x0e\x74\xbd\x38\xce\xc7\ +\x08\x10\xdc\x05\x1a\x35\xc7\xf9\x3e\x76\x9c\xc6\x09\xe0\x7f\x06\ +\xae\xf4\x96\xbf\x52\x07\x66\x3e\x49\xaf\xb5\xb4\xe8\x11\xd0\xb7\ +\x0d\x5c\x5c\xb7\x34\x65\x0f\xb8\xdc\x01\x06\x9f\x0c\xd9\x94\x5d\ +\xc9\x4f\x53\xc8\xe7\x81\xf7\x33\xfa\xa6\x2c\x30\x70\x0b\xf4\xac\ +\x79\xbd\x35\xf7\x71\xfa\x00\xa4\xa9\xab\xe4\x0d\x70\x70\x08\x8c\ +\x16\x28\x7b\xbd\xc3\xbb\x43\xed\xbd\xfd\x7b\xa6\xd9\xdf\x0f\x4d\ +\x30\x72\x98\xc7\x47\xad\xd8\x00\x00\x00\x06\x62\x4b\x47\x44\x00\ +\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09\x70\x48\x59\ +\x73\x00\x00\x45\x7a\x00\x00\x45\x7a\x01\xf5\x6e\xbc\x8a\x00\x00\ +\x00\x07\x74\x49\x4d\x45\x07\xe3\x08\x04\x12\x2e\x2f\x5c\x62\x5c\ +\x71\x00\x00\x02\x20\x49\x44\x41\x54\x58\xc3\xed\x97\xbd\x6b\x14\ +\x51\x14\xc5\x7f\xfb\x92\xd8\xc9\xe0\x47\x20\x98\x26\x45\x10\x2d\ +\x6c\x56\x82\x22\x69\x2c\x84\x34\x16\x31\x10\xff\x80\xa0\x4d\xfe\ +\x06\x0b\xb5\xb6\xd1\x4e\x1b\x3b\xb5\x10\x2c\x5c\xe3\x07\x26\x20\ +\x04\xb1\x50\x08\x82\x11\x59\x70\xd8\xb0\x89\x9f\xb0\x03\x66\x09\ +\x88\x39\x16\xbe\x85\x75\xd8\x79\xef\xce\x5a\x09\x7b\xe0\x31\x30\ +\xef\xbc\x73\xcf\xcc\xbc\xb9\xf7\x3e\x18\xe0\x7f\x40\xbd\x5e\x47\ +\x52\xa9\xd1\x68\x34\x4c\xda\x95\x56\xab\x55\x38\x99\x24\x49\xfe\ +\xd6\x0c\x30\x0d\x1c\x01\x0e\x02\xa3\x40\x05\xf8\x0a\x7c\x07\xde\ +\x03\xab\x40\xad\x7b\x51\x96\x65\xc5\x0e\x22\x4f\x32\x26\xe9\x9c\ +\xa4\xe7\xfa\x1b\x6d\x49\x3f\x24\xed\xaa\x18\x2b\x92\xe6\xbc\x46\ +\x61\x8c\xa2\x89\x11\x49\xd7\x24\xa5\x39\xd1\xba\xa4\x79\x49\x55\ +\x3f\x66\x3c\x2f\x84\xd4\x73\x46\xac\x06\x8e\x4b\xca\x7a\x08\x6d\ +\x48\x1a\x2f\x30\xbc\x4f\xd2\xdb\x88\x91\xcc\x6b\xf7\x34\x50\xf1\ +\xd7\xc5\x80\xc0\x85\xc8\xe7\xda\x23\xe9\x89\xe2\x58\xec\x8e\xd9\ +\x2d\x70\x35\xb0\xa8\x6d\xdc\xfd\x43\x92\x5e\x19\x4c\x5c\xce\xbf\ +\x81\x39\x3f\x51\xb4\xa9\x6e\x97\xf8\x05\x4f\x48\xda\x09\x04\xef\ +\xc4\x98\xed\x18\x48\x24\xad\x47\x1c\x2f\x94\xcc\x03\xcb\x86\xb7\ +\xf0\x4e\x52\xe2\x80\x29\xff\x5f\x87\xf0\xb1\x64\xee\xba\x6e\xe0\ +\x1c\x05\xa6\x1c\x70\xd1\x40\xde\x29\x69\xe0\x01\xb0\x6b\xe0\x2d\ +\x38\xe0\x94\x81\x78\xa0\x8f\x0c\xfe\xd4\xc0\x99\x76\x3e\x9d\xc6\ +\x30\xde\x87\x81\x67\x06\xce\xa8\x03\xb6\x0d\xc4\x93\x7d\x18\xd8\ +\x30\x70\xb6\x1d\xf0\xc6\x40\x3c\xdb\x87\x81\x4f\x06\xce\x6b\x07\ +\xdc\x31\x10\xf7\x03\xc7\x4a\x1a\xa8\x18\x38\xf7\x1c\xf0\xd8\x97\ +\xd3\x18\x2e\x95\x34\x70\x28\x32\xff\x05\x58\x72\x40\x13\xb8\x65\ +\x10\x3c\x0d\x4c\x96\x30\x50\x8d\xcc\xdf\x04\x36\xbb\xb3\xd7\x92\ +\x21\x7b\xdd\xcd\x15\xaf\xd0\x48\x03\x3a\xb5\xa2\x72\x5c\x33\x98\ +\xb8\x62\x08\x5e\x0d\xac\x7f\x18\xea\x07\x86\x7d\x80\x58\x21\xb9\ +\x21\xc9\x05\x2a\xe2\x8b\x80\xf9\x61\x4b\x47\x34\x69\x28\x50\x9f\ +\x7d\xbb\x96\x0f\x7e\xbf\x07\x77\xdd\x6b\x9a\x5b\xb2\xce\x98\x97\ +\xf4\x48\xd2\x66\xc0\xc8\x4f\xdf\x0d\xad\xe5\xee\x6f\xf9\x7d\x75\ +\x3e\x14\xe3\x4f\x57\x12\xc6\x10\x30\x06\x4c\x00\x67\x7c\x56\x3c\ +\xec\xbb\xe2\xbd\x9e\xd3\x06\xbe\x01\x1f\x80\x97\xbe\x0e\xa4\xc0\ +\x16\xf0\xeb\x9f\xcf\x05\x69\x9a\x96\x3e\x17\x34\x9b\xcd\xc1\x81\ +\x6a\x00\x13\x7e\x03\xa8\x32\xb9\xbd\xc4\x2d\xe2\xba\x00\x00\x00\ +\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x0c\x53\ +\x3c\ +\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\xa7\ +\x00\x00\x00\x05\x64\x65\x5f\x44\x45\x42\x00\x00\x00\xa8\x00\x04\ +\x87\x9b\x00\x00\x0a\xa9\x00\x04\xbb\x04\x00\x00\x08\x9b\x00\x04\ +\xd0\x25\x00\x00\x08\xda\x00\x04\xd6\x8d\x00\x00\x0a\xe0\x00\x04\ +\xec\x30\x00\x00\x09\x0f\x00\x04\xf6\x35\x00\x00\x0b\x0b\x00\x05\ +\x8b\xaf\x00\x00\x09\xaa\x00\x05\x98\xc5\x00\x00\x09\xeb\x00\x05\ +\xc4\xaf\x00\x00\x0a\x28\x00\x47\x96\xc4\x00\x00\x00\x00\x00\x51\ +\x4a\x6e\x00\x00\x06\x95\x01\x7e\x97\x89\x00\x00\x04\x21\x03\x18\ +\xa4\x29\x00\x00\x09\x44\x05\x1c\x51\x99\x00\x00\x06\x18\x05\x6c\ +\xc7\x3c\x00\x00\x0a\x67\x05\x6c\xc7\x3c\x00\x00\x0b\x3e\x07\x48\ +\xba\x0e\x00\x00\x02\xff\x0c\x3f\xa2\x40\x00\x00\x05\xc5\x0d\x09\ +\xd2\x63\x00\x00\x08\x44\x0d\x6c\xeb\x9f\x00\x00\x02\xa3\x0f\xc9\ +\x5c\x9e\x00\x00\x00\x2f\x69\x00\x00\x0b\x80\x03\x00\x00\x00\x08\ +\x00\xdc\x00\x62\x00\x65\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\ +\x00\x05\x41\x62\x6f\x75\x74\x07\x00\x00\x00\x0d\x54\x65\x6d\x70\ +\x6c\x61\x74\x65\x41\x62\x6f\x75\x74\x01\x03\x00\x00\x01\x9a\x00\ +\x44\x00\x61\x00\x73\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x67\x00\ +\x72\x00\x61\x00\x6d\x00\x6d\x00\x20\x00\x76\x00\x65\x00\x72\x00\ +\x73\x00\x74\x00\x65\x00\x63\x00\x6b\x00\x74\x00\x20\x00\x73\x00\ +\x69\x00\x63\x00\x68\x00\x20\x00\x6e\x00\x75\x00\x72\x00\x20\x00\ +\x77\x00\x65\x00\x6e\x00\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\ +\x20\x00\x46\x00\x75\x00\x6e\x00\x6b\x00\x74\x00\x69\x00\x6f\x00\ +\x6e\x00\x65\x00\x6e\x00\x20\x00\x77\x00\x69\x00\x65\x00\x20\x00\ +\x41\x00\x6c\x00\x74\x00\x2b\x00\x46\x00\x34\x00\x20\x00\x6f\x00\ +\x64\x00\x65\x00\x72\x00\x20\x00\x64\x00\x61\x00\x73\x00\x20\x00\ +\x5b\x00\x58\x00\x5d\x00\x20\x00\x64\x00\x65\x00\x73\x00\x20\x00\ +\x46\x00\x65\x00\x6e\x00\x73\x00\x74\x00\x65\x00\x72\x00\x6d\x00\ +\x61\x00\x6e\x00\x61\x00\x67\x00\x65\x00\x72\x00\x73\x00\x20\x00\ +\x62\x00\x65\x00\x6e\x00\x75\x00\x74\x00\x7a\x00\x65\x00\x6e\x00\ +\x2e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x6b\x00\xf6\x00\ +\x6e\x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x73\x00\x20\x00\ +\x69\x00\x6d\x00\x20\x00\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\ +\x6f\x00\x6e\x00\x20\x00\x4d\x00\x61\x00\x6e\x00\x61\x00\x67\x00\ +\x65\x00\x72\x00\x20\x00\x6a\x00\x65\x00\x64\x00\x65\x00\x72\x00\ +\x7a\x00\x65\x00\x69\x00\x74\x00\x20\x00\x77\x00\x69\x00\x65\x00\ +\x64\x00\x65\x00\x72\x00\x20\x00\x73\x00\x69\x00\x63\x00\x68\x00\ +\x74\x00\x62\x00\x61\x00\x72\x00\x20\x00\x6d\x00\x61\x00\x63\x00\ +\x68\x00\x65\x00\x6e\x00\x2e\x00\x20\x00\x44\x00\x69\x00\x65\x00\ +\x73\x00\x65\x00\x72\x00\x20\x00\x5a\x00\x75\x00\x73\x00\x74\x00\ +\x61\x00\x6e\x00\x64\x00\x20\x00\x77\x00\x69\x00\x72\x00\x64\x00\ +\x20\x00\x67\x00\x65\x00\x73\x00\x70\x00\x65\x00\x69\x00\x63\x00\ +\x68\x00\x65\x00\x72\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\ +\x00\x00\xb8\x43\x6c\x6f\x73\x69\x6e\x67\x20\x74\x68\x65\x20\x70\ +\x72\x6f\x67\x72\x61\x6d\x20\x77\x69\x74\x68\x20\x74\x68\x65\x20\ +\x5b\x58\x5d\x20\x69\x63\x6f\x6e\x20\x6f\x72\x20\x73\x68\x6f\x72\ +\x74\x63\x75\x74\x73\x20\x6c\x69\x6b\x65\x20\x41\x6c\x74\x2b\x46\ +\x34\x20\x77\x69\x6c\x6c\x20\x6f\x6e\x6c\x79\x20\x68\x69\x64\x65\ +\x20\x74\x68\x65\x20\x77\x69\x6e\x64\x6f\x77\x2e\x20\x54\x68\x69\ +\x73\x20\x73\x74\x61\x74\x65\x20\x77\x69\x6c\x6c\x20\x62\x65\x20\ +\x73\x61\x76\x65\x64\x2c\x20\x73\x6f\x20\x79\x6f\x75\x20\x64\x6f\ +\x6e\x27\x74\x20\x6e\x65\x65\x64\x20\x74\x6f\x20\x73\x65\x65\x20\ +\x74\x68\x65\x20\x77\x69\x6e\x64\x6f\x77\x20\x65\x61\x63\x68\x20\ +\x74\x69\x6d\x65\x20\x79\x6f\x75\x20\x6c\x6f\x61\x64\x20\x79\x6f\ +\x75\x72\x20\x73\x65\x73\x73\x69\x6f\x6e\x2e\x07\x00\x00\x00\x0d\ +\x54\x65\x6d\x70\x6c\x61\x74\x65\x41\x62\x6f\x75\x74\x01\x03\x00\ +\x00\x00\x2a\x00\x57\x00\x75\x00\x73\x00\x73\x00\x74\x00\x65\x00\ +\x6e\x00\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x73\x00\x63\x00\ +\x68\x00\x6f\x00\x6e\x00\x2e\x00\x2e\x00\x2e\x00\x3f\x08\x00\x00\ +\x00\x00\x06\x00\x00\x00\x10\x44\x69\x64\x20\x79\x6f\x75\x20\x6b\ +\x6e\x6f\x77\x2e\x2e\x2e\x3f\x07\x00\x00\x00\x0d\x54\x65\x6d\x70\ +\x6c\x61\x74\x65\x41\x62\x6f\x75\x74\x01\x03\x00\x00\x00\xae\x00\ +\x53\x00\x75\x00\x70\x00\x70\x00\x6f\x00\x72\x00\x74\x00\x20\x00\ +\x75\x00\x6e\x00\x64\x00\x20\x00\x48\x00\x69\x00\x6c\x00\x66\x00\ +\x65\x00\x20\x00\x66\x00\x69\x00\x6e\x00\x64\x00\x65\x00\x6e\x00\ +\x20\x00\x53\x00\x69\x00\x65\x00\x20\x00\x75\x00\x6e\x00\x74\x00\ +\x65\x00\x72\x00\x20\x00\x3c\x00\x61\x00\x20\x00\x68\x00\x72\x00\ +\x65\x00\x66\x00\x3d\x00\x27\x00\x68\x00\x74\x00\x74\x00\x70\x00\ +\x3a\x00\x2f\x00\x2f\x00\x6c\x00\x61\x00\x62\x00\x6f\x00\x72\x00\ +\x65\x00\x6a\x00\x6f\x00\x2e\x00\x6f\x00\x72\x00\x67\x00\x27\x00\ +\x3e\x00\x4c\x00\x61\x00\x62\x00\x6f\x00\x72\x00\x65\x00\x6a\x00\ +\x6f\x00\x20\x00\x43\x00\x6f\x00\x6d\x00\x6d\x00\x75\x00\x6e\x00\ +\x69\x00\x74\x00\x79\x00\x3c\x00\x2f\x00\x61\x00\x3e\x08\x00\x00\ +\x00\x00\x06\x00\x00\x00\x52\x48\x65\x6c\x70\x20\x63\x61\x6e\x20\ +\x62\x65\x20\x66\x6f\x75\x6e\x64\x20\x74\x68\x72\x6f\x75\x67\x68\ +\x20\x74\x68\x65\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x27\x68\x74\ +\x74\x70\x3a\x2f\x2f\x6c\x61\x62\x6f\x72\x65\x6a\x6f\x2e\x6f\x72\ +\x67\x27\x3e\x4c\x61\x62\x6f\x72\x65\x6a\x6f\x20\x43\x6f\x6d\x6d\ +\x75\x6e\x69\x74\x79\x3c\x2f\x61\x3e\x07\x00\x00\x00\x0d\x54\x65\ +\x6d\x70\x6c\x61\x74\x65\x41\x62\x6f\x75\x74\x01\x03\x00\x00\x01\ +\x04\x00\x54\x00\x65\x00\x6d\x00\x70\x00\x6f\x00\x72\x00\xe4\x00\ +\x72\x00\x65\x00\x20\x00\x54\x00\x61\x00\x73\x00\x74\x00\x65\x00\ +\x6e\x00\x6b\x00\xfc\x00\x72\x00\x7a\x00\x65\x00\x6c\x00\x20\x00\ +\x65\x00\x72\x00\x73\x00\x74\x00\x65\x00\x6c\x00\x6c\x00\x65\x00\ +\x6e\x00\x20\x00\x53\x00\x49\x00\x65\x00\x20\x00\x69\x00\x6e\x00\ +\x64\x00\x65\x00\x6d\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\x00\ +\x20\x00\x4e\x00\x75\x00\x6d\x00\x6d\x00\x65\x00\x72\x00\x6e\x00\ +\x62\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x74\x00\x61\x00\x73\x00\ +\x74\x00\x65\x00\x20\x00\x67\x00\x65\x00\x64\x00\x72\x00\xfc\x00\ +\x63\x00\x6b\x00\x74\x00\x20\x00\x77\x00\x69\x00\x72\x00\x64\x00\ +\x20\x00\x77\x00\xe4\x00\x68\x00\x72\x00\x65\x00\x6e\x00\x64\x00\ +\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x4d\x00\x61\x00\x75\x00\ +\x73\x00\x7a\x00\x65\x00\x69\x00\x67\x00\x65\x00\x72\x00\x20\x00\ +\x61\x00\x75\x00\x66\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\x00\ +\x6e\x00\x20\x00\x4d\x00\x65\x00\x6e\x00\xfc\x00\x65\x00\x69\x00\ +\x6e\x00\x74\x00\x72\x00\x61\x00\x67\x00\x20\x00\x7a\x00\x65\x00\ +\x69\x00\x67\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x7e\x54\ +\x65\x6d\x70\x6f\x72\x61\x72\x79\x20\x53\x68\x6f\x72\x74\x63\x75\ +\x74\x73\x20\x63\x61\x6e\x20\x62\x65\x20\x63\x72\x65\x61\x74\x65\ +\x64\x20\x62\x79\x20\x68\x6f\x76\x65\x72\x69\x6e\x67\x20\x74\x68\ +\x65\x20\x6d\x6f\x75\x73\x65\x20\x63\x75\x72\x73\x6f\x72\x20\x6f\ +\x76\x65\x72\x20\x61\x20\x6d\x65\x6e\x75\x20\x65\x6e\x74\x72\x79\ +\x20\x28\x77\x69\x74\x68\x20\x6e\x6f\x20\x73\x68\x6f\x72\x74\x63\ +\x75\x74\x29\x20\x61\x6e\x64\x20\x70\x72\x65\x73\x73\x69\x6e\x67\ +\x20\x61\x20\x6e\x75\x6d\x70\x61\x64\x20\x6b\x65\x79\x07\x00\x00\ +\x00\x0d\x54\x65\x6d\x70\x6c\x61\x74\x65\x41\x62\x6f\x75\x74\x01\ +\x03\x00\x00\x00\x22\x00\x42\x00\x65\x00\x69\x00\x6d\x00\x20\x00\ +\x53\x00\x74\x00\x61\x00\x72\x00\x74\x00\x20\x00\x7a\x00\x65\x00\ +\x69\x00\x67\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\ +\x0f\x73\x68\x6f\x77\x20\x6f\x6e\x20\x73\x74\x61\x72\x74\x75\x70\ +\x07\x00\x00\x00\x0d\x54\x65\x6d\x70\x6c\x61\x74\x65\x41\x62\x6f\ +\x75\x74\x01\x03\x00\x00\x00\x32\x00\x53\x00\x65\x00\x73\x00\x73\ +\x00\x69\x00\x6f\x00\x6e\x00\x76\x00\x65\x00\x72\x00\x7a\x00\x65\ +\x00\x69\x00\x63\x00\x68\x00\x6e\x00\x69\x00\x73\x00\x20\x00\x77\ +\x00\xe4\x00\x68\x00\x6c\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\ +\x00\x00\x00\x18\x43\x68\x6f\x6f\x73\x65\x20\x53\x65\x73\x73\x69\ +\x6f\x6e\x20\x44\x69\x72\x65\x63\x74\x6f\x72\x79\x07\x00\x00\x00\ +\x1e\x54\x65\x6d\x70\x6c\x61\x74\x65\x43\x68\x6f\x6f\x73\x65\x53\ +\x65\x73\x73\x69\x6f\x6e\x44\x69\x72\x65\x63\x74\x6f\x72\x79\x01\ +\x03\x00\x00\x01\x02\x00\x42\x00\x69\x00\x74\x00\x74\x00\x65\x00\ +\x20\x00\x77\x00\xe4\x00\x68\x00\x6c\x00\x65\x00\x6e\x00\x20\x00\ +\x53\x00\x69\x00\x65\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x20\x00\ +\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x76\x00\ +\x65\x00\x72\x00\x7a\x00\x65\x00\x69\x00\x63\x00\x68\x00\x6e\x00\ +\x69\x00\x73\x00\x2e\x00\x20\x00\x44\x00\x61\x00\x72\x00\xfc\x00\ +\x62\x00\x65\x00\x72\x00\x68\x00\x69\x00\x6e\x00\x61\x00\x75\x00\ +\x73\x00\x20\x00\x77\x00\x69\x00\x72\x00\x64\x00\x20\x00\x65\x00\ +\x6d\x00\x70\x00\x66\x00\x6f\x00\x68\x00\x6c\x00\x65\x00\x6e\x00\ +\x20\x00\x73\x00\x74\x00\x61\x00\x74\x00\x74\x00\x64\x00\x65\x00\ +\x73\x00\x73\x00\x65\x00\x6e\x00\x20\x00\xfc\x00\x62\x00\x65\x00\ +\x72\x00\x20\x00\x41\x00\x67\x00\x6f\x00\x72\x00\x64\x00\x65\x00\ +\x6a\x00\x6f\x00\x20\x00\x28\x00\x4e\x00\x65\x00\x77\x00\x20\x00\ +\x53\x00\x65\x00\x73\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\ +\x4d\x00\x61\x00\x6e\x00\x61\x00\x67\x00\x65\x00\x72\x00\x29\x00\ +\x20\x00\x7a\x00\x75\x00\x20\x00\x73\x00\x74\x00\x61\x00\x72\x00\ +\x74\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\ +\x7a\x50\x6c\x65\x61\x73\x65\x20\x63\x68\x6f\x6f\x73\x65\x20\x61\ +\x20\x64\x69\x72\x65\x63\x74\x6f\x72\x79\x20\x66\x6f\x72\x20\x79\ +\x6f\x75\x72\x20\x73\x65\x73\x73\x69\x6f\x6e\x20\x66\x69\x6c\x65\ +\x73\x2e\x20\x49\x74\x20\x69\x73\x20\x72\x65\x63\x6f\x6d\x6d\x65\ +\x6e\x64\x65\x64\x20\x74\x6f\x20\x73\x74\x61\x72\x74\x20\x74\x68\ +\x72\x6f\x75\x67\x68\x20\x41\x67\x6f\x72\x64\x65\x6a\x6f\x2f\x4e\ +\x65\x77\x20\x53\x65\x73\x73\x69\x6f\x6e\x20\x4d\x61\x6e\x61\x67\ +\x65\x72\x20\x69\x6e\x73\x74\x65\x61\x64\x2e\x07\x00\x00\x00\x1e\ +\x54\x65\x6d\x70\x6c\x61\x74\x65\x43\x68\x6f\x6f\x73\x65\x53\x65\ +\x73\x73\x69\x6f\x6e\x44\x69\x72\x65\x63\x74\x6f\x72\x79\x01\x03\ +\x00\x00\x00\x22\x00\x54\x00\x69\x00\x70\x00\x70\x00\x73\x00\x20\ +\x00\x75\x00\x6e\x00\x64\x00\x20\x00\x43\x00\x72\x00\x65\x00\x64\ +\x00\x69\x00\x74\x00\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\ +\x41\x62\x6f\x75\x74\x20\x61\x6e\x64\x20\x54\x69\x70\x73\x07\x00\ +\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\ +\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x14\x00\x42\x00\x65\x00\ +\x61\x00\x72\x00\x62\x00\x65\x00\x69\x00\x74\x00\x65\x00\x6e\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00\x04\x45\x64\x69\x74\x07\x00\x00\ +\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\x69\ +\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x0a\x00\x44\x00\x61\x00\x74\ +\x00\x65\x00\x69\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x46\x69\ +\x6c\x65\x07\x00\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\ +\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x0a\x00\ +\x48\x00\x69\x00\x6c\x00\x66\x00\x65\x08\x00\x00\x00\x00\x06\x00\ +\x00\x00\x04\x48\x65\x6c\x70\x07\x00\x00\x00\x12\x54\x65\x6d\x70\ +\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\x77\x01\x03\ +\x00\x00\x00\x2a\x00\x51\x00\x75\x00\x69\x00\x74\x00\x20\x00\x28\ +\x00\x6f\x00\x68\x00\x6e\x00\x65\x00\x20\x00\x53\x00\x70\x00\x65\ +\x00\x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x00\x29\x08\x00\ +\x00\x00\x00\x06\x00\x00\x00\x15\x51\x75\x69\x74\x20\x28\x77\x69\ +\x74\x68\x6f\x75\x74\x20\x73\x61\x76\x69\x6e\x67\x29\x07\x00\x00\ +\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\x69\ +\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x16\x00\x57\x00\x69\x00\x65\ +\x00\x64\x00\x65\x00\x72\x00\x68\x00\x6f\x00\x6c\x00\x65\x00\x6e\ +\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x52\x65\x64\x6f\x07\x00\ +\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\ +\x69\x6e\x64\x6f\x77\x01\x03\x00\x00\x00\x12\x00\x53\x00\x70\x00\ +\x65\x00\x69\x00\x63\x00\x68\x00\x65\x00\x72\x00\x6e\x08\x00\x00\ +\x00\x00\x06\x00\x00\x00\x04\x53\x61\x76\x65\x07\x00\x00\x00\x12\ +\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\x69\x6e\x64\ +\x6f\x77\x01\x03\x00\x00\x00\x14\x00\x52\x00\xfc\x00\x63\x00\x6b\ +\x00\x67\x00\xe4\x00\x6e\x00\x67\x00\x69\x00\x67\x08\x00\x00\x00\ +\x00\x06\x00\x00\x00\x04\x55\x6e\x64\x6f\x07\x00\x00\x00\x12\x54\ +\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\x69\x6e\x64\x6f\ +\x77\x01\x03\x00\x00\x00\x10\x00\x48\x00\x61\x00\x6e\x00\x64\x00\ +\x62\x00\x75\x00\x63\x00\x68\x08\x00\x00\x00\x00\x06\x00\x00\x00\ +\x0b\x55\x73\x65\x72\x20\x4d\x61\x6e\x75\x61\x6c\x07\x00\x00\x00\ +\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x4d\x61\x69\x6e\x57\x69\x6e\ +\x64\x6f\x77\x01\x03\x00\x00\x00\x0c\x00\x5a\x00\x75\x00\x72\x00\ +\xfc\x00\x63\x00\x6b\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x42\ +\x61\x63\x6b\x07\x00\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\ +\x55\x73\x65\x72\x4d\x61\x6e\x75\x61\x6c\x01\x03\xff\xff\xff\xff\ +\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x46\x6f\x72\x6d\x07\x00\ +\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x55\x73\x65\x72\x4d\ +\x61\x6e\x75\x61\x6c\x01\x03\x00\x00\x00\x08\x00\x48\x00\x6f\x00\ +\x6d\x00\x65\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\x48\x6f\x6d\ +\x65\x07\x00\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\x55\x73\ +\x65\x72\x4d\x61\x6e\x75\x61\x6c\x01\x03\x00\x00\x00\x10\x00\x48\ +\x00\x61\x00\x6e\x00\x64\x00\x62\x00\x75\x00\x63\x00\x68\x08\x00\ +\x00\x00\x00\x06\x00\x00\x00\x0b\x55\x73\x65\x72\x20\x4d\x61\x6e\ +\x75\x61\x6c\x07\x00\x00\x00\x12\x54\x65\x6d\x70\x6c\x61\x74\x65\ +\x55\x73\x65\x72\x4d\x61\x6e\x75\x61\x6c\x01\x88\x00\x00\x00\x02\ +\x01\x01\ +" + +qt_resource_name = b"\ +\x00\x08\ +\x0c\x47\x27\xe5\ +\x00\x74\ +\x00\x65\x00\x6d\x00\x70\x00\x6c\x00\x61\x00\x74\x00\x65\ +\x00\x0c\ +\x0d\xfc\x11\x13\ +\x00\x74\ +\x00\x72\x00\x61\x00\x6e\x00\x73\x00\x6c\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\ +\x00\x05\ +\x00\x6f\xa6\x53\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x73\ +\x00\x0b\ +\x09\xc1\xce\x26\ +\x00\x65\ +\x00\x75\x00\x74\x00\x65\x00\x72\x00\x70\x00\x65\x00\x2e\x00\x74\x00\x74\x00\x66\ +\x00\x0d\ +\x0d\x4c\x02\x07\ +\x00\x77\ +\x00\x68\x00\x6f\x00\x6c\x00\x65\x00\x4e\x00\x6f\x00\x74\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x05\ +\x00\x6a\x85\x7d\ +\x00\x64\ +\x00\x65\x00\x2e\x00\x71\x00\x6d\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x02\ +\x00\x00\x00\x34\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ +\x00\x00\x00\x44\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x01\x00\x00\x00\x05\ +\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x00\xce\x32\ +\x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\xc6\xb4\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x34\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x44\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x73\x80\x8b\x3f\x01\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x01\x00\x00\x00\x05\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x00\xce\x32\ +\x00\x00\x01\x73\x80\x8c\x2c\x95\ +\x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\xc6\xb4\ +\x00\x00\x01\x73\x80\x8b\x3e\xfe\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/template/qtgui/resources/buildresources.sh b/template/qtgui/resources/buildresources.sh new file mode 100644 index 0000000..2bb5bfa --- /dev/null +++ b/template/qtgui/resources/buildresources.sh @@ -0,0 +1,7 @@ +#!/bin/bash +#Template Resources +#Common elements like button icons or translations for the shared about help messages. + +#https://doc.qt.io/qt-5/resources.html +#Resources are kept up-to-date upstream. They are not part of the make and build process. +pyrcc5 -no-compress resources.qrc -o ../resources.py diff --git a/template/qtgui/resources/euterpe.license.txt b/template/qtgui/resources/euterpe.license.txt new file mode 100644 index 0000000..f7c0eae --- /dev/null +++ b/template/qtgui/resources/euterpe.license.txt @@ -0,0 +1,94 @@ +Euterpe Font - Ben Laenen, SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + + + + + + diff --git a/template/qtgui/resources/euterpe.ttf b/template/qtgui/resources/euterpe.ttf new file mode 100644 index 0000000..2ce9462 Binary files /dev/null and b/template/qtgui/resources/euterpe.ttf differ diff --git a/template/qtgui/resources/icons/wholeNote.png b/template/qtgui/resources/icons/wholeNote.png new file mode 100644 index 0000000..65cfa97 Binary files /dev/null and b/template/qtgui/resources/icons/wholeNote.png differ diff --git a/template/qtgui/resources/resources.qrc b/template/qtgui/resources/resources.qrc new file mode 100644 index 0000000..98d1153 --- /dev/null +++ b/template/qtgui/resources/resources.qrc @@ -0,0 +1,7 @@ + + + euterpe.ttf + translations/de.qm + icons/wholeNote.png + + diff --git a/template/qtgui/resources/translations/config.pro b/template/qtgui/resources/translations/config.pro new file mode 100644 index 0000000..72b372f --- /dev/null +++ b/template/qtgui/resources/translations/config.pro @@ -0,0 +1,8 @@ +SOURCES = ../../about.py \ + ../../designer/about.py \ + ../../chooseSessionDirectory.py \ + ../../usermanual.py \ + ../../designer/usermanual.py \ + ../../mainwindow.py \ + ../../menu.py +TRANSLATIONS = de.ts diff --git a/template/qtgui/resources/translations/de.qm b/template/qtgui/resources/translations/de.qm new file mode 100644 index 0000000..2ba84ab Binary files /dev/null and b/template/qtgui/resources/translations/de.qm differ diff --git a/template/qtgui/resources/translations/de.ts b/template/qtgui/resources/translations/de.ts new file mode 100644 index 0000000..cbeb3ed --- /dev/null +++ b/template/qtgui/resources/translations/de.ts @@ -0,0 +1,121 @@ + + + + + TemplateAbout + + + Help can be found through the <a href='http://laborejo.org'>Laborejo Community</a> + Support und Hilfe finden Sie unter <a href='http://laborejo.org'>Laborejo Community</a> + + + + Temporary Shortcuts can be created by hovering the mouse cursor over a menu entry (with no shortcut) and pressing a numpad key + Temporäre Tastenkürzel erstellen SIe indem eine Nummernblocktaste gedrückt wird während der Mauszeiger auf einen Menüeintrag zeigt + + + + Closing the program with the [X] icon or shortcuts like Alt+F4 will only hide the window. This state will be saved, so you don't need to see the window each time you load your session. + Das Programm versteckt sich nur wenn Sie Funktionen wie Alt+F4 oder das [X] des Fenstermanagers benutzen. Sie können es im Session Manager jederzeit wieder sichtbar machen. Dieser Zustand wird gespeichert. + + + + About + Über + + + + Did you know...? + Wussten Sie schon...? + + + + show on startup + Beim Start zeigen + + + + TemplateChooseSessionDirectory + + + Choose Session Directory + Sessionverzeichnis wählen + + + + Please choose a directory for your session files. It is recommended to start through Agordejo/New Session Manager instead. + Bitte wählen Sie ein Sessionverzeichnis. Darüberhinaus wird empfohlen stattdessen über Agordejo (New Session Manager) zu starten. + + + + TemplateMainWindow + + + Help + Hilfe + + + + About and Tips + Tipps und Credits + + + + User Manual + Handbuch + + + + Edit + Bearbeiten + + + + Undo + Rückgängig + + + + Redo + Wiederholen + + + + File + Datei + + + + Save + Speichern + + + + Quit (without saving) + Quit (ohne Speichern) + + + + TemplateUserManual + + + User Manual + Handbuch + + + + Form + + + + + Home + Home + + + + Back + Zurück + + + diff --git a/template/qtgui/resources/translations/update.sh b/template/qtgui/resources/translations/update.sh new file mode 100644 index 0000000..7b4c026 --- /dev/null +++ b/template/qtgui/resources/translations/update.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +pylupdate5 config.pro +echo "linguist-qt5 de.ts" diff --git a/template/qtgui/submenus.py b/template/qtgui/submenus.py new file mode 100644 index 0000000..71ac248 --- /dev/null +++ b/template/qtgui/submenus.py @@ -0,0 +1,127 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +Laborejo2 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +from typing import Iterable, Callable, Tuple +from PyQt5 import QtCore, QtGui, QtWidgets +import engine.api as api +from qtgui.constantsAndConfigs import constantsAndConfigs #Client constantsAndConfigs! + +""" +There are two types of submenus in this file. The majority is created in menu.py during start up. +Like Clef, KeySig etc. These don't need to ask for any dynamic values. + +The other is like SecondaryTempoChangeMenu. In menu.py this is bound with a lambda construct so a +new instance gets created each time the action is called by the user. Thats why this function has +self.__call__ in its init. +""" + + +class Submenu(QtWidgets.QDialog): + def __init__(self, mainWindow, labelString, hasOkCancelButtons=False): + super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it. + #self.setModal(True) #we don't need this when called with self.exec() instead of self.show() + self.layout = QtWidgets.QFormLayout() + #self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) + + label = QtWidgets.QLabel(labelString) #"Choose a clef" or so. + self.layout.addWidget(label) + + #self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons. + + if hasOkCancelButtons == 1: #or true + self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self.process) + self.buttonBox.rejected.connect(self.reject) + elif hasOkCancelButtons == 2: #only cancel. #TODO: unpythonic. + self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Cancel) + self.buttonBox.rejected.connect(self.reject) + else: + self.buttonBox = None + + + def keyPressEvent(self, event): + """Escape closes the dialog by default. + We want Enter as "accept value" + All other methods of mixing editing, window focus and signals + results in strange qt behaviour, triggering the api function twice or more. + Especially unitbox.editingFinished is too easy to trigger. + + The key-event method turned out to be the most straightforward way.""" + try: + getattr(self, "process") + k = event.key() #49=1, 50=2 etc. + if k == 0x01000004 or k == 0x01000005: #normal enter or keypad enter + event.ignore() + self.process() + else: #Pressed Esc + self.abortHandler() + super().keyPressEvent(event) + except AttributeError: + super().keyPressEvent(event) + + def showEvent(self, event): + #TODO: not optimal but better than nothing. + super().showEvent(event) + #self.resize(self.layout.geometry().width(), self.layout.geometry().height()) + self.resize(self.childrenRect().height(), self.childrenRect().width()) + self.updateGeometry() + + def abortHandler(self): + pass + + def process(self): + """Careful! Calling this eats python errors without notice. Make sure your objects exists + and your syntax is correct""" + raise NotImplementedError() + self.done(True) + + def __call__(self): + """This instance can be called like a function""" + if self.buttonBox: + self.layout.addWidget(self.buttonBox) + self.setFixedSize(self.layout.geometry().size()) + self.exec() #blocks until the dialog gets closed + +""" +Most submenus have the line "lambda, r, value=value"... +the r is the return value we get automatically from the Qt buttons which need to be handled. +""" + + +class ChooseOne(Submenu): + """A generic submenu that presents a list of options to the users. + Only supports up to ten entries, for number shortcuts""" + def __init__(self, mainWindow, title:str, lst:Iterable[Tuple[str, Callable]]): + if len(lst) > 9: + raise ValueError(f"ChooseOne submenu supports up to nine entries. You have {len(lst)}") + super().__init__(mainWindow, title) + + for number, (prettyname, function) in enumerate(lst): + button = QtWidgets.QPushButton(f"[{number+1}] {prettyname}") + button.setShortcut(QtGui.QKeySequence(str(number+1))) + button.setStyleSheet("Text-align:left; padding: 5px;"); + self.layout.addWidget(button) + button.clicked.connect(function) + button.clicked.connect(self.done) diff --git a/template/qtgui/usermanual.py b/template/qtgui/usermanual.py new file mode 100644 index 0000000..15c752f --- /dev/null +++ b/template/qtgui/usermanual.py @@ -0,0 +1,68 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + +#System Wide Modules +from PyQt5 import QtCore, QtWidgets, QtGui + +#Local Modules +from engine.config import * #imports METADATA +from .designer.usermanual import Ui_TemplateUserManual +from template.start import PATHS + + +class UserManual(QtWidgets.QWidget): + """A modal window that is hidden""" + + def __init__(self, mainWindow): + super().__init__() + self.mainWindow = mainWindow + self.ui = Ui_TemplateUserManual() + self.ui.setupUi(self) + + self.text = self.ui.textBrowser + + self.text.setSearchPaths([PATHS["doc"]]) #it's a list! + self.text.setSource(QtCore.QUrl("index.html")) + + self.text.highlighted.connect(self.blockLink) + + self.ui.back.clicked.connect(self.text.backward) + self.ui.home.clicked.connect(self.text.home) + + self.setWindowTitle(METADATA["name"] + " " + QtCore.QCoreApplication.translate("TemplateUserManual", "User Manual")) + + self.hide() + + def blockLink(self, qurl): + """The browser displays binary "text" when following a link to an image. + prevent that""" + if qurl.url().endswith(".png") or qurl.url().startswith("http") or qurl.url().startswith("mailto"): + #About to click an url. + self.text.setOpenLinks(False) + return False + else: + self.text.setOpenLinks(True) + return True + + def closeEvent(self, event): + self.hide() diff --git a/template/start.py b/template/start.py new file mode 100644 index 0000000..7a3a7c1 --- /dev/null +++ b/template/start.py @@ -0,0 +1,401 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) + +This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), + +This application 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 . +""" + +#This is the first file in the program to be actually executed, after the executable which uses this as first instruction. + +""" +We use a 'wrong' scheme of importing modules here because there are multiple exit conditions, good and bad. +We don't want to use all the libraries, including the big Qt one, only to end up displaying the --version and exit. +Same with the tests if jack or nsm are running. +""" + +#Give at least some feedback when C libs crash. +#Will still not work for the common case that PyQt crashes and ends Python. +#But every bit helps when hunting bugs. +import faulthandler; faulthandler.enable() + + +from engine.config import * #includes METADATA only. No other environmental setup is executed. +from template.qtgui.chooseSessionDirectory import ChooseSessionDirectory +from template.qtgui.helper import setPaletteAndFont #our error boxes shall look like the rest of the program + +""" +Check parameters first. It is possible that we will just --help or --version and exit. In this case +nothing gets loaded. +""" +import argparse +parser = argparse.ArgumentParser(description=f"""{METADATA["name"]} - Version {METADATA["version"]} - Copyright {METADATA["year"]} by {METADATA["author"]} - {METADATA["url"]}""") +parser.add_argument("-v", "--version", action='version', version="{} {}".format(METADATA["name"], METADATA["version"])) +parser.add_argument("-s", "--save", action='store', dest="directory", help="Use this directory to save. Will be created or loaded from if already present. Deactivates Agordejo/New-Session-Manager support.") +parser.add_argument("-p", "--profiler", action='store_true', help="(Development) Run the python profiler and produce a .cprof file at quit. The name will appear in your STDOUT.") +parser.add_argument("-m", "--mute", action='store_true', help="(Development) Use a fake cbox module, effectively deactivating midi and audio.") +parser.add_argument("-V", "--verbose", action='store_true', help="(Development) Switch the logger to INFO and print out all kinds of information to get a high-level idea of what the program is doing.") +args = parser.parse_args() + +import logging +if args.verbose: + logging.basicConfig(level=logging.INFO) #development + #logging.getLogger().setLevel(logging.INFO) #development +else: + logging.basicConfig(level=logging.ERROR) #production + #logging.getLogger().setLevel(logging.ERROR) #production + +logger = logging.getLogger(__name__) +logger.info("import") + + +"""set up python search path before the program starts and cbox gets imported. +We need to be earliest, so let's put it here. +This is influence during compiling by creating a temporary file "compiledprefix.py". +pyzipapp archives that in, when make is finished we delete it. + +#Default mode is a self-contained directory relative to the uncompiled patroneo python start script +""" + +import sys +import os +import os.path +from PyQt5.QtWidgets import QApplication, QStyleFactory +from PyQt5 import QtGui + + +import inspect +def get_script_dir(follow_symlinks=True): + if getattr(sys, 'frozen', False): # py2exe, PyInstaller, cx_Freeze + path = os.path.abspath(sys.executable) + else: + path = inspect.getabsfile(get_script_dir) + if follow_symlinks: + path = os.path.realpath(path) + return os.path.dirname(path) + + +logger.info(f"Script dir: {get_script_dir()}") + +logger.info(f"Python Version {sys.version}") + +try: + from compiledprefix import prefix + compiledVersion = True + logger.info("Compiled prefix found: {}".format(prefix)) +except ModuleNotFoundError as e: + compiledVersion = False + +logger.info("Compiled version: {}".format(compiledVersion)) + +cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"] +logger.info("Our calfbox extension cpython library should be named " + cboxSharedObjectVersionedName) + +#ZippApp with compiledprefix.py +if compiledVersion: + PATHS={ #this gets imported + "root": "", + "bin": os.path.join(prefix, "bin"), + "doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]), + "desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), #not ~/Desktop but our desktop file + "share": os.path.join(prefix, "share", METADATA["shortName"]), + "templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), + #"lib": os.path.join(prefix, "lib", METADATA["shortName"]), #cbox is found via the PYTHONPATH + } + + cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) + _root = os.path.dirname(__file__) + _root = os.path.abspath(os.path.join(_root, "..")) + + import zipfile + import tempfile + logger.info("Extracting shared library to temporary directory") + zipfilePath = get_script_dir().rstrip("/template") + assert zipfile.is_zipfile(zipfilePath), (zipfilePath) #in our tests this worked. but in lss this results not in a zip file header. linux file also says it is no zip. However, unzip works. + #Extract included .so to tmp dir, tmp dir gets garbage collected at the end of our program. + libsharedDir = tempfile.TemporaryDirectory() + with zipfile.ZipFile(zipfilePath, mode="r") as ourzipappfile: + ourzipappfile.extract(f"sitepackages/{cboxSharedObjectVersionedName}", path=libsharedDir.name) + + sys.path.append(os.path.join(zipfilePath,"sitepackages")) + + cboxso = os.path.join(libsharedDir.name, f"sitepackages/{cboxSharedObjectVersionedName}") + logger.info(f"Shared library extracted to: {cboxso}") + os.environ["CALFBOXLIBABSPATH"] = cboxso + +#Not compiled, not installed. Running pure python directly in the source tree. +else: + _root = os.path.dirname(__file__) + _root = os.path.abspath(os.path.join(_root, "..")) + PATHS={ #this gets imported + "root": _root, + "bin": _root, + "doc": os.path.join(_root, "documentation", "out"), + "desktopfile": os.path.join(_root, "desktop", "desktop.desktop"), #not ~/Desktop but our desktop file + "share": os.path.join(_root, "engine", "resources"), + "templateShare": os.path.join(_root, "template", "engine", "resources"), + #"lib": "", #use only system paths + } + + if os.path.exists (os.path.join(_root, "sitepackages", cboxSharedObjectVersionedName)): + os.environ["CALFBOXLIBABSPATH"] = os.path.join(_root, "sitepackages", cboxSharedObjectVersionedName) + #else use system-wide. + + if os.path.exists (os.path.join(_root, "sitepackages", "calfbox", "cbox.py")): + #add to the front to have higher priority than system sitepackages + logger.info("Will attempt to start with local calfbox python module: {}".format(os.path.join(_root, "sitepackages", "calfbox", "cbox.py"))) + sys.path.insert(0, os.path.join(os.path.join(_root, "sitepackages"))) + #else try to use system-wide calfbox. Check for this and if the .so exists at the end of this file. + + +logger.info("PATHS: {}".format(PATHS)) + +#Construct QAppliction before constantsAndCOnfigs, which has the fontDB +#QtGui.QGuiApplication.setDesktopSettingsAware(False) #This will crash with new Qt! +qtApp = QApplication(sys.argv) +setPaletteAndFont(qtApp) +QApplication.setStyle(QStyleFactory.create("Fusion")) +setPaletteAndFont(qtApp) + +def exitWithMessage(message:str): + title = f"""{METADATA["name"]} Error""" + if sys.stdout.isatty(): + sys.exit(title + ": " + message) + else: + from PyQt5.QtWidgets import QMessageBox + #This is the start file for the Qt client so we know at least that Qt is installed and use that for a warning. + QMessageBox.critical(qtApp.desktop(), title, message) + sys.exit(title + ": " + message) + +def setProcessName(executableName): + """From + https://stackoverflow.com/questions/31132342/change-process-name-while-executing-a-python-script + """ + import ctypes, ctypes.util + lib = ctypes.cdll.LoadLibrary(None) + prctl = lib.prctl + prctl.restype = ctypes.c_int + prctl.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_ulong, + ctypes.c_ulong, ctypes.c_ulong] + def set_proctitle(new_title): + result = prctl(15, new_title, 0, 0, 0) + if result != 0: + raise OSError("prctl result: %d" % result) + set_proctitle(executableName.encode()) + + + libpthread_path = ctypes.util.find_library("pthread") + if not libpthread_path: + return + + libpthread = ctypes.CDLL(libpthread_path) + if hasattr(libpthread, "pthread_setname_np"): + _pthread_setname_np = libpthread.pthread_setname_np + + _pthread_self = libpthread.pthread_self + _pthread_self.argtypes = [] + _pthread_self.restype = ctypes.c_void_p + + _pthread_setname_np.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + _pthread_setname_np.restype = ctypes.c_int + + if _pthread_setname_np is None: + return + + _pthread_setname_np(_pthread_self(), executableName.encode()) + +def checkNsmOrExit(prettyName): + """Check for NSM""" + #NSM changes our cwd to whereever we started new-session-manager from. + #print (os.getcwd()) + import sys + from os import getenv + if not getenv("NSM_URL"): #NSMClient checks for this itself but we can anticipate an error and inform the user. + + path = ChooseSessionDirectory(qtApp).path #ChooseSessionDirectory is calling exec. We can't call qtapp.exec_ because that blocks forever, even after quitting the window. + #qSessionDirApp.quit() + #del qSessionDirApp + #path = "/tmp" + + if path: + startPseudoNSMServer(path) + else: + sys.exit() + + #message = f"""Please start {prettyName} only through the New Session Manager (NSM) or use the --save command line parameter.""" + #exitWithMessage(message) + +def _is_jack_running(): + """Check for JACK""" + import ctypes + import os + silent = os.open(os.devnull, os.O_WRONLY) + stdout = os.dup(1) + stderr = os.dup(2) + os.dup2(silent, 1) #stdout + os.dup2(silent, 2) #stderr + cjack = ctypes.cdll.LoadLibrary("libjack.so.0") + class jack_client_t(ctypes.Structure): + _fields_ = [] + cjack.jack_client_open.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)] #the two ints are enum and pointer to enum. #http://jackaudio.org/files/docs/html/group__ClientFunctions.html#gab8b16ee616207532d0585d04a0bd1d60 + cjack.jack_client_open.restype = ctypes.POINTER(jack_client_t) + ctypesJackClient = cjack.jack_client_open("probe".encode("ascii"), 0x01, None) #0x01 is the bit set for do not autostart JackNoStartServer + try: + ret = bool(ctypesJackClient.contents) + except ValueError: #NULL pointer access + ret = False + cjack.jack_client_close(ctypesJackClient) + os.dup2(stdout, 1) #stdout + os.dup2(stderr, 2) #stderr + return ret + +def checkJackOrExit(mute, prettyName): + import sys + if not mute and (not _is_jack_running()): + exitWithMessage("JACK Audio Connection Kit is not running. Please start it.") + +from contextlib import contextmanager +@contextmanager +def profiler(*pargs, **kwds): + """Eventhough this is a context manager we never get past the yield statement because our + program quits the moment we receive NSM quit signal. The only chance is to register a atexit + handler which will be called no matter what""" + if args.profiler: + import tracemalloc + import cProfile + import atexit + def profilerExit(pr): + snapshot = tracemalloc.take_snapshot() + top_stats = snapshot.statistics('lineno') + print("[ Top 10 ]") + for stat in top_stats[:10]: + print(stat) + + from tempfile import NamedTemporaryFile + cprofPath = NamedTemporaryFile().name + ".cprof" + pr.dump_stats(cprofPath) + logger.info("{}: write profiling data to {}".format(METADATA["name"], cprofPath)) + print (f"pyprof2calltree -k -i {cprofPath}") + + pr = cProfile.Profile() + pr.enable() + #Closing and file writing happens in guiMainWindow._nsmQuit + tracemalloc.start() + atexit.register(lambda: profilerExit(pr)) + + #Program execution + yield + +#Catch Exceptions even if PyQt crashes. +import sys +sys._excepthook = sys.excepthook +def exception_hook(exctype, value, traceback): + """This hook purely exists to call sys.exit(1) even on a Qt crash + so that atexit gets triggered""" + #print(exctype, value, traceback) + logger.error("Caught crash in execpthook. Trying too execute atexit anyway") + sys._excepthook(exctype, value, traceback) + sys.exit(1) +sys.excepthook = exception_hook + + +def startPseudoNSMServer(path): + from os import getenv + assert not getenv("NSM_URL") + from .qtgui.nsmsingleserver import startSingleNSMServer + startSingleNSMServer(path) #provides NSM_URL environment variable and a limited drop-in replacement for NSM that will only answer to our application + assert getenv("NSM_URL") + sys.path.append("sitepackages") # If you compiled but did not install you can still run with the local build of cbox in our temp dir sitepackages. Add path to the last place, in case there is an installed or bundled version + +if args.directory: + #Switch to the mode without NSM. + startPseudoNSMServer(args.directory) + +checkNsmOrExit(METADATA["name"]) +checkJackOrExit(args.mute, METADATA["name"]) +try: + #Only cosmetics + setProcessName(METADATA["shortName"]) +except: + pass + +if args.mute: + """This uses the fact that imports are global and only triggered once. + Inside nullbox it will overwrite the actual cbox functions, + so that the rest of the program can naively import cbox directly. + We only need this one line to change between the two cbox modes. + + There are four possible states: + 1) run program locally, system wide calfbox + 2) run program installed, calfbox bundled/installed + both work fine with a simple import calfbox.nullbox because + bundled has already changed the cbox/python search path + + 3) run program locally, calfbox in local tree + Needs adding of the local tree into python search path. + + 4) impossible: run program installed, calfbox in local tree + + """ + if not compiledVersion: + import template.calfbox.py + sys.modules["calfbox"] = sys.modules["template.calfbox.py"] + + import calfbox.nullbox + + +#Make sure calfbox is available. +pycboxfound = False + +if "CALFBOXLIBABSPATH" in os.environ: + logger.info("Looking for calfbox shared library in absolute path: {}".format(os.environ["CALFBOXLIBABSPATH"])) +else: + logger.info("Looking for calfbox shared library systemwide through ctypes.util.find_library") + + +if compiledVersion and not args.mute: + try: + from sitepackages.calfbox import cbox + logger.info(f"Calbox Python module loaded: {os.path.abspath(cbox.__file__)}") + pycboxfound = True + except Exception as e: + print (e) +else: + try: + from calfbox import cbox + logger.info("{}: using cbox python module from {} . Local version has higher priority than system wide.".format(METADATA["name"], os.path.abspath(cbox.__file__))) + pycboxfound = True + except Exception as e: + print (e) + +if not pycboxfound: + print ("Here is some information. Please show this to the developers.") + print (sys.modules["calfbox"].__file__) + if "calfbox" in sys.modules and sys.modules["calfbox"].__file__: + print (sys.modules["calfbox"], "->", os.path.abspath(sys.modules["calfbox"].__file__)) + else: + print ("calfbox python module is not in sys.modules. This means it truly can't be found or you forgot --mute") + + print ("sys.path start and tail:", sys.path[0:5], sys.path[-5:-1]) + exitWithMessage("Calfbox module could not be loaded.") + + +#Capture Ctlr+C / SIGINT and let @atexit handle the rest. +import signal +import sys +def signal_handler(sig, frame): + sys.exit(0) #atexit will trigger +signal.signal(signal.SIGINT, signal_handler)