diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..efa8610 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,127 @@ +# 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/ + + +#nuitka and makefile +*.bin +*.build +Makefile +site-packages +calfbox/.deps +calfbox/build +calfbox/autom4te.cache +calfbox/Makefile.in +calfbox/aclocal.m4 +calfbox/compile +calfbox/config.h +calfbox/config.h.in +calfbox/config.h.in~ +calfbox/config.status +calfbox/configure +calfbox/depcomp +calfbox/install-sh +calfbox/ltmain.sh +calfbox/missing +calfbox/stamp-h1 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..c1c2be3 --- /dev/null +++ b/template/Makefile.in @@ -0,0 +1,89 @@ + +#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 + printf "prefix = \"$(PREFIX)\"" > compiledprefix.py + PYTHONPATH="site-packages" python3 -m nuitka --recurse-all --python-flag -O --warn-unusual-code --warn-implicit-exceptions --recurse-not-to=PyQt5 --show-progress --show-modules --file-reference-choice=runtime $(PROGRAM) + rm compiledprefix.py + +#A mode that just compiles calfbox locally so you can run the whole program standalone without nuitka +calfbox: + mkdir -p site-packages + #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)/../../site-packages + cd template/calfbox && make + cp template/calfbox/.libs/libcalfbox.so.0.0.0 site-packages/"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 ../../site-packages + #The line above is too much for our specialized use-case. We just copy the few files we need manually. + mkdir -p site-packages/calfbox + cp template/calfbox/py/cbox.py site-packages/calfbox + cp template/calfbox/py/_cbox2.py site-packages/calfbox + cp template/calfbox/py/__init__.py site-packages/calfbox + cp template/calfbox/py/metadata.py site-packages/calfbox + cp template/calfbox/py/sfzparser.py site-packages/calfbox + cp template/calfbox/py/nullbox.py site-packages/calfbox + +clean: + cd template/calfbox && make distclean && rm -rf build + + rm -rf site-packages + rm -f "$(PROGRAM).bin" + rm -rf "$(PROGRAM).build" + rm Makefile + +#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/$(PROGRAM).desktop + + #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 site-packages/lib$(PROGRAM).so.$(VERSION) -t $(DESTDIR)$(PREFIX)/lib/$(PROGRAM) + + mkdir -p $(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/$(PROGRAM).desktop + + #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..aa2053a --- /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" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if USE_NCURSES + +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..58522ff --- /dev/null +++ b/template/calfbox/envelope.h @@ -0,0 +1,322 @@ +/* +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) +{ + 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..88d3ed7 --- /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 2019, 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..cd98241 --- /dev/null +++ b/template/calfbox/experiments/meta.py @@ -0,0 +1,602 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2017, 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..6a2368a --- /dev/null +++ b/template/calfbox/experiments/playPatternsAsMeasures.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2017, 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..408f795 --- /dev/null +++ b/template/calfbox/experiments/printAllMidiEvents.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2017, 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..8638c3d --- /dev/null +++ b/template/calfbox/experiments/printAllMidiEventsSpecificPort.py @@ -0,0 +1,35 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2017, 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..d36ef77 --- /dev/null +++ b/template/calfbox/experiments/simplePlayback.py @@ -0,0 +1,30 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2017, 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..1bf8d14 --- /dev/null +++ b/template/calfbox/experiments/simplerPlayback.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Copyright 2017, 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..5954144 --- /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 2018, 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..7ac37d8 --- /dev/null +++ b/template/calfbox/fluid.c @@ -0,0 +1,412 @@ +/* +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)); + + //Log levels in Fluidsynth are handled as settable functions + fluid_set_log_function(FLUID_PANIC, NULL, NULL); + fluid_set_log_function(FLUID_ERR, NULL, NULL); + fluid_set_log_function(FLUID_WARN, NULL, NULL); + fluid_set_log_function(FLUID_DBG, NULL, NULL); + fluid_set_log_function(FLUID_INFO, NULL, NULL); + + + 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..90860f9 --- /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" + +#include +#include +#include +#include +#include + +#if USE_NCURSES + +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..f17950f --- /dev/null +++ b/template/calfbox/sampler.c @@ -0,0 +1,761 @@ +/* +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; + int exgroups[MAX_RELEASED_GROUPS], exgroupcount = 0; + sampler_voice_start(m->voices_free, pv->channel, pv->layer_data, pv->note, pv->vel, exgroups, &exgroupcount); + if (exgroupcount) + sampler_channel_release_groups(pv->channel, pv->note, exgroups, exgroupcount); +} + +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) + 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..9181093 --- /dev/null +++ b/template/calfbox/sampler.h @@ -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 . +*/ + +#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 4 + +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, int exgroups[MAX_RELEASED_GROUPS], int exgroupcount); +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, int *exgroups, int *pexgroupcount); +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, struct sampler_modulation *sm) +{ + float val = (cc_no < 128) ? c->floatcc[cc_no] : sampler_channel_get_expensive_cc(c, v, NULL, cc_no); + if (sm->step) + val = floorf(0.9999f * val * (sm->step + 1)) / sm->step; + if (sm->curve_id || c->program->interpolated_curves[0]) + val = sampler_program_get_curve_value(c->program, sm->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) +{ + if (cc_no < 128) + return c->floatcc[cc_no]; + return sampler_channel_get_expensive_cc(c, NULL, pv, cc_no); +} + +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;; +} + +#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..aa22bae --- /dev/null +++ b/template/calfbox/sampler_api_test.py @@ -0,0 +1,202 @@ +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): + try: + g1.set_param(param, value) + assert False, "Exception with substring %s expected when setting %s to %s, none caught" % (substr, param, value) + except Exception as e: + assert substr in str(e), "Exception with substring %s expected when setting %s to %s, caught another one: %s" % (substr, param, value, str(e)) + +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") +g1.set_param("locc5", "100") +check_exception("locc8", "110", "Conflicting controller") +check_exception("hicc8", "110", "Conflicting controller") + +#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") + +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"]) + +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', + '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', + ] +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" + r2.set_param(param, value1) + verify_region(r2, ["%s=%s" % (param, value1)], rest) + g1.set_param(param, value2) + verify_region(g1, ["%s=%s" % (param, value2)], []) + + r3 = g1.new_child() + verify_region(r3, [], [param]) + verify_region(r3, ["%s=%s" % (param, value2)], [], full=True) + r3.delete() + + verify_region(r2, ["%s=%s" % (param, value1)], rest) + verify_region(r2, ["%s=%s" % (param, value1)], [], full=True) + r2.unset_param(param) + verify_region(r2, [], params_to_test) + + verify_region(r2, ["%s=%s" % (param, value2)], [], full=True) + g1.unset_param(param) + verify_region(r2, [], ["%s=%s" % (param, value2)], full=True) + + 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 %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..e211a59 --- /dev/null +++ b/template/calfbox/sampler_channel.c @@ -0,0 +1,519 @@ +/* +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; + + if (layer->runtime->on_cc.cc_number == cc && + (val >= layer->runtime->on_cc.locc && val <= layer->runtime->on_cc.hicc) && + (compatible_oncc_behaviour || !(old_value >= layer->runtime->on_cc.locc && old_value <= layer->runtime->on_cc.hicc))) + { + struct sampler_voice *v = m->voices_free; + if (!v) + break; + int exgroups[MAX_RELEASED_GROUPS], exgroupcount = 0; + sampler_voice_start(v, c, layer->runtime, layer->runtime->pitch_keycenter, 127, exgroups, &exgroupcount); + sampler_channel_release_groups(c, -1, exgroups, exgroupcount); + } + } + } + } + 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, int exgroups[MAX_RELEASED_GROUPS], int exgroupcount) +{ + if (exgroupcount) + { + FOREACH_VOICE(c->voices_running, v) + { + for (int j = 0; j < exgroupcount; j++) + { + if (v->off_by == exgroups[j] && v->note != note) + { + if (v->layer->off_mode == som_fast) + { + v->released = 1; + cbox_envelope_go_to(&v->amp_env, 15); + } + else + { + v->released = 1; + } + break; + } + } + } + } +} + +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; + 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->use_prevoice) + delayed_layers[dlcount++] = layer->runtime; + else + layers[lcount++] = layer->runtime; + layer = sampler_rll_iterator_next(&iter); + } + + int exgroups[MAX_RELEASED_GROUPS], exgroupcount = 0; + // If running out of polyphony, do not start the note if all the regions cannot be played + if (lcount <= 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]; + 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, &exgroupcount); + } + 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, exgroupcount); +} + +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..aeab6d3 --- /dev/null +++ b/template/calfbox/sampler_impl.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_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') + return atoi(note); + if (nn < 'a' || nn > 'g') + return -1; + 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 -1; +} + +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..5f6f8d7 --- /dev/null +++ b/template/calfbox/sampler_layer.c @@ -0,0 +1,1991 @@ +/* +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 + +void sampler_layer_data_dump_modulations(struct sampler_layer_data *l) +{ + GSList *p = l->modulations; + while(p) + { + struct sampler_modulation *sm = p->data; + printf("%d x %d -> %d : %f : %d\n", sm->src, sm->src2, sm->dest, sm->amount, sm->curve_id); + p = g_slist_next(p); + } +} + +static struct sampler_modulation *sampler_layer_data_find_modulation(struct sampler_layer_data *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, GSList **link_ptr) +{ + GSList *p = l->modulations; + while(p) + { + struct sampler_modulation *sm = p->data; + if (sm->src == src && sm->src2 == src2 && sm->dest == dest) + { + if (link_ptr) + *link_ptr = p; + return sm; + } + p = g_slist_next(p); + } + return NULL; +} + +static struct sampler_modulation *sampler_layer_data_add_modulation(struct sampler_layer_data *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest) +{ + struct sampler_modulation *sm = sampler_layer_data_find_modulation(l, src, src2, dest, NULL); + if (sm) + return sm; + sm = g_malloc0(sizeof(struct sampler_modulation)); + sm->src = src; + sm->src2 = src2; + sm->dest = dest; + sm->amount = 0; + sm->curve_id = 0; + sm->smooth = 0; + sm->step = 0; + sm->has_amount = FALSE; + sm->has_curve = FALSE; + sm->has_smooth = FALSE; + sm->has_step = FALSE; + l->modulations = g_slist_prepend(l->modulations, sm); + return sm; +} + +static inline gboolean is_null_modulation(const struct sampler_modulation *sm) +{ + return !sm->amount && !sm->curve_id && !sm->smooth && !sm->step && + !sm->has_amount && !sm->has_curve && !sm->has_smooth && !sm->has_step; +} + +static inline gboolean is_null_values_modulation(const struct sampler_modulation *sm) +{ + return !sm->amount && !sm->curve_id && !sm->smooth && !sm->step; +} + +void sampler_layer_propagate_modulation(struct sampler_layer *l, const struct sampler_modulation *srcm, gboolean starting) +{ + if (!starting) + { + struct sampler_modulation *dstm = sampler_layer_data_add_modulation(&l->data, srcm->src, srcm->src2, srcm->dest); + if (!dstm->has_amount) + dstm->amount = srcm->amount; + if (!dstm->has_curve) + dstm->curve_id = srcm->curve_id; + if (!dstm->has_smooth) + dstm->smooth = srcm->smooth; + if (!dstm->has_step) + dstm->step = srcm->step; + } + + if (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)) + { + struct sampler_layer *child = value; + sampler_layer_propagate_modulation(child, srcm, FALSE); + } + } +} + +void sampler_layer_set_modulation_amount(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, float amount) +{ + struct sampler_modulation *dstm = sampler_layer_data_add_modulation(&l->data, src, src2, dest); + dstm->has_amount = TRUE; + dstm->amount = amount; + sampler_layer_propagate_modulation(l, dstm, TRUE); +} + +void sampler_layer_set_modulation_curve(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, int curve) +{ + struct sampler_modulation *dstm = sampler_layer_data_add_modulation(&l->data, src, src2, dest); + dstm->has_curve = TRUE; + dstm->curve_id = curve; + sampler_layer_propagate_modulation(l, dstm, TRUE); +} + +void sampler_layer_set_modulation_smooth(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, float amount) +{ + struct sampler_modulation *dstm = sampler_layer_data_add_modulation(&l->data, src, src2, dest); + dstm->has_smooth = TRUE; + dstm->smooth = amount; + sampler_layer_propagate_modulation(l, dstm, TRUE); +} + +void sampler_layer_set_modulation_step(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, float amount) +{ + struct sampler_modulation *dstm = sampler_layer_data_add_modulation(&l->data, src, src2, dest); + dstm->has_step = TRUE; + dstm->step = amount; + sampler_layer_propagate_modulation(l, dstm, TRUE); +} + +static gboolean sampler_layer_data_unset_modulation(struct sampler_layer_data *l, struct sampler_layer_data *parent_data, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, gboolean remove_local, gboolean unset_amount, gboolean unset_curve, gboolean unset_smooth, gboolean unset_step) +{ + GSList *link = NULL; + struct sampler_modulation *sm = sampler_layer_data_find_modulation(l, src, src2, dest, &link); + if (!sm) + return FALSE; + struct sampler_modulation *psm = remove_local && parent_data != NULL ? sampler_layer_data_find_modulation(parent_data, src, src2, dest, NULL) : NULL; + if (unset_amount && sm->has_amount == remove_local) + { + sm->amount = psm ? psm->amount : 0; + if (remove_local) + sm->has_amount = FALSE; + } + if (unset_curve && sm->has_curve == remove_local) + { + sm->curve_id = psm ? psm->curve_id : 0; + if (remove_local) + sm->has_curve = FALSE; + } + if (unset_smooth && sm->has_smooth == remove_local) + { + sm->smooth = psm ? psm->smooth : 0; + if (remove_local) + sm->has_smooth = FALSE; + } + if (unset_step && sm->has_step == remove_local) + { + sm->step = psm ? psm->step : 0; + if (remove_local) + sm->has_step = FALSE; + } + // Delete if it's all default values and it's not overriding anything + if (is_null_modulation(sm)) + l->modulations = g_slist_delete_link(l->modulations, link); + + return TRUE; +} + +static void sampler_layer_unset_modulation(struct sampler_layer*l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, gboolean remove_local, gboolean unset_amount, gboolean unset_curve, gboolean unset_smooth, gboolean unset_step) +{ + if (!sampler_layer_data_unset_modulation(&l->data, l->parent ? &l->parent->data : NULL, src, src2, dest, remove_local, unset_amount, unset_curve, unset_smooth, unset_step)) + return; + + if (l->child_layers) { + // Also recursively remove propagated copies from child layers, if any + 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; + sampler_layer_unset_modulation(child, src, src2, dest, FALSE, unset_amount, unset_curve, unset_smooth, unset_step); + } + } +} + +void sampler_layer_unset_modulation_amount(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, gboolean remove_local) +{ + sampler_layer_unset_modulation(l, src, src2, dest, remove_local, TRUE, FALSE, FALSE, FALSE); +} + +void sampler_layer_unset_modulation_curve(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, gboolean remove_local) +{ + sampler_layer_unset_modulation(l, src, src2, dest, remove_local, FALSE, TRUE, FALSE, FALSE); +} + +void sampler_layer_unset_modulation_smooth(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, gboolean remove_local) +{ + sampler_layer_unset_modulation(l, src, src2, dest, remove_local, FALSE, FALSE, TRUE, FALSE); +} + +void sampler_layer_unset_modulation_step(struct sampler_layer *l, enum sampler_modsrc src, enum sampler_modsrc src2, enum sampler_moddest dest, gboolean remove_local) +{ + sampler_layer_unset_modulation(l, src, src2, dest, remove_local, FALSE, FALSE, FALSE, TRUE); +} + +void sampler_layer_data_add_nif(struct sampler_layer_data *l, SamplerNoteInitFunc notefunc_voice, SamplerNoteInitFunc2 notefunc_prevoice, int variant, float param, gboolean propagating_defaults) +{ + assert(!(notefunc_voice && notefunc_prevoice)); + GSList **list = notefunc_voice ? &l->voice_nifs : &l->prevoice_nifs; + GSList *p = *list; + while(p) + { + struct sampler_noteinitfunc *nif = p->data; + if (nif->notefunc_voice == notefunc_voice && nif->notefunc_prevoice == notefunc_prevoice && nif->variant == variant) + { + // do not overwrite locally set value with defaults + if (propagating_defaults && nif->has_value) + return; + nif->param = param; + nif->has_value = !propagating_defaults; + return; + } + p = g_slist_next(p); + } + struct sampler_noteinitfunc *nif = malloc(sizeof(struct sampler_noteinitfunc)); + nif->notefunc_voice = notefunc_voice; + nif->notefunc_prevoice = notefunc_prevoice; + nif->variant = variant; + nif->param = param; + nif->has_value = !propagating_defaults; + *list = g_slist_prepend(*list, nif); +} + +void sampler_layer_data_remove_nif(struct sampler_layer_data *l, struct sampler_layer_data *parent_data, SamplerNoteInitFunc notefunc_voice, SamplerNoteInitFunc2 notefunc_prevoice, int variant, gboolean remove_propagated) +{ + assert(!(notefunc_voice && notefunc_prevoice)); + GSList **list = notefunc_voice ? &l->voice_nifs : &l->prevoice_nifs; + GSList *p = *list; + while(p) + { + struct sampler_noteinitfunc *nif = p->data; + if (nif->notefunc_voice == notefunc_voice && nif->notefunc_prevoice == notefunc_prevoice && nif->variant == variant && nif->has_value == !remove_propagated) + { + if (!remove_propagated && parent_data) { + // Try to copy over from parent + GSList *q = notefunc_voice ? parent_data->voice_nifs : parent_data->prevoice_nifs; + while(q) +