diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef8a9a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +cmake-build-* +compile_commands.json +build +.idea diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2dc2b39 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,70 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.17) + +PROJECT(XMIDIX VERSION 1.0 LANGUAGES CXX) + +SET(CMAKE_INCLUDE_CURRENT_DIR ON) +SET(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +SET(CMAKE_AUTOUIC ON) +SET(CMAKE_AUTOMOC ON) +SET(CMAKE_AUTORCC ON) + +SET(CMAKE_CXX_STANDARD 20) +SET(CMAKE_CXX_STANDARD_REQUIRED ON) +SET(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s") + +FIND_PACKAGE(Qt5 COMPONENTS Widgets REQUIRED) +FIND_PACKAGE(Threads REQUIRED) +FIND_PACKAGE(spdlog REQUIRED) +FIND_PACKAGE(fmt REQUIRED) + +INCLUDE(FindPkgConfig) +PKG_CHECK_MODULES(ALSA REQUIRED alsa) + +INCLUDE(cmake/Midifile.cmake) + +SET(PROJECT_SOURCES + xmidix.cc + xmidix_player.cc + xmidix_player.h + xmidix_file.cc + xmidix_file.h + xmidix_seq.cc + xmidix_seq.h + xmidix_config.cc + xmidix_config.h + xmidix_playlist.cc + xmidix_playlist.h + resources/resources.qrc + xmidix_vu.cc + xmidix_vu.h + xmidix_piano.cc + xmidix_piano.h + ) + +ADD_EXECUTABLE(xmidix ${PROJECT_SOURCES}) + +TARGET_INCLUDE_DIRECTORIES(xmidix PRIVATE + ${midifile_INCLUDE_DIRS} + ${ALSA_INCLUDE_DIRS} + ) + +TARGET_LINK_LIBRARIES(xmidix PRIVATE + Qt5::Widgets + Threads::Threads + spdlog::spdlog + fmt::fmt + midifile + ${ALSA_LIBRARIES} + ) + +INSTALL(TARGETS xmidix DESTINATION bin) +INSTALL(FILES resources/xmidix.desktop DESTINATION share/applications) + +SET(CPACK_PACKAGE_CONTACT EMAILBEN145@gmail.com) +SET(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) +SET(CPACK_DEBIAN_PACKAGE_DESCRIPTION "hardware MIDI player") +SET(CPACK_DEBIAN_PACKAGE_SECTION sound) +SET(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +INCLUDE(CPack) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..268630c --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +XMIDIX +========= +XMIDIX is a MIDI hardware device player for the X Window System not written in Rust. +dust off the SC-55, load up some MIDIs and hear the music in stunning fidelity the way the artist intended. + +there are plenty of GUIs for playing MIDIs on softsynths but not too many for hardsynths. +XMIDIX was written to fill that gap. + +![SCREENSHOT](./resources/shot.png) + +## REQUIREMENTS +XMIDIX requires the following libraries: + * Qt5 + * spdlog + * fmt + * asound2 + +## KEYBOARD SHORTCUTS + * `SPACE` - play/pause + * `CTRL + o` - load files + * `CTRL + SPACE` - stop + * `CTRL + p` - configuration + * `CTRL + LEFT` - previous + * `CTRL + RIGHT` - next + +## TROUBLESHOOTING +### THERE IS NO SOUND +XMIDIX is a MIDI hardware device player so unless you have MIDI hardware +hooked up to your computer, nothing will happen. if you would like to use this +software with a softsynth, the author recommends [Fluidsynth](https://github.com/FluidSynth/fluidsynth) +with the [Windows](https://musical-artifacts.com/artifacts/713) soundfont. + +### IT SOUNDS WEIRD AFTER I SEEK +you have likely skipped over control messages which set up the instruments, tempo, etc. +use sparingly. + +### I WOULD LIKE TO RECOMMEND THIS TO MY FRIENDS AND FAMILY BUT I DON’T KNOW HOW TO PRONOUNCE THE NAME +XMIDIX must not be spoken out loud but, should you choose to go against the author's wishes, +"X MIDI 10" or "10 MIDI 10" is acceptable. + +### I FOUND A BUG +nice. + +### I DON’T HAVE ANYTHING TO LISTEN TO +start [here](https://archive.org/details/FALCOM_MIDI) + +### DOES THIS WORK ON WINDOWS +the software was written for Linux but some users on the forum +have had luck with [getting it running on Windows](https://goatse.cx/). + +## ACKNOWLEDGEMENTS +XMIDIX uses the [Midifile](https://github.com/craigsapp/midifile) library for SMF parsing. diff --git a/cmake/Midifile.cmake b/cmake/Midifile.cmake new file mode 100644 index 0000000..08a0c9c --- /dev/null +++ b/cmake/Midifile.cmake @@ -0,0 +1,11 @@ +SET(midifile_INCLUDE_DIRS + ${PROJECT_SOURCE_DIR}/midifile/include) + +FILE(GLOB midifile_SOURCES + ${PROJECT_SOURCE_DIR}/midifile/src/*.cpp) + +ADD_LIBRARY(midifile STATIC + ${midifile_SOURCES}) + +TARGET_INCLUDE_DIRECTORIES(midifile PRIVATE + ${midifile_INCLUDE_DIRS}) diff --git a/cmake/XMIDIXED.cmake b/cmake/XMIDIXED.cmake new file mode 100644 index 0000000..696eeac --- /dev/null +++ b/cmake/XMIDIXED.cmake @@ -0,0 +1,22 @@ +SET(XMIDIXED_SOURCES + xmidixed.cc + xmidix_file.cc + pw/midifile.c + ) + +ADD_EXECUTABLE(xmidixed + ${XMIDIXED_SOURCES} + ) + +TARGET_INCLUDE_DIRECTORIES(xmidixed PRIVATE + ${ALSA_INCLUDE_DIRS} + ${SMF_INCLUDE_DIRS} + ) + +TARGET_LINK_LIBRARIES(xmidixed PRIVATE + Qt5::Widgets + spdlog::spdlog + fmt::fmt + ${ALSA_LIBRARIES} + ${SMF_LIBRARIES} + ) diff --git a/midifile/LICENSE.txt b/midifile/LICENSE.txt new file mode 100644 index 0000000..cc3340a --- /dev/null +++ b/midifile/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright (c) 1999-2018, Craig Stuart Sapp +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/midifile/include/Binasc.h b/midifile/include/Binasc.h new file mode 100644 index 0000000..6aa57f0 --- /dev/null +++ b/midifile/include/Binasc.h @@ -0,0 +1,159 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Mon Feb 16 12:26:32 PST 2015 Adapted from binasc program. +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/include/Binasc.h +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// description: Interface to convert bytes between binary and ASCII forms. +// + +#ifndef _BINASC_H_INCLUDED +#define _BINASC_H_INCLUDED + +#include +#include +#include +#include +#include /* needed for MinGW */ + +namespace smf { + +typedef unsigned char uchar; +typedef unsigned short ushort; +typedef unsigned long ulong; + +class Binasc { + + public: + Binasc (void); + + ~Binasc (); + + // functions for setting options: + int setLineLength (int length); + int getLineLength (void); + int setLineBytes (int length); + int getLineBytes (void); + void setComments (int state); + void setCommentsOn (void); + void setCommentsOff (void); + int getComments (void); + void setBytes (int state); + void setBytesOn (void); + void setBytesOff (void); + int getBytes (void); + void setMidi (int state); + void setMidiOn (void); + void setMidiOff (void); + int getMidi (void); + + // functions for converting into a binary file: + int writeToBinary (const std::string& outfile, + const std::string& infile); + int writeToBinary (const std::string& outfile, + std::istream& input); + int writeToBinary (std::ostream& out, + const std::string& infile); + int writeToBinary (std::ostream& out, + std::istream& input); + + // functions for converting into an ASCII file with hex bytes: + int readFromBinary (const std::string& + outfile, + const std::string& infile); + int readFromBinary (const std::string& outfile, + std::istream& input); + int readFromBinary (std::ostream& out, + const std::string& infile); + int readFromBinary (std::ostream& out, + std::istream& input); + + // static functions for writing ordered bytes: + static std::ostream& writeLittleEndianUShort (std::ostream& out, + ushort value); + static std::ostream& writeBigEndianUShort (std::ostream& out, + ushort value); + static std::ostream& writeLittleEndianShort (std::ostream& out, + short value); + static std::ostream& writeBigEndianShort (std::ostream& out, + short value); + static std::ostream& writeLittleEndianULong (std::ostream& out, + ulong value); + static std::ostream& writeBigEndianULong (std::ostream& out, + ulong value); + static std::ostream& writeLittleEndianLong (std::ostream& out, + long value); + static std::ostream& writeBigEndianLong (std::ostream& out, + long value); + static std::ostream& writeLittleEndianFloat (std::ostream& out, + float value); + static std::ostream& writeBigEndianFloat (std::ostream& out, + float value); + static std::ostream& writeLittleEndianDouble (std::ostream& out, + double value); + static std::ostream& writeBigEndianDouble (std::ostream& out, + double value); + + static std::string keyToPitchName (int key); + + protected: + int m_bytesQ; // option for printing hex bytes in ASCII output. + int m_commentsQ; // option for printing comments in ASCII output. + int m_midiQ; // output ASCII data as parsed MIDI file. + int m_maxLineLength;// number of character in ASCII output on a line. + int m_maxLineBytes; // number of hex bytes in ASCII output on a line. + + private: + // helper functions for reading ASCII content to conver to binary: + int processLine (std::ostream& out, + const std::string& input, + int lineNum); + int processAsciiWord (std::ostream& out, + const std::string& input, + int lineNum); + int processStringWord (std::ostream& out, + const std::string& input, + int lineNum); + int processBinaryWord (std::ostream& out, + const std::string& input, + int lineNum); + int processDecimalWord (std::ostream& out, + const std::string& input, + int lineNum); + int processHexWord (std::ostream& out, + const std::string& input, + int lineNum); + int processVlvWord (std::ostream& out, + const std::string& input, + int lineNum); + int processMidiPitchBendWord(std::ostream& out, + const std::string& input, + int lineNum); + int processMidiTempoWord (std::ostream& out, + const std::string& input, + int lineNum); + + // helper functions for reading binary content to convert to ASCII: + int outputStyleAscii (std::ostream& out, std::istream& input); + int outputStyleBinary (std::ostream& out, std::istream& input); + int outputStyleBoth (std::ostream& out, std::istream& input); + int outputStyleMidi (std::ostream& out, std::istream& input); + + // MIDI parsing helper functions: + int readMidiEvent (std::ostream& out, std::istream& infile, + int& trackbytes, int& command); + int getVLV (std::istream& infile, int& trackbytes); + int getWord (std::string& word, const std::string& input, + const std::string& terminators, int index); + +}; + +} // end of namespace smf + +#endif /* _BINASC_H_INCLUDED */ + + + diff --git a/midifile/include/MidiEvent.h b/midifile/include/MidiEvent.h new file mode 100644 index 0000000..39f141e --- /dev/null +++ b/midifile/include/MidiEvent.h @@ -0,0 +1,71 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat Feb 14 21:47:39 PST 2015 +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/include/MidiEvent.h +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: A class which stores a MidiMessage and a timestamp +// for the MidiFile class. +// + +#ifndef _MIDIEVENT_H_INCLUDED +#define _MIDIEVENT_H_INCLUDED + +#include "MidiMessage.h" +#include + +namespace smf { + +class MidiEvent : public MidiMessage { + public: + MidiEvent (void); + MidiEvent (int command); + MidiEvent (int command, int param1); + MidiEvent (int command, int param1, int param2); + MidiEvent (const MidiMessage& message); + MidiEvent (const MidiEvent& mfevent); + MidiEvent (int aTime, int aTrack, + std::vector& message); + + ~MidiEvent (); + + MidiEvent& operator= (const MidiEvent& mfevent); + MidiEvent& operator= (const MidiMessage& message); + MidiEvent& operator= (const std::vector& bytes); + MidiEvent& operator= (const std::vector& bytes); + MidiEvent& operator= (const std::vector& bytes); + + void clearVariables (void); + + // functions related to event linking (note-ons to note-offs). + void unlinkEvent (void); + void unlinkEvents (void); + void linkEvent (MidiEvent* mev); + void linkEvents (MidiEvent* mev); + void linkEvent (MidiEvent& mev); + void linkEvents (MidiEvent& mev); + int isLinked (void) const; + MidiEvent* getLinkedEvent (void); + const MidiEvent* getLinkedEvent (void) const; + int getTickDuration (void) const; + double getDurationInSeconds (void) const; + + int tick; // delta or absolute MIDI ticks + int track; // [original] track number of event in MIDI file + double seconds; // calculated time in sec. (after doTimeAnalysis()) + int seq; // sorting sequence number of event + + private: + MidiEvent* m_eventlink; // used to match note-ons and note-offs + +}; + +} // end of namespace smf + +#endif /* _MIDIEVENT_H_INCLUDED */ + + + diff --git a/midifile/include/MidiEventList.h b/midifile/include/MidiEventList.h new file mode 100644 index 0000000..e3bfb13 --- /dev/null +++ b/midifile/include/MidiEventList.h @@ -0,0 +1,80 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat Feb 14 21:55:38 PST 2015 +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/include/MidiEventList.h +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: A class that stores a MidiEvents for a MidiFile track. +// + +#ifndef _MIDIEVENTLIST_H_INCLUDED +#define _MIDIEVENTLIST_H_INCLUDED + +#include "MidiEvent.h" +#include + +namespace smf { + +class MidiEventList { + public: + MidiEventList (void); + MidiEventList (const MidiEventList& other); + MidiEventList (MidiEventList&& other); + + ~MidiEventList (); + + MidiEventList& operator= (MidiEventList& other); + MidiEvent& operator[] (int index); + const MidiEvent& operator[] (int index) const; + + MidiEvent& back (void); + const MidiEvent& back (void) const; + MidiEvent& last (void); + const MidiEvent& last (void) const; + MidiEvent& getEvent (int index); + const MidiEvent& getEvent (int index) const; + void clear (void); + void reserve (int rsize); + int getEventCount (void) const; + int getSize (void) const; + int size (void) const; + void removeEmpties (void); + int linkNotePairs (void); + int linkEventPairs (void); + void clearLinks (void); + void clearSequence (void); + int markSequence (int sequence = 1); + + int push (MidiEvent& event); + int push_back (MidiEvent& event); + int append (MidiEvent& event); + + // careful when using these, intended for internal use in MidiFile class: + void detach (void); + int push_back_no_copy (MidiEvent* event); + + // access to the list of MidiEvents for sorting with an external function: + MidiEvent** data (void); + + protected: + std::vector list; + + private: + void sort (void); + + // MidiFile class calls sort() + friend class MidiFile; +}; + + +int eventcompare(const void* a, const void* b); + +} // end of namespace smf + +#endif /* _MIDIEVENTLIST_H_INCLUDED */ + + + diff --git a/midifile/include/MidiFile.h b/midifile/include/MidiFile.h new file mode 100644 index 0000000..91c9cc6 --- /dev/null +++ b/midifile/include/MidiFile.h @@ -0,0 +1,322 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Fri Nov 26 14:12:01 PST 1999 +// Last Modified: Mon Jan 18 20:54:04 PST 2021 Added readSmf(). +// Filename: midifile/include/MidiFile.h +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: A class that can read/write Standard MIDI files. +// MIDI data is stored by track in an array. +// + +#ifndef _MIDIFILE_H_INCLUDED +#define _MIDIFILE_H_INCLUDED + +#include "MidiEventList.h" + +#include +#include +#include +#include + +#define TIME_STATE_DELTA 0 +#define TIME_STATE_ABSOLUTE 1 + +#define TRACK_STATE_SPLIT 0 +#define TRACK_STATE_JOINED 1 + +namespace smf { + +class _TickTime { + public: + int tick; + double seconds; +}; + + +class MidiFile { + public: + MidiFile (void); + MidiFile (const std::string& filename); + MidiFile (std::istream& input); + MidiFile (const MidiFile& other); + MidiFile (MidiFile&& other); + + ~MidiFile (); + + MidiFile& operator= (const MidiFile& other); + MidiFile& operator= (MidiFile&& other); + + // Reading/writing functions: + + // Auto-detected SMF or ASCII-encoded SMF (decoded with Binasc class): + bool read (const std::string& filename); + bool read (std::istream& instream); + bool readBase64 (const std::string& base64data); + bool readBase64 (std::istream& instream); + + // Only allow Standard MIDI File input: + bool readSmf (const std::string& filename); + bool readSmf (std::istream& instream); + + bool write (const std::string& filename); + bool write (std::ostream& out); + bool writeBase64 (const std::string& out, int width = 0); + bool writeBase64 (std::ostream& out, int width = 0); + std::string getBase64 (int width = 0); + bool writeHex (const std::string& filename, int width = 25); + bool writeHex (std::ostream& out, int width = 25); + bool writeBinasc (const std::string& filename); + bool writeBinasc (std::ostream& out); + bool writeBinascWithComments (const std::string& filename); + bool writeBinascWithComments (std::ostream& out); + bool status (void) const; + + // track-related functions: + const MidiEventList& operator[] (int aTrack) const; + MidiEventList& operator[] (int aTrack); + int getTrackCount (void) const; + int getNumTracks (void) const; + int size (void) const; + void removeEmpties (void); + + // tick-related functions: + void makeDeltaTicks (void); + void deltaTicks (void); + void makeAbsoluteTicks (void); + void absoluteTicks (void); + int getTickState (void) const; + bool isDeltaTicks (void) const; + bool isAbsoluteTicks (void) const; + + // join/split track functionality: + void joinTracks (void); + void splitTracks (void); + void splitTracksByChannel (void); + int getTrackState (void) const; + int hasJoinedTracks (void) const; + int hasSplitTracks (void) const; + int getSplitTrack (int track, int index) const; + int getSplitTrack (int index) const; + + // track sorting funcionality: + void sortTrack (int track); + void sortTracks (void); + void markSequence (void); + void markSequence (int track, int sequence = 1); + void clearSequence (void); + void clearSequence (int track); + + // track manipulation functionality: + int addTrack (void); + int addTrack (int count); + int addTracks (int count); + void deleteTrack (int aTrack); + void mergeTracks (int aTrack1, int aTrack2); + int getTrackCountAsType1 (void); + + // ticks-per-quarter related functions: + void setMillisecondTicks (void); + int getTicksPerQuarterNote (void) const; + int getTPQ (void) const; + void setTicksPerQuarterNote (int ticks); + void setTPQ (int ticks); + + // physical-time analysis functions: + void doTimeAnalysis (void); + double getTimeInSeconds (int aTrack, int anIndex); + double getTimeInSeconds (int tickvalue); + double getAbsoluteTickTime (double starttime); + int getFileDurationInTicks (void); + double getFileDurationInQuarters (void); + double getFileDurationInSeconds (void); + + // note-analysis functions: + int linkNotePairs (void); + int linkEventPairs (void); + void clearLinks (void); + + // filename functions: + void setFilename (const std::string& aname); + const char* getFilename (void) const; + + // event functionality: + MidiEvent* addEvent (int aTrack, int aTick, + std::vector& midiData); + MidiEvent* addEvent (MidiEvent& mfevent); + MidiEvent* addEvent (int aTrack, MidiEvent& mfevent); + MidiEvent& getEvent (int aTrack, int anIndex); + const MidiEvent& getEvent (int aTrack, int anIndex) const; + int getEventCount (int aTrack) const; + int getNumEvents (int aTrack) const; + void allocateEvents (int track, int aSize); + void erase (void); + void clear (void); + void clear_no_deallocate (void); + + // MIDI message adding convenience functions: + MidiEvent* addNoteOn (int aTrack, int aTick, + int aChannel, int key, + int vel); + MidiEvent* addNoteOff (int aTrack, int aTick, + int aChannel, int key, + int vel); + MidiEvent* addNoteOff (int aTrack, int aTick, + int aChannel, int key); + MidiEvent* addController (int aTrack, int aTick, + int aChannel, int num, + int value); + MidiEvent* addPatchChange (int aTrack, int aTick, + int aChannel, int patchnum); + MidiEvent* addTimbre (int aTrack, int aTick, + int aChannel, int patchnum); + MidiEvent* addPitchBend (int aTrack, int aTick, + int aChannel, double amount); + + // RPN settings: + void setPitchBendRange (int aTrack, int aTick, + int aChannel, double range); + + // Controller message adding convenience functions: + MidiEvent* addSustain (int aTrack, int aTick, + int aChannel, int value); + MidiEvent* addSustainPedal (int aTrack, int aTick, + int aChannel, int value); + MidiEvent* addSustainOn (int aTrack, int aTick, + int aChannel); + MidiEvent* addSustainPedalOn (int aTrack, int aTick, + int aChannel); + MidiEvent* addSustainOff (int aTrack, int aTick, + int aChannel); + MidiEvent* addSustainPedalOff (int aTrack, int aTick, + int aChannel); + + // Meta-event adding convenience functions: + MidiEvent* addMetaEvent (int aTrack, int aTick, + int aType, + std::vector& metaData); + MidiEvent* addMetaEvent (int aTrack, int aTick, + int aType, + const std::string& metaData); + MidiEvent* addText (int aTrack, int aTick, + const std::string& text); + MidiEvent* addCopyright (int aTrack, int aTick, + const std::string& text); + MidiEvent* addTrackName (int aTrack, int aTick, + const std::string& name); + MidiEvent* addInstrumentName (int aTrack, int aTick, + const std::string& name); + MidiEvent* addLyric (int aTrack, int aTick, + const std::string& text); + MidiEvent* addMarker (int aTrack, int aTick, + const std::string& text); + MidiEvent* addCue (int aTrack, int aTick, + const std::string& text); + MidiEvent* addTempo (int aTrack, int aTick, + double aTempo); + MidiEvent* addTimeSignature (int aTrack, int aTick, + int top, int bottom, + int clocksPerClick = 24, + int num32dsPerQuarter = 8); + MidiEvent* addCompoundTimeSignature(int aTrack, int aTick, + int top, int bottom, + int clocksPerClick = 36, + int num32dsPerQuarter = 8); + + uchar readByte (std::istream& input); + + // static functions: + static ushort readLittleEndian2Bytes (std::istream& input); + static ulong readLittleEndian4Bytes (std::istream& input); + static std::ostream& writeLittleEndianUShort (std::ostream& out, + ushort value); + static std::ostream& writeBigEndianUShort (std::ostream& out, + ushort value); + static std::ostream& writeLittleEndianShort (std::ostream& out, + short value); + static std::ostream& writeBigEndianShort (std::ostream& out, + short value); + static std::ostream& writeLittleEndianULong (std::ostream& out, + ulong value); + static std::ostream& writeBigEndianULong (std::ostream& out, + ulong value); + static std::ostream& writeLittleEndianLong (std::ostream& out, + long value); + static std::ostream& writeBigEndianLong (std::ostream& out, + long value); + static std::ostream& writeLittleEndianFloat (std::ostream& out, + float value); + static std::ostream& writeBigEndianFloat (std::ostream& out, + float value); + static std::ostream& writeLittleEndianDouble (std::ostream& out, + double value); + static std::ostream& writeBigEndianDouble (std::ostream& out, + double value); + + protected: + // m_events == Lists of MidiEvents for each MIDI file track. + std::vector m_events; + + // m_ticksPerQuarterNote == A value for the MIDI file header + // which represents the number of ticks in a quarter note + // that are used as units for the delta times for MIDI events + // in MIDI file track data. + int m_ticksPerQuarterNote = 120; + + // m_theTrackState == state variable for whether the tracks + // are joined or split. + int m_theTrackState = TRACK_STATE_SPLIT; + + // m_theTimeState == state variable for whether the MidiEvent::tick + // variable contain absolute ticks since the start of the file's + // time, or delta ticks since the last MIDI event in the track. + int m_theTimeState = TIME_STATE_ABSOLUTE; + + // m_readFileName == the filename of the last file read into + // the object. + std::string m_readFileName; + + // m_timemapvalid == + bool m_timemapvalid = false; + + // m_timemap == + std::vector<_TickTime> m_timemap; + + // m_rwstatus == True if last read was successful, false if a problem. + bool m_rwstatus = true; + + // m_linkedEventQ == True if link analysis has been done. + bool m_linkedEventsQ = false; + + private: + int extractMidiData (std::istream& inputfile, + std::vector& array, + uchar& runningCommand); + ulong readVLValue (std::istream& inputfile); + ulong unpackVLV (uchar a = 0, uchar b = 0, + uchar c = 0, uchar d = 0, + uchar e = 0); + void writeVLValue (long aValue, + std::vector& data); + int makeVLV (uchar *buffer, int number); + static int ticksearch (const void* A, const void* B); + static int secondsearch (const void* A, const void* B); + void buildTimeMap (void); + double linearTickInterpolationAtSecond (double seconds); + double linearSecondInterpolationAtTick (int ticktime); + std::string base64Encode (const std::string &input); + std::string base64Decode (const std::string &input); + static const std::string encodeLookup; + static const std::vector decodeLookup; +}; + +} // end of namespace smf + +std::ostream& operator<<(std::ostream& out, smf::MidiFile& aMidiFile); + +#endif /* _MIDIFILE_H_INCLUDED */ + + + diff --git a/midifile/include/MidiMessage.h b/midifile/include/MidiMessage.h new file mode 100644 index 0000000..56e1548 --- /dev/null +++ b/midifile/include/MidiMessage.h @@ -0,0 +1,176 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat Feb 14 20:36:32 PST 2015 +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/include/MidiMessage.h +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: Storage for bytes of a MIDI message for use in MidiFile +// class. +// + +#ifndef _MIDIMESSAGE_H_INCLUDED +#define _MIDIMESSAGE_H_INCLUDED + +#include +#include + +namespace smf { + +typedef unsigned char uchar; +typedef unsigned short ushort; +typedef unsigned long ulong; + +class MidiMessage : public std::vector { + + public: + MidiMessage (void); + MidiMessage (int command); + MidiMessage (int command, int p1); + MidiMessage (int command, int p1, int p2); + MidiMessage (const MidiMessage& message); + MidiMessage (const std::vector& message); + MidiMessage (const std::vector& message); + MidiMessage (const std::vector& message); + + ~MidiMessage (); + + MidiMessage& operator= (const MidiMessage& message); + MidiMessage& operator= (const std::vector& bytes); + MidiMessage& operator= (const std::vector& bytes); + MidiMessage& operator= (const std::vector& bytes); + + void sortTrack (void); + void sortTrackWithSequence(void); + + // data access convenience functions (returns -1 if not present): + int getP0 (void) const; + int getP1 (void) const; + int getP2 (void) const; + int getP3 (void) const; + void setP0 (int value); + void setP1 (int value); + void setP2 (int value); + void setP3 (int value); + + int getSize (void) const; + void setSize (int asize); + int setSizeToCommand (void); + int resizeToCommand (void); + + // note-message convenience functions: + int getKeyNumber (void) const; + int getVelocity (void) const; + void setKeyNumber (int value); + void setVelocity (int value); + void setSpelling (int base7, int accidental); + void getSpelling (int& base7, int& accidental); + + // controller-message convenience functions: + int getControllerNumber (void) const; + int getControllerValue (void) const; + + int getCommandNibble (void) const; + int getCommandByte (void) const; + int getChannelNibble (void) const; + int getChannel (void) const; + + void setCommandByte (int value); + void setCommand (int value); + void setCommand (int value, int p1); + void setCommand (int value, int p1, int p2); + void setCommandNibble (int value); + void setChannelNibble (int value); + void setChannel (int value); + void setParameters (int p1, int p2); + void setParameters (int p1); + void setMessage (const std::vector& message); + void setMessage (const std::vector& message); + void setMessage (const std::vector& message); + + // message-type convenience functions: + bool isMetaMessage (void) const; + bool isMeta (void) const; + bool isNote (void) const; + bool isNoteOff (void) const; + bool isNoteOn (void) const; + bool isAftertouch (void) const; + bool isController (void) const; + bool isSustain (void) const; // controller 64 + bool isSustainOn (void) const; + bool isSustainOff (void) const; + bool isSoft (void) const; // controller 67 + bool isSoftOn (void) const; + bool isSoftOff (void) const; + bool isPatchChange (void) const; + bool isTimbre (void) const; + bool isPressure (void) const; + bool isPitchbend (void) const; + bool isEmpty (void) const; // see MidiFile::removeEmpties() + + // helper functions to create various MidiMessages: + void makeNoteOn (int channel, int key, int velocity); + void makeNoteOff (int channel, int key, int velocity); + void makeNoteOff (int channel, int key); + void makeNoteOff (void); + void makePatchChange (int channel, int patchnum); + void makeTimbre (int channel, int patchnum); + void makeController (int channel, int num, int value); + + // helper functions to create various continuous controller messages: + void makeSustain (int channel, int value); + void makeSustainPedal (int channel, int value); + void makeSustainOn (int channel); + void makeSustainPedalOn (int channel); + void makeSustainOff (int channel); + void makeSustainPedalOff (int channel); + + // meta-message creation and helper functions: + void makeMetaMessage (int mnum, const std::string& data); + void makeText (const std::string& name); + void makeCopyright (const std::string& text); + void makeTrackName (const std::string& name); + void makeInstrumentName (const std::string& name); + void makeLyric (const std::string& text); + void makeMarker (const std::string& text); + void makeCue (const std::string& text); + void makeTimeSignature (int top, int bottom, + int clocksPerClick = 24, + int num32dsPerQuarter = 8); + + void makeTempo (double tempo) { setTempo(tempo); } + int getTempoMicro (void) const; + int getTempoMicroseconds (void) const; + double getTempoSeconds (void) const; + double getTempoBPM (void) const; + double getTempoTPS (int tpq) const; + double getTempoSPT (int tpq) const; + + int getMetaType (void) const; + bool isText (void) const; + bool isCopyright (void) const; + bool isTrackName (void) const; + bool isInstrumentName (void) const; + bool isLyricText (void) const; + bool isMarkerText (void) const; + bool isTempo (void) const; + bool isTimeSignature (void) const; + bool isKeySignature (void) const; + bool isEndOfTrack (void) const; + + std::string getMetaContent (void); + void setMetaContent (const std::string& content); + void setTempo (double tempo); + void setTempoMicroseconds (int microseconds); + void setMetaTempo (double tempo); + +}; + +} // end of namespace smf + +#endif /* _MIDIMESSAGE_H_INCLUDED */ + + + diff --git a/midifile/include/Options.h b/midifile/include/Options.h new file mode 100644 index 0000000..a3d2e3d --- /dev/null +++ b/midifile/include/Options.h @@ -0,0 +1,156 @@ +// +// Copyright 1998-2018 by Craig Stuart Sapp, All Rights Reserved. +// Programmer: Craig Stuart Sapp +// Creation Date: Sun Apr 5 13:07:18 PDT 1998 +// Last Modified: Mon Jan 18 18:25:23 PST 2021 Some cleanup +// Filename: midifile/include/Options.h +// Web Address: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: Interface for command-line options. +// + +#ifndef _OPTIONS_H_INCLUDED +#define _OPTIONS_H_INCLUDED + +#include +#include +#include +#include + +#define OPTION_TYPE_unknown '\0' +#define OPTION_TYPE_boolean 'b' +#define OPTION_TYPE_char 'c' +#define OPTION_TYPE_double 'd' +#define OPTION_TYPE_float 'f' +#define OPTION_TYPE_int 'i' +#define OPTION_TYPE_string 's' + +namespace smf { + +class Option_register { + public: + Option_register (void); + Option_register (const std::string& aDefinition, + char aType, + const std::string& aDefaultOption); + Option_register (const std::string& aDefinition, + char aType, + const std::string& aDefaultOption, + const std::string& aModifiedOption); + + ~Option_register (); + + void clearModified (void); + const std::string& getDefinition (void); + const std::string& getDefault (void); + const std::string& getOption (void); + const std::string& getModified (void); + const std::string& getDescription (void); + bool isModified (void); + char getType (void); + void reset (void); + void setDefault (const std::string& aString); + void setDefinition (const std::string& aString); + void setDescription (const std::string& aString); + void setModified (const std::string& aString); + void setType (char aType); + std::ostream& print (std::ostream& out); + + protected: + std::string m_definition; + std::string m_description; + std::string m_defaultOption; + std::string m_modifiedOption; + bool m_modifiedQ; + char m_type; + +}; + + + +class Options { + public: + Options (void); + Options (int argc, char** argv); + + ~Options (); + + int argc (void) const; + const std::vector& argv (void) const; + int define (const std::string& aDefinition); + int define (const std::string& aDefinition, + const std::string& description); + const std::string& getArg (int index); + const std::string& getArgument (int index); + int getArgCount (void); + int getArgumentCount (void); + const std::vector& getArgList (void); + const std::vector& getArgumentList (void); + bool getBoolean (const std::string& optionName); + std::string getCommand (void); + const std::string& getCommandLine (void); + std::string getDefinition (const std::string& optionName); + double getDouble (const std::string& optionName); + char getFlag (void); + char getChar (const std::string& optionName); + float getFloat (const std::string& optionName); + int getInt (const std::string& optionName); + int getInteger (const std::string& optionName); + std::string getString (const std::string& optionName); + char getType (const std::string& optionName); + int optionsArg (void); + std::ostream& print (std::ostream& out); + std::ostream& printOptionList (std::ostream& out); + std::ostream& printOptionListBooleanState(std::ostream& out); + void process (int error_check = 1, int suppress = 0); + void process (int argc, char** argv, int error_check = 1, + int suppress = 0); + void reset (void); + void xverify (int argc, char** argv, + int error_check = 1, + int suppress = 0); + void xverify (int error_check = 1, int suppress = 0); + void setFlag (char aFlag); + void setModified (const std::string& optionName, + const std::string& optionValue); + void setOptions (int argc, char** argv); + void appendOptions (int argc, char** argv); + void appendOptions (const std::string& strang); + void appendOptions (const std::vector& argv); + std::ostream& printRegister (std::ostream& out); + bool isDefined (const std::string& name); + + protected: + int m_options_error_check; // verify command + int m_oargc; + std::vector m_oargv; + std::string m_commandString; + char m_optionFlag; + std::vector m_argument; + + std::vector m_optionRegister; + std::map m_optionList; + + bool m_processedQ; + bool m_suppressQ; // prevent --options + bool m_optionsArgument; // --options present + + std::vector m_extraArgv; + std::vector m_extraArgv_strings; + + private: + int getRegIndex (const std::string& optionName); + int optionQ (const std::string& aString, int& argp); + int storeOption (int gargp, int& position, int& running); + +}; + +} // end of namespace smf + + +#endif /* _OPTIONS_H_INCLUDED */ + + + diff --git a/midifile/src/Binasc.cpp b/midifile/src/Binasc.cpp new file mode 100644 index 0000000..d74457a --- /dev/null +++ b/midifile/src/Binasc.cpp @@ -0,0 +1,1969 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Mon Feb 16 12:26:32 PST 2015 Adapted from binasc program. +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/src/Binasc.cpp +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// description: Interface to convert bytes between binary and ASCII forms. +// + +#include "Binasc.h" + +#include +#include + + +namespace smf { + +////////////////////////////// +// +// Binasc::Binasc -- Constructor: set the default option values. +// + +Binasc::Binasc(void) { + m_bytesQ = 1; // for printing HEX bytes when converting to ASCII + m_commentsQ = 0; // for printing text comments when converting to ASCII + m_midiQ = 0; // for printing ASCII as parsed MIDI file. + m_maxLineLength = 75; + m_maxLineBytes = 25; +} + + + +////////////////////////////// +// +// Binasc::~Binasc -- Destructor. +// + +Binasc::~Binasc() { + // do nothing +} + + + +////////////////////////////// +// +// Binasc::setLineLength -- Set the maximum length of a line when converting +// binary content into ASCII bytes. If the input size is less than one, +// set to the default value of 75 characters per line. +// + +int Binasc::setLineLength(int length) { + if (length < 1) { + m_maxLineLength = 75; + } else { + m_maxLineLength = length; + } + return m_maxLineLength; +} + + + +////////////////////////////// +// +// Binasc::getLineLength -- Set the maximum length of a line when converting +// binary content into ASCII bytes. +// + +int Binasc::getLineLength(void) { + return m_maxLineLength; +} + + + +////////////////////////////// +// +// Binasc::setLineBytes -- Set the maximum number of hex bytes in ASCII output. +// If the input size is less than one, set to the default value of 25 +// hex bytes per line. +// + +int Binasc::setLineBytes(int length) { + if (length < 1) { + m_maxLineBytes = 25; + } else { + m_maxLineBytes = length; + } + return m_maxLineBytes; +} + + + +////////////////////////////// +// +// Binasc::getLineBytes -- Get the maximum number of hex bytes in ASCII output. +// + +int Binasc::getLineBytes(void) { + return m_maxLineLength; +} + + + +////////////////////////////// +// +// Binasc::setComments -- Display or not display printable characters +// as comments when converting binary files to ASCII byte codes. +// + +void Binasc::setComments(int state) { + m_commentsQ = state ? 1 : 0; +} + + +void Binasc::setCommentsOn(void) { + setComments(true); +} + + +void Binasc::setCommentsOff(void) { + setComments(false); +} + + + +////////////////////////////// +// +// Binasc::getComments -- Get the comment display style for +// showing comments in ASCII output; +// + +int Binasc::getComments(void) { + return m_commentsQ; +} + + + +////////////////////////////// +// +// Binasc::setBytes -- Display or not display hex codes (only +// print ASCII printable characters). +// + +void Binasc::setBytes(int state) { + m_bytesQ = state ? 1 : 0; +} + + +void Binasc::setBytesOn(void) { + setBytes(true); +} + + +void Binasc::setBytesOff(void) { + setBytes(false); +} + + +////////////////////////////// +// +// Binasc::getBytes -- Get hex byte display status. +// + +int Binasc::getBytes(void) { + return m_bytesQ; +} + + +////////////////////////////// +// +// Binasc::setMidi -- Display or not display parsed MIDI data. +// + +void Binasc::setMidi(int state) { + m_midiQ = state ? 1 : 0; +} + + +void Binasc::setMidiOn(void) { + setMidi(true); +} + + +void Binasc::setMidiOff(void) { + setMidi(false); +} + + + +////////////////////////////// +// +// Binasc::getMidi -- Get the MIDI file printing style option state. +// + +int Binasc::getMidi(void) { + return m_midiQ; +} + + + +////////////////////////////// +// +// Binasc::writeToBinary -- Convert an ASCII representation of bytes into +// the binary file that it describes. Returns 0 if there was a problem +// otherwise returns 1. +// + +int Binasc::writeToBinary(const std::string& outfile, + const std::string& infile) { + std::ifstream input; + input.open(infile.c_str()); + if (!input.is_open()) { + std::cerr << "Cannot open " << infile + << " for reading in binasc." << std::endl; + return 0; + } + + std::ofstream output; + output.open(outfile.c_str()); + if (!output.is_open()) { + std::cerr << "Cannot open " << outfile + << " for reading in binasc." << std::endl; + return 0; + } + + int status = writeToBinary(output, input); + input.close(); + output.close(); + return status; +} + + +int Binasc::writeToBinary(const std::string& outfile, std::istream& input) { + std::ofstream output; + output.open(outfile.c_str()); + if (!output.is_open()) { + std::cerr << "Cannot open " << outfile + << " for reading in binasc." << std::endl; + return 0; + } + + int status = writeToBinary(output, input); + output.close(); + return status; +} + + +int Binasc::writeToBinary(std::ostream& out, const std::string& infile) { + std::ifstream input; + input.open(infile.c_str()); + if (!input.is_open()) { + std::cerr << "Cannot open " << infile + << " for reading in binasc." << std::endl; + return 0; + } + + int status = writeToBinary(out, input); + input.close(); + return status; +} + + +int Binasc::writeToBinary(std::ostream& out, std::istream& input) { + std::string inputLine; + inputLine.reserve(8196); + int lineNum = 0; // current line number + getline(input, inputLine, '\n'); + lineNum++; + while (!input.eof()) { + int status = processLine(out, inputLine, lineNum); + if (!status) { + return 0; + } + getline(input, inputLine, '\n'); + lineNum++; + } + return 1; +} + + + +////////////////////////////// +// +// Binasc::readFromBinary -- convert an ASCII representation of bytes into +// the binary file that it describes. +// + +int Binasc::readFromBinary(const std::string& outfile, const std::string& infile) { + std::ifstream input; + input.open(infile.c_str()); + if (!input.is_open()) { + std::cerr << "Cannot open " << infile + << " for reading in binasc." << std::endl; + return 0; + } + + std::ofstream output; + output.open(outfile.c_str()); + if (!output.is_open()) { + std::cerr << "Cannot open " << outfile + << " for reading in binasc." << std::endl; + return 0; + } + + int status = readFromBinary(output, input); + input.close(); + output.close(); + return status; +} + + +int Binasc::readFromBinary(const std::string& outfile, std::istream& input) { + std::ofstream output; + output.open(outfile.c_str()); + if (!output.is_open()) { + std::cerr << "Cannot open " << outfile + << " for reading in binasc." << std::endl; + return 0; + } + + int status = readFromBinary(output, input); + output.close(); + return status; +} + + +int Binasc::readFromBinary(std::ostream& out, const std::string& infile) { + std::ifstream input; + input.open(infile.c_str()); + if (!input.is_open()) { + std::cerr << "Cannot open " << infile + << " for reading in binasc." << std::endl; + return 0; + } + + int status = readFromBinary(out, input); + input.close(); + return status; +} + + +int Binasc::readFromBinary(std::ostream& out, std::istream& input) { + int status; + if (m_midiQ) { + status = outputStyleMidi(out, input); + } else if (!m_bytesQ) { + status = outputStyleAscii(out, input); + } else if (m_bytesQ && m_commentsQ) { + status = outputStyleBoth(out, input); + } else { + status = outputStyleBinary(out, input); + } + return status; +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// protected functions -- +// + +////////////////////////////// +// +// Binasc::outputStyleAscii -- read an input file and output bytes in ascii +// form, not displaying any blank lines. Output words are not +// broken unless they are longer than 75 characters. +// + +int Binasc::outputStyleAscii(std::ostream& out, std::istream& input) { + uchar outputWord[256] = {0}; // storage for current word + int index = 0; // current length of word + int lineCount = 0; // current length of line + int type = 0; // 0=space, 1=printable + uchar ch; // current input byte + + ch = static_cast(input.get()); + while (!input.eof()) { + int lastType = type; + type = (isprint(ch) && !isspace(ch)) ? 1 : 0; + + if ((type == 1) && (lastType == 0)) { + // start of a new word. check where to put old word + if (index + lineCount >= m_maxLineLength) { // put on next line + outputWord[index] = '\0'; + out << '\n' << outputWord; + lineCount = index; + index = 0; + } else { // put on current line + outputWord[index] = '\0'; + if (lineCount != 0) { + out << ' '; + lineCount++; + } + out << outputWord; + lineCount += index; + index = 0; + } + } + if (type == 1) { + outputWord[index++] = ch; + } + ch = static_cast(input.get()); + } + + if (index != 0) { + out << std::endl; + } + + return 1; +} + + + +////////////////////////////// +// +// Binasc::outputStyleBinary -- read an input binary file and output bytes +// in ascii form, hexadecimal numbers only. +// + +int Binasc::outputStyleBinary(std::ostream& out, std::istream& input) { + int currentByte = 0; // current byte output in line + uchar ch; // current input byte + + ch = static_cast(input.get()); + if (input.eof()) { + std::cerr << "End of the file right away!" << std::endl; + return 0; + } + + while (!input.eof()) { + if (ch < 0x10) { + out << '0'; + } + out << std::hex << (int)ch << ' '; + currentByte++; + if (currentByte >= m_maxLineBytes) { + out << '\n'; + currentByte = 0; + } + ch = static_cast(input.get()); + } + + if (currentByte != 0) { + out << std::endl; + } + + return 1; +} + + + +////////////////////////////// +// +// Binasc::outputStyleBoth -- read an input file and output bytes in ASCII +// form with both hexadecimal numbers and ascii representation +// + +int Binasc::outputStyleBoth(std::ostream& out, std::istream& input) { + uchar asciiLine[256] = {0}; // storage for output line + int currentByte = 0; // current byte output in line + int index = 0; // current character in asciiLine + uchar ch; // current input byte + + ch = static_cast(input.get()); + while (!input.eof()) { + if (index == 0) { + asciiLine[index++] = ';'; + out << ' '; + } + if (ch < 0x10) { + out << '0'; + } + out << std::hex << (int)ch << ' '; + currentByte++; + + asciiLine[index++] = ' '; + if (isprint(ch)) { + asciiLine[index++] = ch; + } else { + asciiLine[index++] = ' '; + } + asciiLine[index++] = ' '; + + if (currentByte >= m_maxLineBytes) { + out << '\n'; + asciiLine[index] = '\0'; + out << asciiLine << "\n\n"; + currentByte = 0; + index = 0; + } + ch = static_cast(input.get()); + } + + if (currentByte != 0) { + out << '\n'; + asciiLine[index] = '\0'; + out << asciiLine << '\n' << std::endl; + } + + return 1; +} + + + +/////////////////////////////// +// +// Binasc::processLine -- Read a line of input and output any specified bytes. +// + +int Binasc::processLine(std::ostream& out, const std::string& input, + int lineCount) { + int status = 1; + int i = 0; + int length = (int)input.size(); + std::string word; + while (i 2)) { + status = processBinaryWord(out, word, lineCount); + } else { + status = processHexWord(out, word, lineCount); + } + } + + if (status == 0) { + return 0; + } + + } + + return 1; +} + + + +////////////////////////////// +// +// Binasc::getWord -- extract a sub string, stopping at any of the given +// terminator characters. +// + +int Binasc::getWord(std::string& word, const std::string& input, + const std::string& terminators, int index) { + word.resize(0); + int i = index; + int escape = 0; + int ecount = 0; + if (terminators.find('"') != std::string::npos) { + escape = 1; + } + while (i < (int)input.size()) { + if (escape && input[i] == '\"') { + ecount++; + i++; + if (ecount >= 2) { + break; + } + } + if (escape && (i<(int)input.size()-1) && (input[i] == '\\') + && (input[i+1] == '"')) { + word.push_back(input[i+1]); + i += 2; + } else if (terminators.find(input[i]) == std::string::npos) { + word.push_back(input[i]); + i++; + } else { + i++; + return i; + } + } + return i; +} + + + +/////////////////////////////// +// +// Binasc::getVLV -- read a Variable-Length Value from the file +// + +int Binasc::getVLV(std::istream& infile, int& trackbytes) { + int output = 0; + uchar ch = 0; + infile.read((char*)&ch, 1); + trackbytes++; + output = (output << 7) | (0x7f & ch); + while (ch >= 0x80) { + infile.read((char*)&ch, 1); + trackbytes++; + output = (output << 7) | (0x7f & ch); + } + return output; +} + + + +////////////////////////////// +// +// Binasc::readMidiEvent -- Read a delta time and then a MIDI message +// (or meta message). Returns 1 if not end-of-track meta message; +// 0 otherwise. +// + +int Binasc::readMidiEvent(std::ostream& out, std::istream& infile, + int& trackbytes, int& command) { + + // Read and print Variable Length Value for delta ticks + int vlv = getVLV(infile, trackbytes); + + std::stringstream output; + + output << "v" << std::dec << vlv << "\t"; + + std::string comment; + + int status = 1; + uchar ch = 0; + char byte1, byte2; + infile.read((char*)&ch, 1); + trackbytes++; + if (ch < 0x80) { + // running status: command byte is previous one in data stream + output << " "; + } else { + // midi command byte + output << std::hex << (int)ch; + command = ch; + infile.read((char*)&ch, 1); + trackbytes++; + } + byte1 = ch; + switch (command & 0xf0) { + case 0x80: // note-off: 2 bytes + output << " '" << std::dec << (int)byte1; + infile.read((char*)&ch, 1); + trackbytes++; + byte2 = ch; + output << " '" << std::dec << (int)byte2; + if (m_commentsQ) { + comment += "note-off " + keyToPitchName(byte1); + } + break; + case 0x90: // note-on: 2 bytes + output << " '" << std::dec << (int)byte1; + infile.read((char*)&ch, 1); + trackbytes++; + byte2 = ch; + output << " '" << std::dec << (int)byte2; + if (m_commentsQ) { + if (byte2 == 0) { + comment += "note-off " + keyToPitchName(byte1); + } else { + comment += "note-on " + keyToPitchName(byte1); + } + } + break; + case 0xA0: // aftertouch: 2 bytes + output << " '" << std::dec << (int)byte1; + infile.read((char*)&ch, 1); + trackbytes++; + byte2 = ch; + output << " '" << std::dec << (int)byte2; + if (m_commentsQ) { + comment += "after-touch"; + } + break; + case 0xB0: // continuous controller: 2 bytes + output << " '" << std::dec << (int)byte1; + infile.read((char*)&ch, 1); + trackbytes++; + byte2 = ch; + output << " '" << std::dec << (int)byte2; + if (m_commentsQ) { + comment += "controller"; + } + break; + case 0xE0: // pitch-bend: 2 bytes + output << " '" << std::dec << (int)byte1; + infile.read((char*)&ch, 1); + trackbytes++; + byte2 = ch; + output << " '" << std::dec << (int)byte2; + if (m_commentsQ) { + comment += "pitch-bend"; + } + break; + case 0xC0: // patch change: 1 bytes + output << " '" << std::dec << (int)byte1; + if (m_commentsQ) { + output << "\t"; + comment += "patch-change"; + } + break; + case 0xD0: // channel pressure: 1 bytes + output << " '" << std::dec << (int)byte1; + if (m_commentsQ) { + comment += "channel pressure"; + } + break; + case 0xF0: // various system bytes: variable bytes + switch (command) { + case 0xf0: + break; + case 0xf7: + // Read the first byte which is either 0xf0 or 0xf7. + // Then a VLV byte count for the number of bytes + // that remain in the message will follow. + // Then read that number of bytes. + { + infile.putback(byte1); + trackbytes--; + int length = getVLV(infile, trackbytes); + output << " v" << std::dec << length; + for (int i=0; i 0) { + tempout << "\t\t\t; unknown header bytes"; + tempout << std::endl; + } + + for (i=0; i 127 || tempLong < -128) { + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "Decimal number out of range from -128 to 127" << std::endl; + return 0; + } + char charOutput = (char)tempLong; + out << charOutput; + return 1; + } else { + ulong tempLong = (ulong)atoi(&word[quoteIndex + 1]); + uchar ucharOutput = (uchar)tempLong; + if (tempLong > 255) { // || (tempLong < 0)) { + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "Decimal number out of range from 0 to 255" << std::endl; + return 0; + } + out << ucharOutput; + return 1; + } + } + + // left with an integer number with a specified number of bytes + switch (byteCount) { + case 1: + if (signIndex != -1) { + long tempLong = atoi(&word[quoteIndex + 1]); + char charOutput = (char)tempLong; + out << charOutput; + return 1; + } else { + ulong tempLong = (ulong)atoi(&word[quoteIndex + 1]); + uchar ucharOutput = (uchar)tempLong; + out << ucharOutput; + return 1; + } + break; + case 2: + if (signIndex != -1) { + long tempLong = atoi(&word[quoteIndex + 1]); + short shortOutput = (short)tempLong; + if (endianIndex == -1) { + writeBigEndianShort(out, shortOutput); + } else { + writeLittleEndianShort(out, shortOutput); + } + return 1; + } else { + ulong tempLong = (ulong)atoi(&word[quoteIndex + 1]); + ushort ushortOutput = (ushort)tempLong; + if (endianIndex == -1) { + writeBigEndianUShort(out, ushortOutput); + } else { + writeLittleEndianUShort(out, ushortOutput); + } + return 1; + } + break; + case 3: + { + if (signIndex != -1) { + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "negative decimal numbers cannot be stored in 3 bytes" + << std::endl; + return 0; + } + ulong tempLong = (ulong)atoi(&word[quoteIndex + 1]); + uchar byte1 = (uchar)((tempLong & 0x00ff0000) >> 16); + uchar byte2 = (uchar)((tempLong & 0x0000ff00) >> 8); + uchar byte3 = (uchar)((tempLong & 0x000000ff)); + if (endianIndex == -1) { + out << byte1; + out << byte2; + out << byte3; + } else { + out << byte3; + out << byte2; + out << byte1; + } + return 1; + } + break; + case 4: + if (signIndex != -1) { + long tempLong = atoi(&word[quoteIndex + 1]); + if (endianIndex == -1) { + writeBigEndianLong(out, tempLong); + } else { + writeLittleEndianLong(out, tempLong); + } + return 1; + } else { + ulong tempuLong = (ulong)atoi(&word[quoteIndex + 1]); + if (endianIndex == -1) { + writeBigEndianULong(out, tempuLong); + } else { + writeLittleEndianULong(out, tempuLong); + } + return 1; + } + break; + default: + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "invalid byte count specification for decimal number" << std::endl; + return 0; + } +} + + + +////////////////////////////// +// +// Binasc::processHexWord -- interprets a hexadecimal word and converts into +// its binary byte form. +// + +int Binasc::processHexWord(std::ostream& out, const std::string& word, + int lineNum) { + int length = (int)word.size(); + uchar outputByte; + + if (length > 2) { + std::cerr << "Error on line " << lineNum << " at token: " << word << std::endl; + std::cerr << "Size of hexadecimal number is too large. Max is ff." << std::endl; + return 0; + } + + if (!isxdigit(word[0]) || (length == 2 && !isxdigit(word[1]))) { + std::cerr << "Error on line " << lineNum << " at token: " << word << std::endl; + std::cerr << "Invalid character in hexadecimal number." << std::endl; + return 0; + } + + outputByte = (uchar)strtol(word.c_str(), (char**)NULL, 16); + out << outputByte; + return 1; +} + + + +////////////////////////////// +// +// Binasc::processStringWord -- interprets a binary word into +// its constituent byte +// + +int Binasc::processStringWord(std::ostream& out, const std::string& word, + int /* lineNum */) { + out << word; + return 1; +} + + + +////////////////////////////// +// +// Binasc::processAsciiWord -- interprets a binary word into +// its constituent byte +// + +int Binasc::processAsciiWord(std::ostream& out, const std::string& word, + int lineNum) { + int length = (int)word.size(); + uchar outputByte; + + if (word[0] != '+') { + std::cerr << "Error on line " << lineNum << " at token: " << word << std::endl; + std::cerr << "character byte must start with \'+\' sign: " << std::endl; + return 0; + } + + if (length > 2) { + std::cerr << "Error on line " << lineNum << " at token: " << word << std::endl; + std::cerr << "character byte word is too long -- specify only one character" + << std::endl; + return 0; + } + + if (length == 2) { + outputByte = (uchar)word[1]; + } else { + outputByte = ' '; + } + out << outputByte; + return 1; +} + + + +////////////////////////////// +// +// Binasc::processBinaryWord -- interprets a binary word into +// its constituent byte +// + +int Binasc::processBinaryWord(std::ostream& out, const std::string& word, + int lineNum) { + int length = (int)word.size(); // length of ascii binary number + int commaIndex = -1; // index location of comma in number + int leftDigits = -1; // number of digits to left of comma + int rightDigits = -1; // number of digits to right of comma + int i = 0; + + // make sure that all characters are valid + for (i=0; i 8) { + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "too many digits in binary number" << std::endl; + return 0; + } + // if there is a comma, then there cannot be more than 4 digits on a side + if (leftDigits > 4) { + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "too many digits to left of comma" << std::endl; + return 0; + } + if (rightDigits > 4) { + std::cerr << "Error on line " << lineNum << " at token: " << word + << std::endl; + std::cerr << "too many digits to right of comma" << std::endl; + return 0; + } + + // OK, we have a valid binary number, so calculate the byte + + uchar output = 0; + + // if no comma in binary number + if (commaIndex == -1) { + for (i=0; i> 28) & 0x7f; + byte[1] = (value >> 21) & 0x7f; + byte[2] = (value >> 14) & 0x7f; + byte[3] = (value >> 7) & 0x7f; + byte[4] = (value >> 0) & 0x7f; + + int i; + int flag = 0; + for (i=0; i<4; i++) { + if (byte[i] != 0) { + flag = 1; + } + if (flag) { + byte[i] |= 0x80; + } + } + + for (i=0; i<5; i++) { + if (byte[i] >= 0x80 || i == 4) { + out << byte[i]; + } + } + + return 1; +} + + + +//////////////////////////// +// +// Binasc::processMidiTempoWord -- convert a floating point tempo into +// a three-byte number of microseconds per beat per minute value. +// + +int Binasc::processMidiTempoWord(std::ostream& out, const std::string& word, + int lineNum) { + if (word.size() < 2) { + std::cerr << "Error on line: " << lineNum + << ": 't' needs to be followed immediately by " + << "a floating-point number" << std::endl; + return 0; + } + if (!(isdigit(word[1]) || word[1] == '.' || word[1] == '-' + || word[1] == '+')) { + std::cerr << "Error on line: " << lineNum + << ": 't' needs to be followed immediately by " + << "a floating-point number" << std::endl; + return 0; + } + double value = strtod(&word[1], NULL); + + if (value < 0.0) { + value = -value; + } + + int intval = int(60.0 * 1000000.0 / value + 0.5); + + uchar byte0 = intval & 0xff; + uchar byte1 = (intval >> 8) & 0xff; + uchar byte2 = (intval >> 16) & 0xff; + out << byte2 << byte1 << byte0; + return 1; +} + + + +//////////////////////////// +// +// Binasc::processMidiPitchBendWord -- convert a floating point number in +// the range from +1.0 to -1.0 into a 14-point integer with -1.0 mapping +// to 0 and +1.0 mapping to 2^15-1. This integer will be packed into +// two bytes, with the LSB coming first and containing the bottom +// 7-bits of the 14-bit value, then the MSB coming second and containing +// the top 7-bits of the 14-bit value. + +int Binasc::processMidiPitchBendWord(std::ostream& out, const std::string& word, + int lineNum) { + if (word.size() < 2) { + std::cerr << "Error on line: " << lineNum + << ": 'p' needs to be followed immediately by " + << "a floating-point number" << std::endl; + return 0; + } + if (!(isdigit(word[1]) || word[1] == '.' || word[1] == '-' + || word[1] == '+')) { + std::cerr << "Error on line: " << lineNum + << ": 'p' needs to be followed immediately by " + << "a floating-point number" << std::endl; + return 0; + } + double value = strtod(&word[1], NULL); + + if (value > 1.0) { + value = 1.0; + } + if (value < -1.0) { + value = -1.0; + } + + int intval = (int)(((1 << 13)-0.5) * (value + 1.0) + 0.5); + uchar LSB = intval & 0x7f; + uchar MSB = (intval >> 7) & 0x7f; + out << LSB << MSB; + return 1; +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// Ordered byte writing functions -- +// + +////////////////////////////// +// +// Binasc::writeLittleEndianUShort -- +// + +std::ostream& Binasc::writeLittleEndianUShort(std::ostream& out, ushort value) { + union { char bytes[2]; ushort us; } data; + data.us = value; + out << data.bytes[0]; + out << data.bytes[1]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeBigEndianUShort -- +// + +std::ostream& Binasc::writeBigEndianUShort(std::ostream& out, ushort value) { + union { char bytes[2]; ushort us; } data; + data.us = value; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeLittleEndianShort -- +// + +std::ostream& Binasc::writeLittleEndianShort(std::ostream& out, short value) { + union { char bytes[2]; short s; } data; + data.s = value; + out << data.bytes[0]; + out << data.bytes[1]; + return out; +} + + + +////////////////////////////// +// +// writeBigEndianShort -- +// + +std::ostream& Binasc::writeBigEndianShort(std::ostream& out, short value) { + union { char bytes[2]; short s; } data; + data.s = value; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeLittleEndianULong -- +// + +std::ostream& Binasc::writeLittleEndianULong(std::ostream& out, ulong value) { + union { char bytes[4]; ulong ul; } data; + data.ul = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeBigEndianULong -- +// + +std::ostream& Binasc::writeBigEndianULong(std::ostream& out, ulong value) { + union { char bytes[4]; long ul; } data; + data.ul = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeLittleEndianLong -- +// + +std::ostream& Binasc::writeLittleEndianLong(std::ostream& out, long value) { + union { char bytes[4]; long l; } data; + data.l = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeBigEndianLong -- +// + +std::ostream& Binasc::writeBigEndianLong(std::ostream& out, long value) { + union { char bytes[4]; long l; } data; + data.l = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; + +} + + + +////////////////////////////// +// +// Binasc::writeBigEndianFloat -- +// + +std::ostream& Binasc::writeBigEndianFloat(std::ostream& out, float value) { + union { char bytes[4]; float f; } data; + data.f = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeLittleEndianFloat -- +// + +std::ostream& Binasc::writeLittleEndianFloat(std::ostream& out, float value) { + union { char bytes[4]; float f; } data; + data.f = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeBigEndianDouble -- +// + +std::ostream& Binasc::writeBigEndianDouble(std::ostream& out, double value) { + union { char bytes[8]; double d; } data; + data.d = value; + out << data.bytes[7]; + out << data.bytes[6]; + out << data.bytes[5]; + out << data.bytes[4]; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// Binasc::writeLittleEndianDouble -- +// + +std::ostream& Binasc::writeLittleEndianDouble(std::ostream& out, double value) { + union { char bytes[8]; double d; } data; + data.d = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + out << data.bytes[4]; + out << data.bytes[5]; + out << data.bytes[6]; + out << data.bytes[7]; + return out; +} + + +} // end namespace smf + + + diff --git a/midifile/src/MidiEvent.cpp b/midifile/src/MidiEvent.cpp new file mode 100644 index 0000000..945e3c5 --- /dev/null +++ b/midifile/src/MidiEvent.cpp @@ -0,0 +1,284 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat Feb 14 21:40:14 PST 2015 +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/src/MidiEvent.cpp +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: A class which stores a MidiMessage and a timestamp +// for the MidiFile class. +// + +#include "MidiEvent.h" + +#include + + +namespace smf { + +////////////////////////////// +// +// MidiEvent::MidiEvent -- Constructor classes +// + +MidiEvent::MidiEvent(void) : MidiMessage() { + clearVariables(); +} + + +MidiEvent::MidiEvent(int command) : MidiMessage(command) { + clearVariables(); +} + + +MidiEvent::MidiEvent(int command, int p1) : MidiMessage(command, p1) { + clearVariables(); +} + + +MidiEvent::MidiEvent(int command, int p1, int p2) + : MidiMessage(command, p1, p2) { + clearVariables(); +} + + +MidiEvent::MidiEvent(int aTime, int aTrack, vector& message) + : MidiMessage(message) { + track = aTrack; + tick = aTime; + seconds = 0.0; + seq = 0; + m_eventlink = NULL; +} + + +MidiEvent::MidiEvent(const MidiEvent& mfevent) : MidiMessage() { + track = mfevent.track; + tick = mfevent.tick; + seconds = mfevent.seconds; + seq = mfevent.seq; + m_eventlink = NULL; + + this->resize(mfevent.size()); + for (int i=0; i<(int)this->size(); i++) { + (*this)[i] = mfevent[i]; + } +} + + + +////////////////////////////// +// +// MidiEvent::~MidiEvent -- MidiFile Event destructor +// + +MidiEvent::~MidiEvent() { + track = -1; + tick = -1; + seconds = -1.0; + seq = -1; + this->resize(0); + m_eventlink = NULL; +} + + +////////////////////////////// +// +// MidiEvent::clearVariables -- Clear everything except MidiMessage data. +// + +void MidiEvent::clearVariables(void) { + track = 0; + tick = 0; + seconds = 0.0; + seq = 0; + m_eventlink = NULL; +} + + +////////////////////////////// +// +// MidiEvent::operator= -- Copy the contents of another MidiEvent. +// + +MidiEvent& MidiEvent::operator=(const MidiEvent& mfevent) { + if (this == &mfevent) { + return *this; + } + tick = mfevent.tick; + track = mfevent.track; + seconds = mfevent.seconds; + seq = mfevent.seq; + m_eventlink = NULL; + this->resize(mfevent.size()); + for (int i=0; i<(int)this->size(); i++) { + (*this)[i] = mfevent[i]; + } + return *this; +} + + +MidiEvent& MidiEvent::operator=(const MidiMessage& message) { + if (this == &message) { + return *this; + } + clearVariables(); + this->resize(message.size()); + for (int i=0; i<(int)this->size(); i++) { + (*this)[i] = message[i]; + } + return *this; +} + + +MidiEvent& MidiEvent::operator=(const vector& bytes) { + clearVariables(); + this->resize(bytes.size()); + for (int i=0; i<(int)this->size(); i++) { + (*this)[i] = bytes[i]; + } + return *this; +} + + +MidiEvent& MidiEvent::operator=(const vector& bytes) { + clearVariables(); + setMessage(bytes); + return *this; +} + + +MidiEvent& MidiEvent::operator=(const vector& bytes) { + clearVariables(); + setMessage(bytes); + return *this; +} + + + +////////////////////////////// +// +// MidiEvent::unlinkEvent -- Disassociate this event with another. +// Also tell the other event to disassociate from this event. +// + +void MidiEvent::unlinkEvent(void) { + if (m_eventlink == NULL) { + return; + } + MidiEvent* mev = m_eventlink; + m_eventlink = NULL; + mev->unlinkEvent(); +} + + + +////////////////////////////// +// +// MidiEvent::linkEvent -- Make a link between two messages. +// Unlinking +// + +void MidiEvent::linkEvent(MidiEvent* mev) { + if (mev->m_eventlink != NULL) { + // unlink other event if it is linked to something else; + mev->unlinkEvent(); + } + // if this is already linked to something else, then unlink: + if (m_eventlink != NULL) { + m_eventlink->unlinkEvent(); + } + unlinkEvent(); + + mev->m_eventlink = this; + m_eventlink = mev; +} + + +void MidiEvent::linkEvent(MidiEvent& mev) { + linkEvent(&mev); +} + + + +////////////////////////////// +// +// MidiEvent::getLinkedEvent -- Returns a linked event. Usually +// this is the note-off message for a note-on message and vice-versa. +// Returns null if there are no links. +// + +MidiEvent* MidiEvent::getLinkedEvent(void) { + return m_eventlink; +} + + +const MidiEvent* MidiEvent::getLinkedEvent(void) const { + return m_eventlink; +} + + + +////////////////////////////// +// +// MidiEvent::isLinked -- Returns true if there is an event which is not +// NULL. This function is similar to getLinkedEvent(). +// + +int MidiEvent::isLinked(void) const { + return m_eventlink == NULL ? 0 : 1; +} + + + +////////////////////////////// +// +// MidiEvent::getTickDuration -- For linked events (note-ons and note-offs), +// return the absolute tick time difference between the two events. +// The tick values are presumed to be in absolute tick mode rather than +// delta tick mode. Returns 0 if not linked. +// + +int MidiEvent::getTickDuration(void) const { + const MidiEvent* mev = getLinkedEvent(); + if (mev == NULL) { + return 0; + } + int tick2 = mev->tick; + if (tick2 > tick) { + return tick2 - tick; + } else { + return tick - tick2; + } +} + + + +////////////////////////////// +// +// MidiEvent::getDurationInSeconds -- For linked events (note-ons and +// note-offs), return the duration of the note in seconds. The +// seconds analysis must be done first; otherwise the duration will be +// reported as zero. +// + +double MidiEvent::getDurationInSeconds(void) const { + const MidiEvent* mev = getLinkedEvent(); + if (mev == NULL) { + return 0; + } + double seconds2 = mev->seconds; + if (seconds2 > seconds) { + return seconds2 - seconds; + } else { + return seconds - seconds2; + } +} + + +} // end namespace smf + + + diff --git a/midifile/src/MidiEventList.cpp b/midifile/src/MidiEventList.cpp new file mode 100644 index 0000000..52749c5 --- /dev/null +++ b/midifile/src/MidiEventList.cpp @@ -0,0 +1,624 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat Feb 14 21:55:38 PST 2015 +// Last Modified: Sat Apr 21 10:52:19 PDT 2018 Removed using namespace std; +// Filename: midifile/src/MidiEventList.cpp +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: A class which stores a MidiEvents for a MidiFile track. +// + + +#include "MidiEventList.h" + +#include +#include +#include +#include + +#include + +namespace smf { + +////////////////////////////// +// +// MidiEventList::MidiEventList -- Constructor. +// + +MidiEventList::MidiEventList(void) { + reserve(1000); +} + + + +////////////////////////////// +// +// MidiEventList::MidiEventList(MidiEventList&) -- Copy constructor. +// + +MidiEventList::MidiEventList(const MidiEventList& other) { + list.reserve(other.list.size()); + auto it = other.list.begin(); + std::generate_n(std::back_inserter(list), other.list.size(), [&]() -> MidiEvent* { + return new MidiEvent(**it++); + }); +} + + + +////////////////////////////// +// +// MidiEventList::MidiEventList(MidiEventList&&) -- Move constructor. +// + +MidiEventList::MidiEventList(MidiEventList&& other) { + list = std::move(other.list); + other.list.clear(); +} + + + +////////////////////////////// +// +// MidiEventList::~MidiEventList -- Deconstructor. Deallocate all stored +// data. +// + +MidiEventList::~MidiEventList() { + clear(); +} + + + +////////////////////////////// +// +// MidiEventList::operator[] -- +// + +MidiEvent& MidiEventList::operator[](int index) { + return *list[index]; +} + + +const MidiEvent& MidiEventList::operator[](int index) const { + return *list[index]; +} + + + +////////////////////////////// +// +// MidiEventList::back -- Return the last element in the list. +// + +MidiEvent& MidiEventList::back(void) { + return *list.back(); +} + + +const MidiEvent& MidiEventList::back(void) const { + return *list.back(); +} + +// +// MidiEventList::last -- Alias for MidiEventList::back(). +// + +MidiEvent& MidiEventList::last(void) { + return back(); +} + + +const MidiEvent& MidiEventList::last(void) const { + return back(); +} + + + +////////////////////////////// +// +// MidiEventList::getEvent -- The same thing as operator[], for +// internal use when operator[] would look more messy. +// + +MidiEvent& MidiEventList::getEvent(int index) { + return *list[index]; +} + + +const MidiEvent& MidiEventList::getEvent(int index) const { + return *list[index]; +} + + + +////////////////////////////// +// +// MidiEventList::clear -- De-allocate any MidiEvents present in the list +// and set the size of the list to 0. +// + +void MidiEventList::clear(void) { + for (int i=0; i<(int)list.size(); i++) { + if (list[i] != NULL) { + delete list[i]; + list[i] = NULL; + } + } + list.resize(0); +} + + + +////////////////////////////// +// +// MidiEventList::data -- Return the low-level array of MidiMessage +// pointers. This is useful for applying your own sorting +// function to the list. +// + +MidiEvent** MidiEventList::data(void) { + return list.data(); +} + + + +////////////////////////////// +// +// MidiEventList::reserve -- Pre-allocate space in the list for storing +// elements. +// + +void MidiEventList::reserve(int rsize) { + if (rsize > (int)list.size()) { + list.reserve(rsize); + } +} + + +////////////////////////////// +// +// MidiEventList::getSize -- Return the number of MidiEvents stored +// in the list. +// + +int MidiEventList::getSize(void) const { + return (int)list.size(); +} + +// +// MidiEventList::size -- Alias for MidiEventList::getSize(). +// + +int MidiEventList::size(void) const { + return getSize(); +} + +// +// MidiEventList::getEventCount -- Alias for MidiEventList::getSize(). +// + +int MidiEventList::getEventCount(void) const { + return getSize(); +} + + + +////////////////////////////// +// +// MidiEventList::append -- add a MidiEvent at the end of the list. Returns +// the index of the appended event. +// + +int MidiEventList::append(MidiEvent& event) { + MidiEvent* ptr = new MidiEvent(event); + list.push_back(ptr); + return (int)list.size()-1; +} + +// +// MidiEventList::push -- Alias for MidiEventList::append(). +// + +int MidiEventList::push(MidiEvent& event) { + return append(event); +} + +// +// MidiEventList::push_back -- Alias for MidiEventList::append(). +// + +int MidiEventList::push_back(MidiEvent& event) { + return append(event); +} + + + +////////////////////////////// +// +// MidiEventList::removeEmpties -- Remove any MIDI message which contain no +// bytes. This function first deallocates any empty MIDI events, and then +// removes them from the list of events. +// + +void MidiEventList::removeEmpties(void) { + int count = 0; + for (int i=0; i<(int)list.size(); i++) { + if (list[i]->empty()) { + delete list[i]; + list[i] = NULL; + count++; + } + } + if (count == 0) { + return; + } + std::vector newlist; + newlist.reserve(list.size() - count); + for (int i=0; i<(int)list.size(); i++) { + if (list[i]) { + newlist.push_back(list[i]); + } + } + list.swap(newlist); +} + + + +////////////////////////////// +// +// MidiEventList::linkNotePairs -- Match note-ones and note-offs together +// There are two models that can be done if two notes are overlapping +// on the same pitch: the first note-off affects the last note-on, +// or the first note-off affects the first note-on. Currently the +// first note-off affects the last note-on, but both methods could +// be implemented with user selectability. The current state of the +// track is assumed to be in time-sorted order. Returns the number +// of linked notes (note-on/note-off pairs). +// + +int MidiEventList::linkEventPairs(void) { + return linkNotePairs(); +} + + +int MidiEventList::linkNotePairs(void) { + + // Note-on states: + // dimension 1: MIDI channel (0-15) + // dimension 2: MIDI key (0-127) (but 0 not used for note-ons) + // dimension 3: List of active note-ons or note-offs. + std::vector>> noteons; + noteons.resize(16); + for (int i=0; i<(int)noteons.size(); i++) { + noteons[i].resize(128); + } + + // Controller linking: The following General MIDI controller numbers are + // also monitored for linking within the track (but not between tracks). + // hex dec name range + // 40 64 Hold pedal (Sustain) on/off 0..63=off 64..127=on + // 41 65 Portamento on/off 0..63=off 64..127=on + // 42 66 Sustenuto Pedal on/off 0..63=off 64..127=on + // 43 67 Soft Pedal on/off 0..63=off 64..127=on + // 44 68 Legato Pedal on/off 0..63=off 64..127=on + // 45 69 Hold Pedal 2 on/off 0..63=off 64..127=on + // 50 80 General Purpose Button 0..63=off 64..127=on + // 51 81 General Purpose Button 0..63=off 64..127=on + // 52 82 General Purpose Button 0..63=off 64..127=on + // 53 83 General Purpose Button 0..63=off 64..127=on + // 54 84 Undefined on/off 0..63=off 64..127=on + // 55 85 Undefined on/off 0..63=off 64..127=on + // 56 86 Undefined on/off 0..63=off 64..127=on + // 57 87 Undefined on/off 0..63=off 64..127=on + // 58 88 Undefined on/off 0..63=off 64..127=on + // 59 89 Undefined on/off 0..63=off 64..127=on + // 5A 90 Undefined on/off 0..63=off 64..127=on + // 7A 122 Local Keyboard On/Off 0..63=off 64..127=on + + // first keep track of whether the controller is an on/off switch: + std::vector> contmap; + contmap.resize(128); + std::pair zero(0, 0); + std::fill(contmap.begin(), contmap.end(), zero); + contmap[64].first = 1; contmap[64].second = 0; + contmap[65].first = 1; contmap[65].second = 1; + contmap[66].first = 1; contmap[66].second = 2; + contmap[67].first = 1; contmap[67].second = 3; + contmap[68].first = 1; contmap[68].second = 4; + contmap[69].first = 1; contmap[69].second = 5; + contmap[80].first = 1; contmap[80].second = 6; + contmap[81].first = 1; contmap[81].second = 7; + contmap[82].first = 1; contmap[82].second = 8; + contmap[83].first = 1; contmap[83].second = 9; + contmap[84].first = 1; contmap[84].second = 10; + contmap[85].first = 1; contmap[85].second = 11; + contmap[86].first = 1; contmap[86].second = 12; + contmap[87].first = 1; contmap[87].second = 13; + contmap[88].first = 1; contmap[88].second = 14; + contmap[89].first = 1; contmap[89].second = 15; + contmap[90].first = 1; contmap[90].second = 16; + contmap[122].first = 1; contmap[122].second = 17; + + // dimensions: + // 1: mapped controller (0 to 17) + // 2: channel (0 to 15) + std::vector> contevents; + contevents.resize(18); + std::vector> oldstates; + oldstates.resize(18); + for (int i=0; i<18; i++) { + contevents[i].resize(16); + std::fill(contevents[i].begin(), contevents[i].end(), nullptr); + oldstates[i].resize(16); + std::fill(oldstates[i].begin(), oldstates[i].end(), -1); + } + + // Now iterate through the MidiEventList keeping track of note and + // select controller states and linking notes/controllers as needed. + int channel; + int key; + int contnum; + int contval; + int conti; + int contstate; + int counter = 0; + MidiEvent* mev; + MidiEvent* noteon; + for (int i=0; iunlinkEvent(); + if (mev->isNoteOn()) { + // store the note-on to pair later with a note-off message. + key = mev->getKeyNumber(); + channel = mev->getChannel(); + noteons[channel][key].push_back(mev); + } else if (mev->isNoteOff()) { + key = mev->getKeyNumber(); + channel = mev->getChannel(); + if (noteons[channel][key].size() > 0) { + noteon = noteons[channel][key].back(); + noteons[channel][key].pop_back(); + noteon->linkEvent(mev); + counter++; + } + } else if (mev->isController()) { + contnum = mev->getP1(); + if (contmap[contnum].first) { + conti = contmap[contnum].second; + channel = mev->getChannel(); + contval = mev->getP2(); + contstate = contval < 64 ? 0 : 1; + if ((oldstates[conti][channel] == -1) && contstate) { + // a newly initialized onstate was detected, so store for + // later linking to an off state. + contevents[conti][channel] = mev; + oldstates[conti][channel] = contstate; + } else if (oldstates[conti][channel] == contstate) { + // the controller state is redundant and will be ignored. + } else if ((oldstates[conti][channel] == 0) && contstate) { + // controller is currently off, so store on-state for next link + contevents[conti][channel] = mev; + oldstates[conti][channel] = contstate; + } else if ((oldstates[conti][channel] == 1) && (contstate == 0)) { + // controller has just been turned off, so link to + // stored on-message. + contevents[conti][channel]->linkEvent(mev); + oldstates[conti][channel] = contstate; + // not necessary, but maybe use for something later: + contevents[conti][channel] = mev; + } + } + } + } + return counter; +} + + + +////////////////////////////// +// +// MidiEventList::clearLinks -- remove all note-on/note-off links. +// + +void MidiEventList::clearLinks(void) { + for (int i=0; i<(int)getSize(); i++) { + getEvent(i).unlinkEvent(); + } +} + + + +////////////////////////////// +// +// MidiEventList::clearSequence -- Remove any seqence serial numbers from +// MidiEvents in the list. This will cause the default ordering by +// sortTracks() to be used, in which case the ordering of MidiEvents +// occurring at the same tick may switch their ordering. +// + +void MidiEventList::clearSequence(void) { + for (int i=0; i bevent.tick) { + // aevent occurs after bevent + return +1; + } else if (aevent.tick < bevent.tick) { + // aevent occurs before bevent + return -1; + } else if ((aevent.seq != 0) && (bevent.seq != 0) && (aevent.seq > bevent.seq)) { + // aevent sequencing state occurs after bevent + // see MidiEventList::markSequence() + return +1; + } else if ((aevent.seq != 0) && (bevent.seq != 0) && (aevent.seq < bevent.seq)) { + // aevent sequencing state occurs before bevent + // see MidiEventList::markSequence() + return -1; + } else if (aevent.getP0() == 0xff && aevent.getP1() == 0x2f) { + // end-of-track meta-message should always be last (but won't really + // matter since the writing function ignores all end-of-track messages + // and writes its own. + return +1; + } else if (bevent.getP0() == 0xff && bevent.getP1() == 0x2f) { + // end-of-track meta-message should always be last (but won't really + // matter since the writing function ignores all end-of-track messages + // and writes its own. + return -1; + } else if (aevent.getP0() == 0xff && bevent.getP0() != 0xff) { + // other meta-messages are placed before real MIDI messages + return -1; + } else if (aevent.getP0() != 0xff && bevent.getP0() == 0xff) { + // other meta-messages are placed before real MIDI messages + return +1; + } else if (((aevent.getP0() & 0xf0) == 0x90) && (aevent.getP2() != 0)) { + // note-ons come after all other types of MIDI messages + return +1; + } else if (((bevent.getP0() & 0xf0) == 0x90) && (bevent.getP2() != 0)) { + // note-ons come after all other types of MIDI messages + return -1; + } else if (((aevent.getP0() & 0xf0) == 0x90) || ((aevent.getP0() & 0xf0) == 0x80)) { + // note-offs come after all other MIDI messages (except note-ons) + return +1; + } else if (((bevent.getP0() & 0xf0) == 0x90) || ((bevent.getP0() & 0xf0) == 0x80)) { + // note-offs come after all other MIDI messages (except note-ons) + return -1; + } else if (((aevent.getP0() & 0xf0) == 0xb0) && ((bevent.getP0() & 0xf0) == 0xb0)) { + // both events are continuous controllers. Sort them by controller number + if (aevent.getP1() > bevent.getP1()) { + return +1; + } if (aevent.getP1() < bevent.getP1()) { + return -1; + } else { + // same controller number, so sort by data value + if (aevent.getP2() > bevent.getP2()) { + return +1; + } if (aevent.getP2() < bevent.getP2()) { + return -1; + } else { + return 0; + } + } + } else { + return 0; + } +} + + +} // end namespace smf + + + diff --git a/midifile/src/MidiFile.cpp b/midifile/src/MidiFile.cpp new file mode 100644 index 0000000..685198b --- /dev/null +++ b/midifile/src/MidiFile.cpp @@ -0,0 +1,3395 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Fri Nov 26 14:12:01 PST 1999 +// Last Modified: Thu Jun 24 18:35:30 PDT 2021 Added base64 encoding read/write +// Filename: midifile/src/MidiFile.cpp +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: A class which can read/write Standard MIDI files. +// MIDI data is stored by track in an array. This +// class is used for example in the MidiPerform class. +// + +#include "MidiFile.h" +#include "Binasc.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace smf { + + +const std::string MidiFile::encodeLookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + +const std::vector MidiFile::decodeLookup { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, + -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +}; + + + +////////////////////////////// +// +// MidiFile::MidiFile -- Constuctor. +// + +MidiFile::MidiFile(void) { + m_events.resize(1); + for (int i=0; i<(int)m_events.size(); i++) { + m_events[i] = new MidiEventList; + } +} + + +MidiFile::MidiFile(const std::string& filename) { + m_events.resize(1); + for (int i=0; i<(int)m_events.size(); i++) { + m_events[i] = new MidiEventList; + } + read(filename); +} + + +MidiFile::MidiFile(std::istream& input) { + m_events.resize(1); + for (int i=0; i<(int)m_events.size(); i++) { + m_events[i] = new MidiEventList; + } + read(input); +} + + + +MidiFile::MidiFile(const MidiFile& other) { + *this = other; +} + + + +MidiFile::MidiFile(MidiFile&& other) { + *this = std::move(other); +} + + + +////////////////////////////// +// +// MidiFile::~MidiFile -- Deconstructor. +// + +MidiFile::~MidiFile() { + m_readFileName.clear(); + clear(); + if (m_events[0] != NULL) { + delete m_events[0]; + m_events[0] = NULL; + } + m_events.resize(0); + m_rwstatus = false; + m_timemap.clear(); + m_timemapvalid = 0; +} + + + +////////////////////////////// +// +// MidiFile::operator= -- Copying another +// + +MidiFile& MidiFile::operator=(const MidiFile& other) { + if (this == &other) { + return *this; + } + m_events.reserve(other.m_events.size()); + auto it = other.m_events.begin(); + std::generate_n(std::back_inserter(m_events), other.m_events.size(), + [&]()->MidiEventList* { + return new MidiEventList(**it++); + } + ); + m_ticksPerQuarterNote = other.m_ticksPerQuarterNote; + m_theTrackState = other.m_theTrackState; + m_theTimeState = other.m_theTimeState; + m_readFileName = other.m_readFileName; + m_timemapvalid = other.m_timemapvalid; + m_timemap = other.m_timemap; + m_rwstatus = other.m_rwstatus; + if (other.m_linkedEventsQ) { + linkEventPairs(); + } + return *this; +} + + +MidiFile& MidiFile::operator=(MidiFile&& other) { + m_events = std::move(other.m_events); + m_linkedEventsQ = other.m_linkedEventsQ; + other.m_linkedEventsQ = false; + other.m_events.clear(); + other.m_events.emplace_back(new MidiEventList); + m_ticksPerQuarterNote = other.m_ticksPerQuarterNote; + m_theTrackState = other.m_theTrackState; + m_theTimeState = other.m_theTimeState; + m_readFileName = other.m_readFileName; + m_timemapvalid = other.m_timemapvalid; + m_timemap = other.m_timemap; + m_rwstatus = other.m_rwstatus; + return *this; +} + + +/////////////////////////////////////////////////////////////////////////// +// +// reading/writing functions -- +// + + +////////////////////////////// +// +// MidiFile::read -- Parse a Standard MIDI File or ASCII-encoded Standard MIDI +// File and store its contents in the object. +// + +bool MidiFile::read(const std::string& filename) { + m_timemapvalid = 0; + setFilename(filename); + m_rwstatus = true; + + std::fstream input; + input.open(filename.c_str(), std::ios::binary | std::ios::in); + + if (!input.is_open()) { + m_rwstatus = false; + return m_rwstatus; + } + + m_rwstatus = read(input); + return m_rwstatus; +} + +// +// istream version of read(). +// + +bool MidiFile::read(std::istream& input) { + m_rwstatus = true; + if (input.peek() != 'M') { + // If the first byte in the input stream is not 'M', then presume that + // the MIDI file is in the binasc format which is an ASCII representation + // of the MIDI file. Convert the binasc content into binary content and + // then continue reading with this function. + std::stringstream binarydata; + Binasc binasc; + binasc.writeToBinary(binarydata, input); + binarydata.seekg(0, std::ios_base::beg); + if (binarydata.peek() != 'M') { + std::cerr << "Bad MIDI data input" << std::endl; + m_rwstatus = false; + return m_rwstatus; + } else { + m_rwstatus = readSmf(binarydata); + return m_rwstatus; + } + } else { + m_rwstatus = readSmf(input); + return m_rwstatus; + } +} + + + +////////////////////////////// +// +// MidiFile::readBase64 -- First decode base64 string and then parse as either a +// Standard MIDI File or binasc-encoded Standard MIDI File. +// + +bool MidiFile::readBase64(const std::string& base64data) { + std::stringstream stream; + stream << MidiFile::base64Decode(base64data); + return MidiFile::read(stream); +} + +bool MidiFile::readBase64(std::istream& instream) { + std::string base64data((std::istreambuf_iterator(instream)), + std::istreambuf_iterator()); + std::stringstream stream; + stream << MidiFile::base64Decode(base64data); + return MidiFile::read(stream); +} + + + +////////////////////////////// +// +// MidiFile::readSmf -- Parse a Standard MIDI File and store its contents +// in the object. +// + +bool MidiFile::readSmf(const std::string& filename) { + m_timemapvalid = 0; + setFilename(filename); + m_rwstatus = true; + + std::fstream input; + input.open(filename.c_str(), std::ios::binary | std::ios::in); + + if (!input.is_open()) { + m_rwstatus = false; + return m_rwstatus; + } + + m_rwstatus = readSmf(input); + return m_rwstatus; +} + + + +////////////////////////////// +// +// MidiFile::readSmf -- Parse a Standard MIDI File and store its contents in the object. +// + +bool MidiFile::readSmf(std::istream& input) { + m_rwstatus = true; + + std::string filename = getFilename(); + + int character; + // uchar buffer[123456] = {0}; + ulong longdata; + ushort shortdata; + + // Read the MIDI header (4 bytes of ID, 4 byte data size, + // anticipated 6 bytes of data. + + character = input.get(); + if (character == EOF) { + std::cerr << "In file " << filename << ": unexpected end of file." << std::endl; + std::cerr << "Expecting 'M' at first byte, but found nothing." << std::endl; + m_rwstatus = false; return m_rwstatus; + } else if (character != 'M') { + std::cerr << "File " << filename << " is not a MIDI file" << std::endl; + std::cerr << "Expecting 'M' at first byte but got '" + << (char)character << "'" << std::endl; + m_rwstatus = false; return m_rwstatus; + } + + character = input.get(); + if (character == EOF) { + std::cerr << "In file " << filename << ": unexpected end of file." << std::endl; + std::cerr << "Expecting 'T' at second byte, but found nothing." << std::endl; + m_rwstatus = false; return m_rwstatus; + } else if (character != 'T') { + std::cerr << "File " << filename << " is not a MIDI file" << std::endl; + std::cerr << "Expecting 'T' at second byte but got '" + << (char)character << "'" << std::endl; + m_rwstatus = false; return m_rwstatus; + } + + character = input.get(); + if (character == EOF) { + std::cerr << "In file " << filename << ": unexpected end of file." << std::endl; + std::cerr << "Expecting 'h' at third byte, but found nothing." << std::endl; + m_rwstatus = false; return m_rwstatus; + } else if (character != 'h') { + std::cerr << "File " << filename << " is not a MIDI file" << std::endl; + std::cerr << "Expecting 'h' at third byte but got '" + << (char)character << "'" << std::endl; + m_rwstatus = false; return m_rwstatus; + } + + character = input.get(); + if (character == EOF) { + std::cerr << "In file " << filename << ": unexpected end of file." << std::endl; + std::cerr << "Expecting 'd' at fourth byte, but found nothing." << std::endl; + m_rwstatus = false; return m_rwstatus; + } else if (character != 'd') { + std::cerr << "File " << filename << " is not a MIDI file" << std::endl; + std::cerr << "Expecting 'd' at fourth byte but got '" + << (char)character << "'" << std::endl; + m_rwstatus = false; return m_rwstatus; + } + + // read header size (allow larger header size?) + longdata = readLittleEndian4Bytes(input); + if (longdata != 6) { + std::cerr << "File " << filename + << " is not a MIDI 1.0 Standard MIDI file." << std::endl; + std::cerr << "The header size is " << longdata << " bytes." << std::endl; + m_rwstatus = false; return m_rwstatus; + } + + // Header parameter #1: format type + int type; + shortdata = readLittleEndian2Bytes(input); + switch (shortdata) { + case 0: + type = 0; + break; + case 1: + type = 1; + break; + case 2: + // Type-2 MIDI files should probably be allowed as well, + // but I have never seen one in the wild to test with. + default: + std::cerr << "Error: cannot handle a type-" << shortdata + << " MIDI file" << std::endl; + m_rwstatus = false; return m_rwstatus; + } + + // Header parameter #2: track count + int tracks; + shortdata = readLittleEndian2Bytes(input); + if (type == 0 && shortdata != 1) { + std::cerr << "Error: Type 0 MIDI file can only contain one track" << std::endl; + std::cerr << "Instead track count is: " << shortdata << std::endl; + m_rwstatus = false; return m_rwstatus; + } else { + tracks = shortdata; + } + clear(); + if (m_events[0] != NULL) { + delete m_events[0]; + } + m_events.resize(tracks); + for (int z=0; zreserve(10000); // Initialize with 10,000 event storage. + m_events[z]->clear(); + } + + // Header parameter #3: Ticks per quarter note + shortdata = readLittleEndian2Bytes(input); + if (shortdata >= 0x8000) { + int framespersecond = 255 - ((shortdata >> 8) & 0x00ff) + 1; + int subframes = shortdata & 0x00ff; + switch (framespersecond) { + case 25: framespersecond = 25; break; + case 24: framespersecond = 24; break; + case 29: framespersecond = 29; break; // really 29.97 for color television + case 30: framespersecond = 30; break; + default: + std::cerr << "Warning: unknown FPS: " << framespersecond << std::endl; + std::cerr << "Using non-standard FPS: " << framespersecond << std::endl; + } + m_ticksPerQuarterNote = framespersecond * subframes; + + // std::cerr << "SMPTE ticks: " << m_ticksPerQuarterNote << " ticks/sec" << std::endl; + // std::cerr << "SMPTE frames per second: " << framespersecond << std::endl; + // std::cerr << "SMPTE subframes per frame: " << subframes << std::endl; + } else { + m_ticksPerQuarterNote = shortdata; + } + + + ////////////////////////////////////////////////// + // + // now read individual tracks: + // + + uchar runningCommand; + MidiEvent event; + std::vector bytes; + int xstatus; + + for (int i=0; ireserve((int)longdata/2); + m_events[i]->clear(); + + // Read MIDI events in the track, which are pairs of VLV values + // and then the bytes for the MIDI message. Running status messags + // will be filled in with their implicit command byte. + // The timestamps are converted from delta ticks to absolute ticks, + // with the absticks variable accumulating the VLV tick values. + int absticks = 0; + while (!input.eof()) { + longdata = readVLValue(input); + absticks += longdata; + xstatus = extractMidiData(input, bytes, runningCommand); + if (xstatus == 0) { + m_rwstatus = false; return m_rwstatus; + } + event.setMessage(bytes); + event.tick = absticks; + event.track = i; + + if (bytes[0] == 0xff && bytes[1] == 0x2f) { + // end-of-track message + // comment out the following line if you don't want to see the + // end of track message (which is always required, and will added + // automatically when a MIDI is written, so it is not necessary. + m_events[i]->push_back(event); + break; + } + m_events[i]->push_back(event); + } + } + + m_theTimeState = TIME_STATE_ABSOLUTE; + + // The original order of the MIDI events is marked with an enumeration which + // allows for reconstruction of the order when merging/splitting tracks to/from + // a type-0 configuration. + markSequence(); + + return m_rwstatus; +} + + + +////////////////////////////// +// +// MidiFile::write -- write a standard MIDI file to a file or an output +// stream. +// + +bool MidiFile::write(const std::string& filename) { + std::fstream output(filename.c_str(), std::ios::binary | std::ios::out); + + if (!output.is_open()) { + std::cerr << "Error: could not write: " << filename << std::endl; + return false; + } + m_rwstatus = write(output); + output.close(); + return m_rwstatus; +} + +// +// ostream version of MidiFile::write(). +// + +bool MidiFile::write(std::ostream& out) { + int oldTimeState = getTickState(); + if (oldTimeState == TIME_STATE_ABSOLUTE) { + makeDeltaTicks(); + } + + // write the header of the Standard MIDI File + char ch; + // 1. The characters "MThd" + ch = 'M'; out << ch; + ch = 'T'; out << ch; + ch = 'h'; out << ch; + ch = 'd'; out << ch; + + // 2. write the size of the header (always a "6" stored in unsigned long + // (4 bytes). + ulong longdata = 6; + writeBigEndianULong(out, longdata); + + // 3. MIDI file format, type 0, 1, or 2 + ushort shortdata; + shortdata = static_cast(getNumTracks() == 1 ? 0 : 1); + writeBigEndianUShort(out,shortdata); + + // 4. write out the number of tracks. + shortdata = static_cast(getNumTracks()); + writeBigEndianUShort(out, shortdata); + + // 5. write out the number of ticks per quarternote. (avoiding SMTPE for now) + shortdata = static_cast(getTicksPerQuarterNote()); + writeBigEndianUShort(out, shortdata); + + // now write each track. + std::vector trackdata; + uchar endoftrack[4] = {0, 0xff, 0x2f, 0x00}; + int i, j, k; + int size; + for (i=0; isize(); j++) { + if ((*m_events[i])[j].empty()) { + // Don't write empty m_events (probably a delete message). + continue; + } + if ((*m_events[i])[j].isEndOfTrack()) { + // Suppress end-of-track meta messages (one will be added + // automatically after all track data has been written). + continue; + } + writeVLValue((*m_events[i])[j].tick, trackdata); + if (((*m_events[i])[j].getCommandByte() == 0xf0) || + ((*m_events[i])[j].getCommandByte() == 0xf7)) { + // 0xf0 == Complete sysex message (0xf0 is part of the raw MIDI). + // 0xf7 == Raw byte message (0xf7 not part of the raw MIDI). + // Print the first byte of the message (0xf0 or 0xf7), then + // print a VLV length for the rest of the bytes in the message. + // In other words, when creating a 0xf0 or 0xf7 MIDI message, + // do not insert the VLV byte length yourself, as this code will + // do it for you automatically. + trackdata.push_back((*m_events[i])[j][0]); // 0xf0 or 0xf7; + writeVLValue(((int)(*m_events[i])[j].size())-1, trackdata); + for (k=1; k<(int)(*m_events[i])[j].size(); k++) { + trackdata.push_back((*m_events[i])[j][k]); + } + } else { + // non-sysex type of message, so just output the + // bytes of the message: + for (k=0; k<(int)(*m_events[i])[j].size(); k++) { + trackdata.push_back((*m_events[i])[j][k]); + } + } + } + size = (int)trackdata.size(); + if ((size < 3) || !((trackdata[size-3] == 0xff) + && (trackdata[size-2] == 0x2f))) { + trackdata.push_back(endoftrack[0]); + trackdata.push_back(endoftrack[1]); + trackdata.push_back(endoftrack[2]); + trackdata.push_back(endoftrack[3]); + } + + // now ready to write to MIDI file. + + // first write the track ID marker "MTrk": + ch = 'M'; out << ch; + ch = 'T'; out << ch; + ch = 'r'; out << ch; + ch = 'k'; out << ch; + + // A. write the size of the MIDI data to follow: + longdata = (int)trackdata.size(); + writeBigEndianULong(out, longdata); + + // B. write the actual data + out.write((char*)trackdata.data(), trackdata.size()); + } + + if (oldTimeState == TIME_STATE_ABSOLUTE) { + makeAbsoluteTicks(); + } + + return true; +} + + + +////////////////////////////// +// +// MidiFile::writeBase64 -- Write Standard MIDI file with base64 encoding. +// The width parameter can be used to add line breaks. Zero or negative +// width will prevent linebreaks from being added to the data. +// Default value: width = 0 +// + +bool MidiFile::writeBase64(const std::string& filename, int width) { + std::fstream output(filename.c_str(), std::ios::binary | std::ios::out); + + if (!output.is_open()) { + std::cerr << "Error: could not write: " << filename << std::endl; + return false; + } + m_rwstatus = writeBase64(output, width); + output.close(); + return m_rwstatus; +} + + +bool MidiFile::writeBase64(std::ostream& out, int width) { + std::stringstream raw; + bool status = MidiFile::write(raw); + if (!status) { + return status; + } + std::string encoded = MidiFile::base64Encode(raw.str()); + if (width <= 0) { + out << encoded; + return status; + } + int length = (int)encoded.size(); + for (int i=0; i= 0 ? width : 25; + for (int i=0; iremoveEmpties(); + } +} + + + +////////////////////////////// +// +// MidiFile::markSequence -- Assign a sequence serial number to +// every MidiEvent in every track in the MIDI file. This is +// useful if you want to preseve the order of MIDI messages in +// a track when they occur at the same tick time. Particularly +// for use with joinTracks() or sortTracks(). markSequence will +// be done automatically when a MIDI file is read, in case the +// ordering of m_events occuring at the same time is important. +// Use clearSequence() to use the default sorting behavior of +// sortTracks(). +// + +void MidiFile::markSequence(void) { + int sequence = 1; + for (int i=0; i= 0) && (track < getTrackCount())) { + operator[](track).markSequence(sequence); + } else { + std::cerr << "Warning: track " << track << " does not exist." << std::endl; + } +} + + + +////////////////////////////// +// +// MidiFile::clearSequence -- Remove any seqence serial numbers from +// MidiEvents in the MidiFile. This will cause the default ordering by +// sortTracks() to be used, in which case the ordering of MidiEvents +// occurring at the same tick may switch their ordering. +// + +void MidiFile::clearSequence(void) { + for (int i=0; i= 0) && (track < getTrackCount())) { + operator[](track).clearSequence(); + } else { + std::cerr << "Warning: track " << track << " does not exist." << std::endl; + } +} + + + +////////////////////////////// +// +// MidiFile::joinTracks -- Interleave the data from all tracks, +// but keeping the identity of the tracks unique so that +// the function splitTracks can be called to split the +// tracks into separate units again. The style of the +// MidiFile when read from a file is with tracks split. +// The original track index is stored in the MidiEvent::track +// variable. +// + +void MidiFile::joinTracks(void) { + if (getTrackState() == TRACK_STATE_JOINED) { + return; + } + if (getNumTracks() == 1) { + m_theTrackState = TRACK_STATE_JOINED; + return; + } + + MidiEventList* joinedTrack; + joinedTrack = new MidiEventList; + + int messagesum = 0; + int length = getNumTracks(); + int i, j; + for (i=0; ireserve((int)(messagesum + 32 + messagesum * 0.1)); + + int oldTimeState = getTickState(); + if (oldTimeState == TIME_STATE_DELTA) { + makeAbsoluteTicks(); + } + for (i=0; isize(); j++) { + joinedTrack->push_back_no_copy(&(*m_events[i])[j]); + } + } + + clear_no_deallocate(); + + delete m_events[0]; + m_events.resize(0); + m_events.push_back(joinedTrack); + sortTracks(); + if (oldTimeState == TIME_STATE_DELTA) { + makeDeltaTicks(); + } + + m_theTrackState = TRACK_STATE_JOINED; +} + + + +////////////////////////////// +// +// MidiFile::splitTracks -- Take the joined tracks and split them +// back into their separate track identities. +// + +void MidiFile::splitTracks(void) { + if (getTrackState() == TRACK_STATE_SPLIT) { + return; + } + int oldTimeState = getTickState(); + if (oldTimeState == TIME_STATE_DELTA) { + makeAbsoluteTicks(); + } + + int maxTrack = 0; + int i; + int length = m_events[0]->size(); + for (i=0; i maxTrack) { + maxTrack = (*m_events[0])[i].track; + } + } + int trackCount = maxTrack + 1; + + if (trackCount <= 1) { + return; + } + + MidiEventList* olddata = m_events[0]; + m_events[0] = NULL; + m_events.resize(trackCount); + for (i=0; ipush_back_no_copy(&(*olddata)[i]); + } + + olddata->detach(); + delete olddata; + + if (oldTimeState == TIME_STATE_DELTA) { + makeDeltaTicks(); + } + + m_theTrackState = TRACK_STATE_SPLIT; +} + + + +////////////////////////////// +// +// MidiFile::splitTracksByChannel -- Take the joined tracks and split them +// back into their separate track identities. +// + +void MidiFile::splitTracksByChannel(void) { + joinTracks(); + if (getTrackState() == TRACK_STATE_SPLIT) { + return; + } + + int oldTimeState = getTickState(); + if (oldTimeState == TIME_STATE_DELTA) { + makeAbsoluteTicks(); + } + + int maxTrack = 0; + int i; + MidiEventList& eventlist = *m_events[0]; + MidiEventList* olddata = &eventlist; + int length = eventlist.size(); + for (i=0; i 0) { + trackValue = (eventlist[i][0] & 0x0f) + 1; + } + m_events[trackValue]->push_back_no_copy(&eventlist[i]); + } + + olddata->detach(); + delete olddata; + + if (oldTimeState == TIME_STATE_DELTA) { + makeDeltaTicks(); + } + + m_theTrackState = TRACK_STATE_SPLIT; +} + + + +////////////////////////////// +// +// MidiFile::getTrackState -- returns what type of track method +// is being used: either TRACK_STATE_JOINED or TRACK_STATE_SPLIT. +// + +int MidiFile::getTrackState(void) const { + return m_theTrackState; +} + + + +////////////////////////////// +// +// MidiFile::hasJoinedTracks -- Returns true if the MidiFile tracks +// are in a joined state. +// + +int MidiFile::hasJoinedTracks(void) const { + return m_theTrackState == TRACK_STATE_JOINED; +} + + + +////////////////////////////// +// +// MidiFile::hasSplitTracks -- Returns true if the MidiFile tracks +// are in a split state. +// + +int MidiFile::hasSplitTracks(void) const { + return m_theTrackState == TRACK_STATE_SPLIT; +} + + + +////////////////////////////// +// +// MidiFile::getSplitTrack -- Return the track index when the MidiFile +// is in the split state. This function returns the original track +// when the MidiFile is in the joined state. The MidiEvent::track +// variable is used to store the original track index when the +// MidiFile is converted to the joined-track state. +// + +int MidiFile::getSplitTrack(int track, int index) const { + if (hasSplitTracks()) { + return track; + } else { + return getEvent(track, index).track; + } +} + +// +// When the parameter is void, assume track 0: +// + +int MidiFile::getSplitTrack(int index) const { + if (hasSplitTracks()) { + return 0; + } else { + return getEvent(0, index).track; + } +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// tick-related functions -- +// + +////////////////////////////// +// +// MidiFile::makeDeltaTicks -- convert the time data to +// delta time, which means that the time field +// in the MidiEvent struct represents the time +// since the last event was played. When a MIDI file +// is read from a file, this is the default setting. +// + +void MidiFile::makeDeltaTicks(void) { + if (getTickState() == TIME_STATE_DELTA) { + return; + } + int i, j; + int temp; + int length = getNumTracks(); + int *timedata = new int[length]; + for (i=0; isize() > 0) { + timedata[i] = (*m_events[i])[0].tick; + } else { + continue; + } + for (j=1; j<(int)m_events[i]->size(); j++) { + temp = (*m_events[i])[j].tick; + int deltatick = temp - timedata[i]; + if (deltatick < 0) { + std::cerr << "Error: negative delta tick value: " << deltatick << std::endl + << "Timestamps must be sorted first" + << " (use MidiFile::sortTracks() before writing)." << std::endl; + } + (*m_events[i])[j].tick = deltatick; + timedata[i] = temp; + } + } + m_theTimeState = TIME_STATE_DELTA; + delete [] timedata; +} + +// +// MidiFile::deltaTicks -- Alias for MidiFile::makeDeltaTicks(). +// + +void MidiFile::deltaTicks(void) { + makeDeltaTicks(); +} + + + +////////////////////////////// +// +// MidiFile::makeAbsoluteTicks -- convert the time data to +// absolute time, which means that the time field +// in the MidiEvent struct represents the exact tick +// time to play the event rather than the time since +// the last event to wait untill playing the current +// event. +// + +void MidiFile::makeAbsoluteTicks(void) { + if (getTickState() == TIME_STATE_ABSOLUTE) { + return; + } + int i, j; + int length = getNumTracks(); + int* timedata = new int[length]; + for (i=0; isize() > 0) { + timedata[i] = (*m_events[i])[0].tick; + } else { + continue; + } + for (j=1; j<(int)m_events[i]->size(); j++) { + timedata[i] += (*m_events[i])[j].tick; + (*m_events[i])[j].tick = timedata[i]; + } + } + m_theTimeState = TIME_STATE_ABSOLUTE; + delete [] timedata; +} + +// +// MidiFile::absoluteTicks -- Alias for MidiFile::makeAbsoluteTicks(). +// + +void MidiFile::absoluteTicks(void) { + makeAbsoluteTicks(); +} + + + +////////////////////////////// +// +// MidiFile::getTickState -- returns what type of time method is +// being used: either TIME_STATE_ABSOLUTE or TIME_STATE_DELTA. +// + +int MidiFile::getTickState(void) const { + return m_theTimeState; +} + + + +////////////////////////////// +// +// MidiFile::isDeltaTicks -- Returns true if MidiEvent .tick +// variables are in delta time mode. +// + +bool MidiFile::isDeltaTicks(void) const { + return m_theTimeState == TIME_STATE_DELTA ? true : false; +} + + + +////////////////////////////// +// +// MidiFile::isAbsoluteTicks -- Returns true if MidiEvent .tick +// variables are in absolute time mode. +// + +bool MidiFile::isAbsoluteTicks(void) const { + return m_theTimeState == TIME_STATE_ABSOLUTE ? true : false; +} + + + +////////////////////////////// +// +// MidiFile::getFileDurationInTicks -- Returns the largest +// tick value in any track. The tracks must be sorted +// before calling this function, since this function +// assumes that the last MidiEvent in the track has the +// highest tick timestamp. The file state can be in delta +// ticks since this function will temporarily go to absolute +// tick mode for the calculation of the max tick. +// + +int MidiFile::getFileDurationInTicks(void) { + bool revertToDelta = false; + if (isDeltaTicks()) { + makeAbsoluteTicks(); + revertToDelta = true; + } + const MidiFile& mf = *this; + int output = 0; + for (int i=0; i output) { + output = mf[i].back().tick; + } + } + if (revertToDelta) { + deltaTicks(); + } + return output; +} + + + +/////////////////////////////// +// +// MidiFile::getFileDurationInQuarters -- Returns the Duration of the MidiFile +// in units of quarter notes. If the MidiFile is in delta tick mode, +// then temporarily got into absolute tick mode to do the calculations. +// Note that this is expensive, so you should normally call this function +// while in aboslute tick (default) mode. +// + +double MidiFile::getFileDurationInQuarters(void) { + return (double)getFileDurationInTicks() / (double)getTicksPerQuarterNote(); +} + + + +////////////////////////////// +// +// MidiFile::getFileDurationInSeconds -- returns the duration of the +// logest track in the file. The tracks must be sorted before +// calling this function, since this function assumes that the +// last MidiEvent in the track has the highest timestamp. +// The file state can be in delta ticks since this function +// will temporarily go to absolute tick mode for the calculation +// of the max time. + +double MidiFile::getFileDurationInSeconds(void) { + if (m_timemapvalid == 0) { + buildTimeMap(); + if (m_timemapvalid == 0) { + return -1.0; // something went wrong + } + } + bool revertToDelta = false; + if (isDeltaTicks()) { + makeAbsoluteTicks(); + revertToDelta = true; + } + const MidiFile& mf = *this; + double output = 0.0; + for (int i=0; i output) { + output = mf[i].back().seconds; + } + } + if (revertToDelta) { + deltaTicks(); + } + return output; +} + + +/////////////////////////////////////////////////////////////////////////// +// +// physical-time analysis functions -- +// + +////////////////////////////// +// +// MidiFile::doTimeAnalysis -- Identify the real-time position of +// all events by monitoring the tempo in relations to the tick +// times in the file. +// + +void MidiFile::doTimeAnalysis(void) { + buildTimeMap(); +} + + + +////////////////////////////// +// +// MidiFile::getTimeInSeconds -- return the time in seconds for +// the current message. +// + +double MidiFile::getTimeInSeconds(int aTrack, int anIndex) { + return getTimeInSeconds(getEvent(aTrack, anIndex).tick); +} + + +double MidiFile::getTimeInSeconds(int tickvalue) { + if (m_timemapvalid == 0) { + buildTimeMap(); + if (m_timemapvalid == 0) { + return -1.0; // something went wrong + } + } + + _TickTime key; + key.tick = tickvalue; + key.seconds = -1; + + void* ptr = bsearch(&key, m_timemap.data(), m_timemap.size(), + sizeof(_TickTime), ticksearch); + + if (ptr == NULL) { + // The specific tick value was not found, so do a linear + // search for the two tick values which occur before and + // after the tick value, and do a linear interpolation of + // the time in seconds values to figure out the final + // time in seconds. + // Since the code is not yet written, kill the program at this point: + return linearSecondInterpolationAtTick(tickvalue); + } else { + return ((_TickTime*)ptr)->seconds; + } +} + + + +////////////////////////////// +// +// MidiFile::getAbsoluteTickTime -- return the tick value represented +// by the input time in seconds. If there is not tick entry at +// the given time in seconds, then interpolate between two values. +// + +double MidiFile::getAbsoluteTickTime(double starttime) { + if (m_timemapvalid == 0) { + buildTimeMap(); + if (m_timemapvalid == 0) { + return -1.0; // something went wrong + } + } + + _TickTime key; + key.tick = -1; + key.seconds = starttime; + + void* ptr = bsearch(&key, m_timemap.data(), m_timemap.size(), + sizeof(_TickTime), secondsearch); + + if (ptr == NULL) { + // The specific seconds value was not found, so do a linear + // search for the two time values which occur before and + // after the given time value, and do a linear interpolation of + // the time in tick values to figure out the final time in ticks. + return linearTickInterpolationAtSecond(starttime); + } else { + return ((_TickTime*)ptr)->tick; + } + +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// note-analysis functions -- +// + +////////////////////////////// +// +// MidiFile::linkNotePairs -- Link note-ons to note-offs separately +// for each track. Returns the total number of note message pairs +// that were linked. +// + +int MidiFile::linkNotePairs(void) { + int i; + int sum = 0; + for (i=0; ilinkNotePairs(); + } + m_linkedEventsQ = true; + return sum; +} + +// +// MidiFile::linkEventPairs -- Alias for MidiFile::linkNotePairs(). +// + +int MidiFile::linkEventPairs(void) { + return linkNotePairs(); +} + + +/////////////////////////////////////////////////////////////////////////// +// +// filename functions -- +// + +////////////////////////////// +// +// MidiFile::setFilename -- sets the filename of the MIDI file. +// Currently removed any directory path. +// + +void MidiFile::setFilename(const std::string& aname) { + auto loc = aname.rfind('/'); + if (loc != std::string::npos) { + m_readFileName = aname.substr(loc+1); + } else { + m_readFileName = aname; + } +} + + + +////////////////////////////// +// +// MidiFile::getFilename -- returns the name of the file read into the +// structure (if the data was read from a file). +// + +const char* MidiFile::getFilename(void) const { + return m_readFileName.c_str(); +} + + + +////////////////////////////// +// +// MidiFile::addEvent -- +// + +MidiEvent* MidiFile::addEvent(int aTrack, int aTick, + std::vector& midiData) { + m_timemapvalid = 0; + MidiEvent* me = new MidiEvent; + me->tick = aTick; + me->track = aTrack; + me->setMessage(midiData); + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addEvent -- Some bug here when joinedTracks(), but track==1... +// + +MidiEvent* MidiFile::addEvent(MidiEvent& mfevent) { + if (getTrackState() == TRACK_STATE_JOINED) { + m_events[0]->push_back(mfevent); + return &m_events[0]->back(); + } else { + m_events.at(mfevent.track)->push_back(mfevent); + return &m_events.at(mfevent.track)->back(); + } +} + +// +// Variant where the track is an input parameter: +// + +MidiEvent* MidiFile::addEvent(int aTrack, MidiEvent& mfevent) { + if (getTrackState() == TRACK_STATE_JOINED) { + m_events[0]->push_back(mfevent); + m_events[0]->back().track = aTrack; + return &m_events[0]->back(); + } else { + m_events.at(aTrack)->push_back(mfevent); + m_events.at(aTrack)->back().track = aTrack; + return &m_events.at(aTrack)->back(); + } +} + + + +/////////////////////////////// +// +// MidiFile::addMetaEvent -- +// + +MidiEvent* MidiFile::addMetaEvent(int aTrack, int aTick, int aType, + std::vector& metaData) { + m_timemapvalid = 0; + int i; + int length = (int)metaData.size(); + std::vector fulldata; + uchar size[23] = {0}; + int lengthsize = makeVLV(size, length); + + fulldata.resize(2+lengthsize+length); + fulldata[0] = 0xff; + fulldata[1] = aType & 0x7F; + for (i=0; i buffer; + buffer.resize(length); + int i; + for (i=0; imakeText(text); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addCopyright -- Add a copyright notice meta-message (#2). +// + +MidiEvent* MidiFile::addCopyright(int aTrack, int aTick, const std::string& text) { + MidiEvent* me = new MidiEvent; + me->makeCopyright(text); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addTrackName -- Add an track name meta-message (#3). +// + +MidiEvent* MidiFile::addTrackName(int aTrack, int aTick, const std::string& name) { + MidiEvent* me = new MidiEvent; + me->makeTrackName(name); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addInstrumentName -- Add an instrument name meta-message (#4). +// + +MidiEvent* MidiFile::addInstrumentName(int aTrack, int aTick, + const std::string& name) { + MidiEvent* me = new MidiEvent; + me->makeInstrumentName(name); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addLyric -- Add a lyric meta-message (meta #5). +// + +MidiEvent* MidiFile::addLyric(int aTrack, int aTick, const std::string& text) { + MidiEvent* me = new MidiEvent; + me->makeLyric(text); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addMarker -- Add a marker meta-message (meta #6). +// + +MidiEvent* MidiFile::addMarker(int aTrack, int aTick, const std::string& text) { + MidiEvent* me = new MidiEvent; + me->makeMarker(text); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addCue -- Add a cue-point meta-message (meta #7). +// + +MidiEvent* MidiFile::addCue(int aTrack, int aTick, const std::string& text) { + MidiEvent* me = new MidiEvent; + me->makeCue(text); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addTempo -- Add a tempo meta message (meta #0x51). +// + +MidiEvent* MidiFile::addTempo(int aTrack, int aTick, double aTempo) { + MidiEvent* me = new MidiEvent; + me->makeTempo(aTempo); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addTimeSignature -- Add a time signature meta message +// (meta #0x58). The "bottom" parameter must be a power of two; +// otherwise, it will be set to the next highest power of two. +// +// Default values: +// clocksPerClick == 24 (quarter note) +// num32ndsPerQuarter == 8 (8 32nds per quarter note) +// +// Time signature of 4/4 would be: +// top = 4 +// bottom = 4 (converted to 2 in the MIDI file for 2nd power of 2). +// clocksPerClick = 24 (2 eighth notes based on num32ndsPerQuarter) +// num32ndsPerQuarter = 8 +// +// Time signature of 6/8 would be: +// top = 6 +// bottom = 8 (converted to 3 in the MIDI file for 3rd power of 2). +// clocksPerClick = 36 (3 eighth notes based on num32ndsPerQuarter) +// num32ndsPerQuarter = 8 +// + +MidiEvent* MidiFile::addTimeSignature(int aTrack, int aTick, int top, int bottom, + int clocksPerClick, int num32ndsPerQuarter) { + MidiEvent* me = new MidiEvent; + me->makeTimeSignature(top, bottom, clocksPerClick, num32ndsPerQuarter); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addCompoundTimeSignature -- Add a time signature meta message +// (meta #0x58), where the clocksPerClick parameter is set to three +// eighth notes for compount meters such as 6/8 which represents +// two beats per measure. +// +// Default values: +// clocksPerClick == 36 (quarter note) +// num32ndsPerQuarter == 8 (8 32nds per quarter note) +// + +MidiEvent* MidiFile::addCompoundTimeSignature(int aTrack, int aTick, int top, + int bottom, int clocksPerClick, int num32ndsPerQuarter) { + return addTimeSignature(aTrack, aTick, top, bottom, clocksPerClick, + num32ndsPerQuarter); +} + + + +////////////////////////////// +// +// MidiFile::makeVLV -- This function is used to create +// size byte(s) for meta-messages. If the size of the data +// in the meta-message is greater than 127, then the size +// should (?) be specified as a VLV. +// + +int MidiFile::makeVLV(uchar *buffer, int number) { + + unsigned long value = (unsigned long)number; + + if (value >= (1 << 28)) { + std::cerr << "Error: Meta-message size too large to handle" << std::endl; + buffer[0] = 0; + buffer[1] = 0; + buffer[2] = 0; + buffer[3] = 0; + return 1; + } + + buffer[0] = (value >> 21) & 0x7f; + buffer[1] = (value >> 14) & 0x7f; + buffer[2] = (value >> 7) & 0x7f; + buffer[3] = (value >> 0) & 0x7f; + + int i; + int flag = 0; + int length = -1; + for (i=0; i<3; i++) { + if (buffer[i] != 0) { + flag = 1; + } + if (flag) { + buffer[i] |= 0x80; + } + if (length == -1 && buffer[i] >= 0x80) { + length = 4-i; + } + } + + if (length == -1) { + length = 1; + } + + if (length < 4) { + for (i=0; imakeNoteOn(aChannel, key, vel); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addNoteOff -- Add a note-off message (using 0x80 messages). +// + +MidiEvent* MidiFile::addNoteOff(int aTrack, int aTick, int aChannel, int key, + int vel) { + MidiEvent* me = new MidiEvent; + me->makeNoteOff(aChannel, key, vel); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addNoteOff -- Add a note-off message (using 0x90 messages with +// zero attack velocity). +// + +MidiEvent* MidiFile::addNoteOff(int aTrack, int aTick, int aChannel, int key) { + MidiEvent* me = new MidiEvent; + me->makeNoteOff(aChannel, key); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addController -- Add a controller message in the given +// track at the given tick time in the given channel. +// + +MidiEvent* MidiFile::addController(int aTrack, int aTick, int aChannel, + int num, int value) { + MidiEvent* me = new MidiEvent; + me->makeController(aChannel, num, value); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addPatchChange -- Add a patch-change message in the given +// track at the given tick time in the given channel. +// + +MidiEvent* MidiFile::addPatchChange(int aTrack, int aTick, int aChannel, + int patchnum) { + MidiEvent* me = new MidiEvent; + me->makePatchChange(aChannel, patchnum); + me->tick = aTick; + m_events[aTrack]->push_back_no_copy(me); + return me; +} + + + +////////////////////////////// +// +// MidiFile::addTimbre -- Add a patch-change message in the given +// track at the given tick time in the given channel. Alias for +// MidiFile::addPatchChange(). +// + +MidiEvent* MidiFile::addTimbre(int aTrack, int aTick, int aChannel, int patchnum) { + return addPatchChange(aTrack, aTick, aChannel, patchnum); +} + + + +////////////////////////////// +// +// MidiFile::addPitchBend -- convert number in the range from -1 to +1 +// into two 7-bit numbers (smallest piece first) +// +// -1.0 maps to 0 (0x0000) +// 0.0 maps to 8192 (0x2000 --> 0x40 0x00) +// +1.0 maps to 16383 (0x3FFF --> 0x7F 0x7F) +// + +MidiEvent* MidiFile::addPitchBend(int aTrack, int aTick, int aChannel, double amount) { + m_timemapvalid = 0; + amount += 1.0; + int value = int(amount * 8192 + 0.5); + + // prevent any wrap-around in case of round-off errors + if (value > 0x3fff) { + value = 0x3fff; + } + if (value < 0) { + value = 0; + } + + int lsbint = 0x7f & value; + int msbint = 0x7f & (value >> 7); + + std::vector mididata; + mididata.resize(3); + if (aChannel < 0) { + aChannel = 0; + } else if (aChannel > 15) { + aChannel = 15; + } + mididata[0] = uchar(0xe0 | aChannel); + mididata[1] = uchar(lsbint); + mididata[2] = uchar(msbint); + + return addEvent(aTrack, aTick, mididata); +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// RPN convenience functions: +// + +////////////////////////////// +// +// MidiFile::setPitchBendRange -- Set the range for the min/max pitch bend +// alteration of a note. Default is 2.0 (meaning +/- 2 semitones from given pitch). +// Fractional values are cents, so 2.5 means a range of two semitones plus 50 cents, +// which is two semitones plus a quarter tone. +// + +void MidiFile::setPitchBendRange(int aTrack, int aTick, int aChannel, double range) { + if (range < 0.0) { + range = -range; + } + if (range > 24.0) { + std::cerr << "Warning: pitch bend range is too large: " << range << std::endl; + std::cerr << "Setting to 24." << std::endl; + range = 24.0; + } + int irange = int(range); + int cents = int((range - irange) * 100.0 + 0.5); + + // Select pitch bend RPN: + addController(aTrack, aTick, aChannel, 101, 0); // RPN selector (byte 1) + addController(aTrack, aTick, aChannel, 100, 0); // RPN selector (byte 2) + + // Set the semitone range (will be +/-range above/below a note): + addController(aTrack, aTick, aChannel, 6, irange); // coarse: number of semitones + addController(aTrack, aTick, aChannel, 38, cents); // fine: cents (1/100ths of semitone) +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// Controller message adding convenience functions: +// + +////////////////////////////// +// +// MidiFile::addSustain -- Add a continuous controller message for the sustain pedal. +// + +MidiEvent* MidiFile::addSustain(int aTrack, int aTick, int aChannel, int value) { + return addController(aTrack, aTick, aChannel, 64, value); +} + +// +// MidiFile::addSustainPedal -- Alias for MidiFile::addSustain(). +// + +MidiEvent* MidiFile::addSustainPedal(int aTrack, int aTick, int aChannel, int value) { + return addSustain(aTrack, aTick, aChannel, value); +} + + + +////////////////////////////// +// +// MidiFile::addSustainOn -- Add a continuous controller message for the sustain pedal on. +// + +MidiEvent* MidiFile::addSustainOn(int aTrack, int aTick, int aChannel) { + return addSustain(aTrack, aTick, aChannel, 127); +} + +// +// MidiFile::addSustainPedalOn -- Alias for MidiFile::addSustainOn(). +// + +MidiEvent* MidiFile::addSustainPedalOn(int aTrack, int aTick, int aChannel) { + return addSustainOn(aTrack, aTick, aChannel); +} + + + +////////////////////////////// +// +// MidiFile::addSustainOff -- Add a continuous controller message for the sustain pedal off. +// + +MidiEvent* MidiFile::addSustainOff(int aTrack, int aTick, int aChannel) { + return addSustain(aTrack, aTick, aChannel, 0); +} + +// +// MidiFile::addSustainPedalOff -- Alias for MidiFile::addSustainOff(). +// + +MidiEvent* MidiFile::addSustainPedalOff(int aTrack, int aTick, int aChannel) { + return addSustainOff(aTrack, aTick, aChannel); +} + + + +////////////////////////////// +// +// MidiFile::addTrack -- adds a blank track at end of the +// track list. Returns the track number of the added +// track. +// + +int MidiFile::addTrack(void) { + int length = getNumTracks(); + m_events.resize(length+1); + m_events[length] = new MidiEventList; + m_events[length]->reserve(10000); + m_events[length]->clear(); + return length; +} + +int MidiFile::addTrack(int count) { + int length = getNumTracks(); + m_events.resize(length+count); + int i; + for (i=0; ireserve(10000); + m_events[length + i]->clear(); + } + return length + count - 1; +} + +// +// MidiFile::addTracks -- Alias for MidiFile::addTrack(). +// + +int MidiFile::addTracks(int count) { + return addTrack(count); +} + + + + +////////////////////////////// +// +// MidiFile::allocateEvents -- +// + +void MidiFile::allocateEvents(int track, int aSize) { + int oldsize = m_events[track]->size(); + if (oldsize < aSize) { + m_events[track]->reserve(aSize); + } +} + + + +////////////////////////////// +// +// MidiFile::deleteTrack -- remove a track from the MidiFile. +// Tracks are numbered starting at track 0. +// + +void MidiFile::deleteTrack(int aTrack) { + int length = getNumTracks(); + if (aTrack < 0 || aTrack >= length) { + return; + } + if (length == 1) { + return; + } + delete m_events[aTrack]; + for (int i=aTrack; isize(); +} + + +int MidiFile::getNumEvents(int aTrack) const { + return m_events[aTrack]->size(); +} + + + +////////////////////////////// +// +// MidiFile::mergeTracks -- combine the data from two +// tracks into one. Placing the data in the first +// track location listed, and Moving the other tracks +// in the file around to fill in the spot where Track2 +// used to be. The results of this function call cannot +// be reversed. +// + +void MidiFile::mergeTracks(int aTrack1, int aTrack2) { + MidiEventList* mergedTrack; + mergedTrack = new MidiEventList; + int oldTimeState = getTickState(); + if (oldTimeState == TIME_STATE_DELTA) { + makeAbsoluteTicks(); + } + int length = getNumTracks(); + for (int i=0; i<(int)m_events[aTrack1]->size(); i++) { + mergedTrack->push_back((*m_events[aTrack1])[i]); + } + for (int j=0; j<(int)m_events[aTrack2]->size(); j++) { + (*m_events[aTrack2])[j].track = aTrack1; + mergedTrack->push_back((*m_events[aTrack2])[j]); + } + + mergedTrack->sort(); + + delete m_events[aTrack1]; + + m_events[aTrack1] = mergedTrack; + + for (int i=aTrack2; isize(); j++) { + (*m_events[i])[j].track = i; + } + } + + m_events[length-1] = NULL; + m_events.resize(length-1); + + if (oldTimeState == TIME_STATE_DELTA) { + deltaTicks(); + } +} + + + +////////////////////////////// +// +// MidiFile::setTicksPerQuarterNote -- +// + +void MidiFile::setTicksPerQuarterNote(int ticks) { + m_ticksPerQuarterNote = ticks; +} + +// +// Alias for setTicksPerQuarterNote: +// + +void MidiFile::setTPQ(int ticks) { + setTicksPerQuarterNote(ticks); +} + + +////////////////////////////// +// +// MidiFile::setMillisecondTicks -- set the ticks per quarter note +// value to milliseconds. The format for this specification is +// highest 8-bits: SMPTE Frame rate (as a negative 2's compliment value). +// lowest 8-bits: divisions per frame (as a positive number). +// for millisecond resolution, the SMPTE value is -25, and the +// frame rate is 40 frame per division. In hexadecimal, these +// values are: -25 = 1110,0111 = 0xE7 and 40 = 0010,1000 = 0x28 +// So setting the ticks per quarter note value to 0xE728 will cause +// delta times in the MIDI file to represent milliseconds. Calling +// this function will not change any exiting timestamps, it will +// only change the meaning of the timestamps. +// + +void MidiFile::setMillisecondTicks(void) { + m_ticksPerQuarterNote = 0xE728; +} + + + +////////////////////////////// +// +// MidiFile::sortTrack -- Sort the specified track in tick order. +// If the MidiEvent::seq variables have been filled in with +// a sequence value, this will preserve the order of the +// events that occur at the same tick time before the sort +// was done. +// + +void MidiFile::sortTrack(int track) { + if ((track >= 0) && (track < getTrackCount())) { + m_events.at(track)->sort(); + } else { + std::cerr << "Warning: track " << track << " does not exist." << std::endl; + } +} + + + +////////////////////////////// +// +// MidiFile::sortTracks -- sort all tracks in the MidiFile. +// + +void MidiFile::sortTracks(void) { + if (m_theTimeState == TIME_STATE_ABSOLUTE) { + for (int i=0; isort(); + } + } else { + std::cerr << "Warning: Sorting only allowed in absolute tick mode."; + } +} + + + +////////////////////////////// +// +// MidiFile::getTrackCountAsType1 -- Return the number of tracks in the +// MIDI file. Returns the size of the events if not in joined state. +// If in joined state, reads track 0 to find the maximum track +// value from the original unjoined tracks. +// + +int MidiFile::getTrackCountAsType1(void) { + if (getTrackState() == TRACK_STATE_JOINED) { + int output = 0; + int i; + for (i=0; i<(int)m_events[0]->size(); i++) { + if (getEvent(0,i).track > output) { + output = getEvent(0,i).track; + } + } + return output+1; // I think the track values are 0 offset... + } else { + return (int)m_events.size(); + } +} + + + +////////////////////////////// +// +// MidiFile::clearLinks -- +// + +void MidiFile::clearLinks(void) { + for (int i=0; iclearLinks(); + } + m_linkedEventsQ = false; +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// private functions +// + +////////////////////////////// +// +// MidiFile::linearTickInterpolationAtSecond -- return the tick value at the +// given input time. +// + +double MidiFile::linearTickInterpolationAtSecond(double seconds) { + if (m_timemapvalid == 0) { + buildTimeMap(); + if (m_timemapvalid == 0) { + return -1.0; // something went wrong + } + } + + int i; + double lasttime = m_timemap[m_timemap.size()-1].seconds; + // give an error value of -1 if time is out of range of data. + if (seconds < 0.0) { + return -1.0; + } + if (seconds > m_timemap[m_timemap.size()-1].seconds) { + return -1.0; + } + + // Guess which side of the list is closest to target: + // Could do a more efficient algorithm since time values are sorted, + // but good enough for now... + int startindex = -1; + if (seconds < lasttime / 2) { + for (i=0; i<(int)m_timemap.size(); i++) { + if (m_timemap[i].seconds > seconds) { + startindex = i-1; + break; + } else if (m_timemap[i].seconds == seconds) { + startindex = i; + break; + } + } + } else { + for (i=(int)m_timemap.size()-1; i>0; i--) { + if (m_timemap[i].seconds < seconds) { + startindex = i+1; + break; + } else if (m_timemap[i].seconds == seconds) { + startindex = i; + break; + } + } + } + + if (startindex < 0) { + return -1.0; + } + if (startindex >= (int)m_timemap.size()-1) { + return -1.0; + } + + double x1 = m_timemap[startindex].seconds; + double x2 = m_timemap[startindex+1].seconds; + double y1 = m_timemap[startindex].tick; + double y2 = m_timemap[startindex+1].tick; + double xi = seconds; + + return (xi-x1) * ((y2-y1)/(x2-x1)) + y1; +} + + + +////////////////////////////// +// +// MidiFile::linearSecondInterpolationAtTick -- return the time in seconds +// value at the given input tick time. (Ticks input could be made double). +// + +double MidiFile::linearSecondInterpolationAtTick(int ticktime) { + if (m_timemapvalid == 0) { + buildTimeMap(); + if (m_timemapvalid == 0) { + return -1.0; // something went wrong + } + } + + int i; + double lasttick = m_timemap[m_timemap.size()-1].tick; + // give an error value of -1 if time is out of range of data. + if (ticktime < 0.0) { + return -1; + } + if (ticktime > m_timemap.back().tick) { + return -1; // don't try to extrapolate + } + + // Guess which side of the list is closest to target: + // Could do a more efficient algorithm since time values are sorted, + // but good enough for now... + int startindex = -1; + if (ticktime < lasttick / 2) { + for (i=0; i<(int)m_timemap.size(); i++) { + if (m_timemap[i].tick > ticktime) { + startindex = i-1; + break; + } else if (m_timemap[i].tick == ticktime) { + startindex = i; + break; + } + } + } else { + for (i=(int)m_timemap.size()-1; i>0; i--) { + if (m_timemap[i].tick < ticktime) { + startindex = i; + break; + } else if (m_timemap[i].tick == ticktime) { + startindex = i; + break; + } + } + } + + if (startindex < 0) { + return -1; + } + if (startindex >= (int)m_timemap.size()-1) { + return -1; + } + + if (m_timemap[startindex].tick == ticktime) { + return m_timemap[startindex].seconds; + } + + double x1 = m_timemap[startindex].tick; + double x2 = m_timemap[startindex+1].tick; + double y1 = m_timemap[startindex].seconds; + double y2 = m_timemap[startindex+1].seconds; + double xi = ticktime; + + return (xi-x1) * ((y2-y1)/(x2-x1)) + y1; +} + + + +////////////////////////////// +// +// MidiFile::buildTimeMap -- build an index of the absolute tick values +// found in a MIDI file, and their corresponding time values in +// seconds, taking into consideration tempo change messages. If no +// tempo messages are given (or untill they are given, then the +// tempo is set to 120 beats per minute). If SMPTE time code is +// used, then ticks are actually time values. So don't build +// a time map for SMPTE ticks, and just calculate the time in +// seconds from the tick value (1000 ticks per second SMPTE +// is the only mode tested (25 frames per second and 40 subframes +// per frame). +// + +void MidiFile::buildTimeMap(void) { + + // convert the MIDI file to absolute time representation + // in single track mode (and undo if the MIDI file was not + // in that state when this function was called. + // + int trackstate = getTrackState(); + int timestate = getTickState(); + + makeAbsoluteTicks(); + joinTracks(); + + int allocsize = getNumEvents(0); + m_timemap.reserve(allocsize+10); + m_timemap.clear(); + + _TickTime value; + + int lasttick = 0; + int tickinit = 0; + + int i; + int tpq = getTicksPerQuarterNote(); + double defaultTempo = 120.0; + double secondsPerTick = 60.0 / (defaultTempo * tpq); + + double lastsec = 0.0; + double cursec = 0.0; + + for (i=0; i lasttick) || !tickinit) { + tickinit = 1; + + // calculate the current time in seconds: + cursec = lastsec + (curtick - lasttick) * secondsPerTick; + getEvent(0, i).seconds = cursec; + + // store the new tick to second mapping + value.tick = curtick; + value.seconds = cursec; + m_timemap.push_back(value); + lasttick = curtick; + lastsec = cursec; + } + + // update the tempo if needed: + if (getEvent(0,i).isTempo()) { + secondsPerTick = getEvent(0,i).getTempoSPT(getTicksPerQuarterNote()); + } + } + + // reset the states of the tracks or time values if necessary here: + if (timestate == TIME_STATE_DELTA) { + deltaTicks(); + } + if (trackstate == TRACK_STATE_SPLIT) { + splitTracks(); + } + + m_timemapvalid = 1; + +} + + + +////////////////////////////// +// +// MidiFile::extractMidiData -- Extract MIDI data from input +// stream. Return value is 0 if failure; otherwise, returns 1. +// + +int MidiFile::extractMidiData(std::istream& input, std::vector& array, + uchar& runningCommand) { + + int character; + uchar byte; + array.clear(); + int runningQ; + + character = input.get(); + if (character == EOF) { + std::cerr << "Error: unexpected end of file." << std::endl; + return 0; + } else { + byte = (uchar)character; + } + + if (byte < 0x80) { + runningQ = 1; + if (runningCommand == 0) { + std::cerr << "Error: running command with no previous command" << std::endl; + return 0; + } + if (runningCommand >= 0xf0) { + std::cerr << "Error: running status not permitted with meta and sysex" + << " event." << std::endl; + std::cerr << "Byte is 0x" << std::hex << (int)byte << std::dec << std::endl; + return 0; + } + } else { + runningCommand = byte; + runningQ = 0; + } + + array.push_back(runningCommand); + if (runningQ) { + array.push_back(byte); + } + + switch (runningCommand & 0xf0) { + case 0x80: // note off (2 more bytes) + case 0x90: // note on (2 more bytes) + case 0xA0: // aftertouch (2 more bytes) + case 0xB0: // cont. controller (2 more bytes) + case 0xE0: // pitch wheel (2 more bytes) + byte = readByte(input); + if (!status()) { return m_rwstatus; } + if (byte > 0x7f) { + std::cerr << "MIDI data byte too large: " << (int)byte << std::endl; + m_rwstatus = false; return m_rwstatus; + } + array.push_back(byte); + if (!runningQ) { + byte = readByte(input); + if (!status()) { return m_rwstatus; } + if (byte > 0x7f) { + std::cerr << "MIDI data byte too large: " << (int)byte << std::endl; + m_rwstatus = false; return m_rwstatus; + } + array.push_back(byte); + } + break; + case 0xC0: // patch change (1 more byte) + case 0xD0: // channel pressure (1 more byte) + if (!runningQ) { + byte = readByte(input); + if (!status()) { return m_rwstatus; } + if (byte > 0x7f) { + std::cerr << "MIDI data byte too large: " << (int)byte << std::endl; + m_rwstatus = false; return m_rwstatus; + } + array.push_back(byte); + } + break; + case 0xF0: + switch (runningCommand) { + case 0xff: // meta event + { + if (!runningQ) { + byte = readByte(input); // meta type + if (!status()) { return m_rwstatus; } + array.push_back(byte); + } + ulong length = 0; + uchar byte1 = 0; + uchar byte2 = 0; + uchar byte3 = 0; + uchar byte4 = 0; + byte1 = readByte(input); + if (!status()) { return m_rwstatus; } + array.push_back(byte1); + if (byte1 >= 0x80) { + byte2 = readByte(input); + if (!status()) { return m_rwstatus; } + array.push_back(byte2); + if (byte2 > 0x80) { + byte3 = readByte(input); + if (!status()) { return m_rwstatus; } + array.push_back(byte3); + if (byte3 >= 0x80) { + byte4 = readByte(input); + if (!status()) { return m_rwstatus; } + array.push_back(byte4); + if (byte4 >= 0x80) { + std::cerr << "Error: cannot handle large VLVs" << std::endl; + m_rwstatus = false; return m_rwstatus; + } else { + length = unpackVLV(byte1, byte2, byte3, byte4); + if (!m_rwstatus) { return m_rwstatus; } + } + } else { + length = unpackVLV(byte1, byte2, byte3); + if (!m_rwstatus) { return m_rwstatus; } + } + } else { + length = unpackVLV(byte1, byte2); + if (!m_rwstatus) { return m_rwstatus; } + } + } else { + length = byte1; + } + for (int j=0; j<(int)length; j++) { + byte = readByte(input); // meta type + if (!status()) { return m_rwstatus; } + array.push_back(byte); + } + } + break; + + // The 0xf0 and 0xf7 meta commands deal with system-exclusive + // messages. 0xf0 is used to either start a message or to store + // a complete message. The 0xf0 is part of the outgoing MIDI + // bytes. The 0xf7 message is used to send arbitrary bytes, + // typically the middle or ends of system exclusive messages. The + // 0xf7 byte at the start of the message is not part of the + // outgoing raw MIDI bytes, but is kept in the MidiFile message + // to indicate a raw MIDI byte message (typically a partial + // system exclusive message). + case 0xf7: // Raw bytes. 0xf7 is not part of the raw + // bytes, but are included to indicate + // that this is a raw byte message. + case 0xf0: // System Exclusive message + { // (complete, or start of message). + int length = (int)readVLValue(input); + for (int i=0; i 0x7f)) { + count++; + } + count++; + if (count >= 6) { + std::cerr << "VLV number is too large" << std::endl; + m_rwstatus = false; + return 0; + } + + ulong output = 0; + for (int i=0; i& outdata) { + uchar bytes[4] = {0}; + + if ((unsigned long)aValue >= (1 << 28)) { + std::cerr << "Error: number too large to convert to VLV" << std::endl; + aValue = 0x0FFFffff; + } + + bytes[0] = (uchar)(((ulong)aValue >> 21) & 0x7f); // most significant 7 bits + bytes[1] = (uchar)(((ulong)aValue >> 14) & 0x7f); + bytes[2] = (uchar)(((ulong)aValue >> 7) & 0x7f); + bytes[3] = (uchar)(((ulong)aValue) & 0x7f); // least significant 7 bits + + int start = 0; + while ((start<4) && (bytes[start] == 0)) start++; + + for (int i=start; i<3; i++) { + bytes[i] = bytes[i] | 0x80; + outdata.push_back(bytes[i]); + } + outdata.push_back(bytes[3]); +} + + + +////////////////////////////// +// +// MidiFile::clear_no_deallocate -- Similar to clear() but does not +// delete the Events in the lists. This is primarily used internally +// to the MidiFile class, so don't use unless you really know what you +// are doing (otherwise you will end up with memory leaks or +// segmentation faults). +// + +void MidiFile::clear_no_deallocate(void) { + for (int i=0; idetach(); + delete m_events[i]; + m_events[i] = NULL; + } + m_events.resize(1); + m_events[0] = new MidiEventList; + m_timemapvalid=0; + m_timemap.clear(); + // m_events.resize(0); // causes a memory leak [20150205 Jorden Thatcher] +} + + + +////////////////////////////// +// +// MidiFile::ticksearch -- for finding a tick entry in the time map. +// + +int MidiFile::ticksearch(const void* A, const void* B) { + _TickTime& a = *((_TickTime*)A); + _TickTime& b = *((_TickTime*)B); + + if (a.tick < b.tick) { + return -1; + } else if (a.tick > b.tick) { + return 1; + } + return 0; +} + + + +////////////////////////////// +// +// MidiFile::secondsearch -- for finding a second entry in the time map. +// + +int MidiFile::secondsearch(const void* A, const void* B) { + _TickTime& a = *((_TickTime*)A); + _TickTime& b = *((_TickTime*)B); + + if (a.seconds < b.seconds) { + return -1; + } else if (a.seconds > b.seconds) { + return 1; + } + return 0; +} + + +/////////////////////////////////////////////////////////////////////////// +// +// Static functions: +// + + +////////////////////////////// +// +// MidiFile::readLittleEndian4Bytes -- Read four bytes which are in +// little-endian order (smallest byte is first). Then flip +// the order of the bytes to create the return value. +// + +ulong MidiFile::readLittleEndian4Bytes(std::istream& input) { + uchar buffer[4] = {0}; + input.read((char*)buffer, 4); + if (input.eof()) { + std::cerr << "Error: unexpected end of file." << std::endl; + return 0; + } + return buffer[3] | (buffer[2] << 8) | (buffer[1] << 16) | (buffer[0] << 24); +} + + + +////////////////////////////// +// +// MidiFile::readLittleEndian2Bytes -- Read two bytes which are in +// little-endian order (smallest byte is first). Then flip +// the order of the bytes to create the return value. +// + +ushort MidiFile::readLittleEndian2Bytes(std::istream& input) { + uchar buffer[2] = {0}; + input.read((char*)buffer, 2); + if (input.eof()) { + std::cerr << "Error: unexpected end of file." << std::endl; + return 0; + } + return buffer[1] | (buffer[0] << 8); +} + + + +////////////////////////////// +// +// MidiFile::readByte -- Read one byte from input stream. Set +// fail status error if there was a problem (calling function +// has to check this status for an error after reading). +// + +uchar MidiFile::readByte(std::istream& input) { + uchar buffer[1] = {0}; + input.read((char*)buffer, 1); + if (input.eof()) { + std::cerr << "Error: unexpected end of file." << std::endl; + m_rwstatus = false; + return 0; + } + return buffer[0]; +} + + + +////////////////////////////// +// +// MidiFile::writeLittleEndianUShort -- +// + +std::ostream& MidiFile::writeLittleEndianUShort(std::ostream& out, ushort value) { + union { char bytes[2]; ushort us; } data; + data.us = value; + out << data.bytes[0]; + out << data.bytes[1]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeBigEndianUShort -- +// + +std::ostream& MidiFile::writeBigEndianUShort(std::ostream& out, ushort value) { + union { char bytes[2]; ushort us; } data; + data.us = value; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeLittleEndianShort -- +// + +std::ostream& MidiFile::writeLittleEndianShort(std::ostream& out, short value) { + union { char bytes[2]; short s; } data; + data.s = value; + out << data.bytes[0]; + out << data.bytes[1]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeBigEndianShort -- +// + +std::ostream& MidiFile::writeBigEndianShort(std::ostream& out, short value) { + union { char bytes[2]; short s; } data; + data.s = value; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeLittleEndianULong -- +// + +std::ostream& MidiFile::writeLittleEndianULong(std::ostream& out, ulong value) { + union { char bytes[4]; ulong ul; } data; + data.ul = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeBigEndianULong -- +// + +std::ostream& MidiFile::writeBigEndianULong(std::ostream& out, ulong value) { + union { char bytes[4]; long ul; } data; + data.ul = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeLittleEndianLong -- +// + +std::ostream& MidiFile::writeLittleEndianLong(std::ostream& out, long value) { + union { char bytes[4]; long l; } data; + data.l = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeBigEndianLong -- +// + +std::ostream& MidiFile::writeBigEndianLong(std::ostream& out, long value) { + union { char bytes[4]; long l; } data; + data.l = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; + +} + + + +////////////////////////////// +// +// MidiFile::writeBigEndianFloat -- +// + +std::ostream& MidiFile::writeBigEndianFloat(std::ostream& out, float value) { + union { char bytes[4]; float f; } data; + data.f = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeLittleEndianFloat -- +// + +std::ostream& MidiFile::writeLittleEndianFloat(std::ostream& out, float value) { + union { char bytes[4]; float f; } data; + data.f = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeBigEndianDouble -- +// + +std::ostream& MidiFile::writeBigEndianDouble(std::ostream& out, double value) { + union { char bytes[8]; double d; } data; + data.d = value; + out << data.bytes[7]; + out << data.bytes[6]; + out << data.bytes[5]; + out << data.bytes[4]; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::writeLittleEndianDouble -- +// + +std::ostream& MidiFile::writeLittleEndianDouble(std::ostream& out, double value) { + union { char bytes[8]; double d; } data; + data.d = value; + out << data.bytes[0]; + out << data.bytes[1]; + out << data.bytes[2]; + out << data.bytes[3]; + out << data.bytes[4]; + out << data.bytes[5]; + out << data.bytes[6]; + out << data.bytes[7]; + return out; +} + + + +////////////////////////////// +// +// MidiFile::base64Encode -- Encode a string as base64. +// + +std::string MidiFile::base64Encode(const std::string& input) { + std::string output; + output.reserve(((input.size()/3) + (input.size() % 3 > 0)) * 4); + int vala = 0; + int valb = -6; + for (unsigned char c : input) { + vala = (vala << 8) + c; + valb += 8; + while (valb >=0) { + output.push_back(MidiFile::encodeLookup[(vala >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) { + output.push_back(MidiFile::encodeLookup[((vala << 8) >> (valb + 8)) & 0x3F]); + } + while (output.size() % 4) { + output.push_back(MidiFile::encodeLookup.back()); + } + return output; +} + + + +////////////////////////////// +// +// MidiFile::base64Decode -- Decode a base64 string. +// + +std::string MidiFile::base64Decode(const std::string& input) { + // vector decodeLookup(256,-1); + // for (int i=0; i<64; i++) decodeLookup[encodeLookup[i]] = i; + + std::string output; + int vala = 0; + int valb = -8; + for (unsigned char c : input) { + if (c == '=') { + break; + } else if (MidiFile::decodeLookup[c] == -1) { + // Ignore whitespace, for example. + continue; + } + vala = (vala << 6) + MidiFile::decodeLookup[c]; + valb += 6; + if (valb >= 0) { + output.push_back(char((vala >> valb) & 0xFF)); + valb -= 8; + } + } + return output; +} + + + +} // end namespace smf + +/////////////////////////////////////////////////////////////////////////// +// +// external functions +// + +////////////////////////////// +// +// operator<< -- for printing an ASCII version of the MIDI file +// + +std::ostream& operator<<(std::ostream& out, smf::MidiFile& aMidiFile) { + aMidiFile.writeBinascWithComments(out); + return out; +} + + + diff --git a/midifile/src/MidiMessage.cpp b/midifile/src/MidiMessage.cpp new file mode 100644 index 0000000..4edf9af --- /dev/null +++ b/midifile/src/MidiMessage.cpp @@ -0,0 +1,1934 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat Feb 14 20:49:21 PST 2015 +// Last Modified: Sun Apr 15 11:11:05 PDT 2018 Added event removal system. +// Filename: midifile/src/MidiMessage.cpp +// Website: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: Storage for bytes of a MIDI message for Standard +// MIDI Files. +// + +#include "MidiMessage.h" + +#include +#include +#include + + +namespace smf { + +////////////////////////////// +// +// MidiMessage::MidiMessage -- Constructor. +// + +MidiMessage::MidiMessage(void) : vector() { + // do nothing +} + + +MidiMessage::MidiMessage(int command) : vector(1, (uchar)command) { + // do nothing +} + + +MidiMessage::MidiMessage(int command, int p1) : vector(2) { + (*this)[0] = (uchar)command; + (*this)[1] = (uchar)p1; +} + + +MidiMessage::MidiMessage(int command, int p1, int p2) : vector(3) { + (*this)[0] = (uchar)command; + (*this)[1] = (uchar)p1; + (*this)[2] = (uchar)p2; +} + + +MidiMessage::MidiMessage(const MidiMessage& message) : vector() { + (*this) = message; +} + + +MidiMessage::MidiMessage(const std::vector& message) : vector() { + setMessage(message); +} + + +MidiMessage::MidiMessage(const std::vector& message) : vector() { + setMessage(message); +} + + +MidiMessage::MidiMessage(const std::vector& message) : vector() { + setMessage(message); +} + + + +////////////////////////////// +// +// MidiMessage::~MidiMessage -- Deconstructor. +// + +MidiMessage::~MidiMessage() { + resize(0); +} + + + +////////////////////////////// +// +// MidiMessage::operator= -- +// + +MidiMessage& MidiMessage::operator=(const MidiMessage& message) { + if (this == &message) { + return *this; + } + std::vector::operator=(static_cast &>(message)); + return *this; +} + + +MidiMessage& MidiMessage::operator=(const std::vector& bytes) { + if (this == &bytes) { + return *this; + } + setMessage(bytes); + return *this; +} + + +MidiMessage& MidiMessage::operator=(const std::vector& bytes) { + setMessage(bytes); + return *this; +} + + +MidiMessage& MidiMessage::operator=(const std::vector& bytes) { + setMessage(bytes); + return *this; +} + + + +////////////////////////////// +// +// MidiMessage::setSize -- Change the size of the message byte list. +// If the size is increased, then the new bytes are not initialized +// to any specific values. +// + +void MidiMessage::setSize(int asize) { + this->resize(asize); +} + + + +////////////////////////////// +// +// MidiMessage::getSize -- Return the size of the MIDI message bytes. +// + +int MidiMessage::getSize(void) const { + return (int)this->size(); +} + + + +////////////////////////////// +// +// MidiMessage::setSizeToCommand -- Set the number of parameters if the +// command byte is set in the range from 0x80 to 0xef. Any newly +// added parameter bytes will be set to 0. Commands in the range +// of 0xF) should not use this function, and they will ignore +// modification by this command. +// + +int MidiMessage::setSizeToCommand(void) { + int osize = (int)this->size(); + if (osize < 1) { + return 0; + } + int command = getCommandNibble(); + if (command < 0) { + return 0; + } + int bytecount = 1; + switch (command) { + case 0x80: bytecount = 2; break; // Note Off + case 0x90: bytecount = 2; break; // Note On + case 0xA0: bytecount = 2; break; // Aftertouch + case 0xB0: bytecount = 2; break; // Continuous Controller + case 0xC0: bytecount = 1; break; // Patch Change + case 0xD0: bytecount = 1; break; // Channel Pressure + case 0xE0: bytecount = 2; break; // Pitch Bend + case 0xF0: + default: + return (int)size(); + } + if (bytecount + 1 < osize) { + resize(bytecount+1); + for (int i=osize; i= 64) { + return true; + } else { + return false; + } +} + + + +////////////////////////////// +// +// MidiMessage::isSustainOff -- Returns true if a sustain-pedal-off control message. +// Sustain-off is a value in the range from 0-63 for controller 64. +// + +bool MidiMessage::isSustainOff(void) const { + if (!isSustain()) { + return false; + } + if (getP2() < 64) { + return true; + } else { + return false; + } +} + + + +////////////////////////////// +// +// MidiMessage::isSoft -- Returns true if the MidiMessages is a soft pedal +// control event. Controller 67 is the sustain pedal for general MIDI. +// + +bool MidiMessage::isSoft(void) const { + if (!isController()) { + return false; + } + if (getP1() == 67) { + return true; + } else { + return false; + } +} + + + +////////////////////////////// +// +// MidiMessage::isSoftOn -- Returns true if a sustain-pedal-on control message. +// Soft-on is a value in the range from 64-127 for controller 67. +// + +bool MidiMessage::isSoftOn(void) const { + if (!isSoft()) { + return false; + } + if (getP2() >= 64) { + return true; + } else { + return false; + } +} + + + +////////////////////////////// +// +// MidiMessage::isSoftOff -- Returns true if a sustain-pedal-off control message. +// Soft-off is a value in the range from 0-63 for controller 67. +// + +bool MidiMessage::isSoftOff(void) const { + if (!isSoft()) { + return false; + } + if (getP2() < 64) { + return true; + } else { + return false; + } +} + + + +////////////////////////////// +// +// MidiMessage::isTimbre -- Returns true of a patch change message +// (command nibble 0xc0). +// + +bool MidiMessage::isTimbre(void) const { + if (((*this)[0] & 0xf0) != 0xc0) { + return false; + } else if (size() != 2) { + return false; + } else { + return true; + } +} + + +bool MidiMessage::isPatchChange(void) const { + return isTimbre(); +} + + + +////////////////////////////// +// +// MidiMessage::isPressure -- Returns true of a channel pressure message +// (command nibble 0xd0). +// + +bool MidiMessage::isPressure(void) const { + if (((*this)[0] & 0xf0) != 0xd0) { + return false; + } else if (size() != 2) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isPitchbend -- Returns true of a pitch-bend message +// (command nibble 0xe0). +// + +bool MidiMessage::isPitchbend(void) const { + if (((*this)[0] & 0xf0) != 0xe0) { + return false; + } else if (size() != 3) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isEmpty -- Returns true if size of data array is zero. +// + +bool MidiMessage::isEmpty(void) const { + return empty(); +} + + + +/////////////////////////////// +// +// MidiMessage::getMetaType -- returns the meta-message type for the +// MidiMessage. If the message is not a meta message, then returns +// -1. +// + +int MidiMessage::getMetaType(void) const { + if (!isMetaMessage()) { + return -1; + } else { + return (int)(*this)[1]; + } +} + + + +////////////////////////////// +// +// MidiMessage::isText -- Returns true if message is a meta +// message describing some text (meta message type 0x01). +// + +bool MidiMessage::isText(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x01) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isCopyright -- Returns true if message is a meta +// message describing a copyright notice (meta message type 0x02). +// Copyright messages should be at absolute tick position 0 +// (and be the first event in the track chunk as well), but this +// function does not check for those requirements. +// + +bool MidiMessage::isCopyright(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x02) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isTrackName -- Returns true if message is a meta +// message describing a track name (meta message type 0x03). +// + +bool MidiMessage::isTrackName(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x03) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isInstrumentName -- Returns true if message is a +// meta message describing an instrument name (for the track) +// (meta message type 0x04). +// + +bool MidiMessage::isInstrumentName(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x04) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isLyricText -- Returns true if message is a meta message +// describing some lyric text (for karakoke MIDI files) +// (meta message type 0x05). +// + +bool MidiMessage::isLyricText(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x05) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isMarkerText -- Returns true if message is a meta message +// describing a marker text (meta message type 0x06). +// + +bool MidiMessage::isMarkerText(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x06) { + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isTempo -- Returns true if message is a meta message +// describing tempo (meta message type 0x51). +// + +bool MidiMessage::isTempo(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x51) { + return false; + } else if (size() != 6) { + // Meta tempo message can only be 6 bytes long. + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isTimeSignature -- Returns true if message is +// a meta message describing a time signature (meta message +// type 0x58). +// + +bool MidiMessage::isTimeSignature(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x58) { + return false; + } else if (size() != 7) { + // Meta time signature message can only be 7 bytes long: + // FF 58 <32nds> + return false; + } else { + return true; + } +} + + + +////////////////////////////// +// +// MidiMessage::isKeySignature -- Returns true if message is +// a meta message describing a key signature (meta message +// type 0x59). +// + +bool MidiMessage::isKeySignature(void) const { + if (!isMetaMessage()) { + return false; + } else if ((*this)[1] != 0x59) { + return false; + } else if (size() != 5) { + // Meta key signature message can only be 5 bytes long: + // FF 59 + return false; + } else { + return true; + } +} + + +////////////////////////////// +// +// MidiMessage::isEndOfTrack -- Returns true if message is a meta message +// for end-of-track (meta message type 0x2f). +// + +bool MidiMessage::isEndOfTrack(void) const { + return getMetaType() == 0x2f ? 1 : 0; +} + + + +////////////////////////////// +// +// MidiMessage::getP0 -- Return index 1 byte, or -1 if it doesn't exist. +// + +int MidiMessage::getP0(void) const { + return size() < 1 ? -1 : (*this)[0]; +} + + + +////////////////////////////// +// +// MidiMessage::getP1 -- Return index 1 byte, or -1 if it doesn't exist. +// + +int MidiMessage::getP1(void) const { + return size() < 2 ? -1 : (*this)[1]; +} + + + +////////////////////////////// +// +// MidiMessage::getP2 -- Return index 2 byte, or -1 if it doesn't exist. +// + +int MidiMessage::getP2(void) const { + return size() < 3 ? -1 : (*this)[2]; +} + + + +////////////////////////////// +// +// MidiMessage::getP3 -- Return index 3 byte, or -1 if it doesn't exist. +// + +int MidiMessage::getP3(void) const { + return size() < 4 ? -1 : (*this)[3]; +} + + + +////////////////////////////// +// +// MidiMessage::getKeyNumber -- Return the key number (such as 60 for +// middle C). If the message does not have a note parameter, then +// return -1; if the key is invalid (above 127 in value), then +// limit to the range 0 to 127. +// + +int MidiMessage::getKeyNumber(void) const { + if (isNote() || isAftertouch()) { + int output = getP1(); + if (output < 0) { + return output; + } else { + return 0xff & output; + } + } else { + return -1; + } +} + + + +////////////////////////////// +// +// MidiMessage::getVelocity -- Return the key veolocity. If the message +// is not a note-on or a note-off, then return -1. If the value is +// out of the range 0-127, then chop off the high-bits. +// + +int MidiMessage::getVelocity(void) const { + if (isNote()) { + int output = getP2(); + if (output < 0) { + return output; + } else { + return 0xff & output; + } + } else { + return -1; + } +} + + + +////////////////////////////// +// +// MidiMessage::getControllerNumber -- Return the controller number (such as 1 +// for modulation wheel). If the message does not have a controller number +// parameter, then return -1. If the controller number is invalid (above 127 +// in value), then limit the range to to 0-127. +// + +int MidiMessage::getControllerNumber(void) const { + if (isController()) { + int output = getP1(); + if (output < 0) { + // -1 means no P1, although isController() is false in such a case. + return output; + } else { + return 0x7f & output; + } + } else { + return -1; + } +} + + + +////////////////////////////// +// +// MidiMessage::getControllerValue -- Return the controller value. If the +// message is not a control change message, then return -1. If the value is +// out of the range 0-127, then chop off the high-bits. +// + +int MidiMessage::getControllerValue(void) const { + if (isController()) { + int output = getP2(); + if (output < 0) { + // -1 means no P2, although isController() is false in such a case. + return output; + } else { + return 0x7f & output; + } + } else { + return -1; + } +} + + + +////////////////////////////// +// +// MidiMessage::setP0 -- Set the command byte. +// If the MidiMessage is too short, add extra spaces to +// allow for P0. The value should be in the range from +// 128 to 255, but this function will not babysit you. +// + +void MidiMessage::setP0(int value) { + if (getSize() < 1) { + resize(1); + } + (*this)[0] = static_cast(value); +} + + + +////////////////////////////// +// +// MidiMessage::setP1 -- Set the first parameter value. +// If the MidiMessage is too short, add extra spaces to +// allow for P1. The command byte will be undefined if +// it was added. The value should be in the range from +// 0 to 127, but this function will not babysit you. +// + +void MidiMessage::setP1(int value) { + if (getSize() < 2) { + resize(2); + } + (*this)[1] = static_cast(value); +} + + + +////////////////////////////// +// +// MidiMessage::setP2 -- Set the second paramter value. +// If the MidiMessage is too short, add extra spaces +// to allow for P2. The command byte and/or the P1 value +// will be undefined if extra space needs to be added and +// those slots are created. The value should be in the range +// from 0 to 127, but this function will not babysit you. +// + +void MidiMessage::setP2(int value) { + if (getSize() < 3) { + resize(3); + } + (*this)[2] = static_cast(value); +} + + + +////////////////////////////// +// +// MidiMessage::setP3 -- Set the third paramter value. +// If the MidiMessage is too short, add extra spaces +// to allow for P3. The command byte and/or the P1/P2 values +// will be undefined if extra space needs to be added and +// those slots are created. The value should be in the range +// from 0 to 127, but this function will not babysit you. +// + +void MidiMessage::setP3(int value) { + if (getSize() < 4) { + resize(4); + } + (*this)[3] = static_cast(value); +} + + + +////////////////////////////// +// +// MidiMessage::setKeyNumber -- Set the note on/off key number (or +// aftertouch key). Ignore if not note or aftertouch message. +// Limits the input value to the range from 0 to 127. +// + +void MidiMessage::setKeyNumber(int value) { + if (isNote() || isAftertouch()) { + setP1(value & 0xff); + } else { + // don't do anything since this is not a note-related message. + } +} + + + +////////////////////////////// +// +// MidiMessage::setVelocity -- Set the note on/off velocity; ignore +// if note a note message. Limits the input value to the range +// from 0 to 127. +// + +void MidiMessage::setVelocity(int value) { + if (isNote()) { + setP2(value & 0xff); + } else { + // don't do anything since this is not a note-related message. + } +} + + + +////////////////////////////// +// +// MidiMessage::getCommandNibble -- Returns the top 4 bits of the (*this)[0] +// entry, or -1 if there is not (*this)[0]. +// + +int MidiMessage::getCommandNibble(void) const { + if (size() < 1) { + return -1; + } else { + return (*this)[0] & 0xf0; + } +} + + + +////////////////////////////// +// +// MidiMessage::getCommandByte -- Return the command byte or -1 if not +// allocated. +// + +int MidiMessage::getCommandByte(void) const { + if (size() < 1) { + return -1; + } else { + return (*this)[0]; + } +} + + + +////////////////////////////// +// +// MidiMessage::getChannelNibble -- Returns the bottom 4 bites of the +// (*this)[0] entry, or -1 if there is not (*this)[0]. Should be refined +// to return -1 if the top nibble is 0xf0, since those commands are +// not channel specific. +// + +int MidiMessage::getChannelNibble(void) const { + if (size() < 1) { + return -1; + } else { + return (*this)[0] & 0x0f; + } +} + + +int MidiMessage::getChannel(void) const { + return getChannelNibble(); +} + + + +////////////////////////////// +// +// MidiMessage::setCommandByte -- +// + +void MidiMessage::setCommandByte(int value) { + if (size() < 1) { + resize(1); + } else { + (*this)[0] = (uchar)(value & 0xff); + } +} + +void MidiMessage::setCommand(int value) { + setCommandByte(value); +} + + + +////////////////////////////// +// +// MidiMessage::setCommand -- Set the command byte and parameter bytes +// for a MidiMessage. The size of the message will be adjusted to +// the number of input parameters. +// + +void MidiMessage::setCommand(int value, int p1) { + this->resize(2); + (*this)[0] = (uchar)value; + (*this)[1] = (uchar)p1; +} + + +void MidiMessage::setCommand(int value, int p1, int p2) { + this->resize(3); + (*this)[0] = (uchar)value; + (*this)[1] = (uchar)p1; + (*this)[2] = (uchar)p2; +} + + + +////////////////////////////// +// +// MidiMessage::setCommandNibble -- +// + +void MidiMessage::setCommandNibble(int value) { + if (this->size() < 1) { + this->resize(1); + } + if (value <= 0x0f) { + (*this)[0] = ((*this)[0] & 0x0f) | ((uchar)((value << 4) & 0xf0)); + } else { + (*this)[0] = ((*this)[0] & 0x0f) | ((uchar)(value & 0xf0)); + } +} + + + + +////////////////////////////// +// +// MidiMessage::setChannelNibble -- +// + +void MidiMessage::setChannelNibble(int value) { + if (this->size() < 1) { + this->resize(1); + } + (*this)[0] = ((*this)[0] & 0xf0) | ((uchar)(value & 0x0f)); +} + + +void MidiMessage::setChannel(int value) { + setChannelNibble(value); +} + + + +////////////////////////////// +// +// MidiMessage::setParameters -- Set the second and optionally the +// third MIDI byte of a MIDI message. The command byte will not +// be altered, and will be set to 0 if it currently does not exist. +// + +void MidiMessage::setParameters(int p1) { + int oldsize = (int)size(); + resize(2); + (*this)[1] = (uchar)p1; + if (oldsize < 1) { + (*this)[0] = 0; + } +} + + +void MidiMessage::setParameters(int p1, int p2) { + int oldsize = (int)size(); + resize(3); + (*this)[1] = (uchar)p1; + (*this)[2] = (uchar)p2; + if (oldsize < 1) { + (*this)[0] = 0; + } +} + + +////////////////////////////// +// +// MidiMessage::setMessage -- Set the contents of MIDI bytes to the +// input list of bytes. +// + +void MidiMessage::setMessage(const std::vector& message) { + this->resize(message.size()); + for (int i=0; i<(int)this->size(); i++) { + (*this)[i] = message[i]; + } +} + + +void MidiMessage::setMessage(const std::vector& message) { + resize(message.size()); + for (int i=0; i<(int)size(); i++) { + (*this)[i] = (uchar)message[i]; + } +} + + +void MidiMessage::setMessage(const std::vector& message) { + resize(message.size()); + for (int i=0; i<(int)size(); i++) { + (*this)[i] = (uchar)message[i]; + } +} + + + +////////////////////////////// +// +// MidiMessage::setSpelling -- Encode a MidiPlus accidental state for a note. +// For example, if a note's key number is 60, the enharmonic pitch name +// could be any of these possibilities: +// C, B-sharp, D-double-flat +// MIDI note 60 is ambiguous as to which of these names are intended, +// so MIDIPlus allows these mappings to be preserved for later recovery. +// See Chapter 5 (pp. 99-104) of Beyond MIDI (1997). +// +// The first parameter is the diatonic pitch number (or pitch class +// if the octave is set to 0): +// octave * 7 + 0 = C pitches +// octave * 7 + 1 = D pitches +// octave * 7 + 2 = E pitches +// octave * 7 + 3 = F pitches +// octave * 7 + 4 = G pitches +// octave * 7 + 5 = A pitches +// octave * 7 + 6 = B pitches +// +// The second parameter is the semitone alteration (accidental). +// 0 = natural state, 1 = sharp, 2 = double sharp, -1 = flat, +// -2 = double flat. +// +// Only note-on messages can be processed (other messages will be +// silently ignored). +// + +void MidiMessage::setSpelling(int base7, int accidental) { + if (!isNoteOn()) { + return; + } + // The bottom two bits of the attack velocity are used for the + // spelling, so need to make sure the velocity will not accidentally + // be set to zero (and make the note-on a note-off). + if (getVelocity() < 4) { + setVelocity(4); + } + int dpc = base7 % 7; + uchar spelling = 0; + + // Table 5.1, page 101 in Beyond MIDI (1997) + // http://beyondmidi.ccarh.org/beyondmidi-600dpi.pdf + switch (dpc) { + + case 0: + switch (accidental) { + case -2: spelling = 1; break; // Cbb + case -1: spelling = 1; break; // Cb + case 0: spelling = 2; break; // C + case +1: spelling = 2; break; // C# + case +2: spelling = 3; break; // C## + } + break; + + case 1: + switch (accidental) { + case -2: spelling = 1; break; // Dbb + case -1: spelling = 1; break; // Db + case 0: spelling = 2; break; // D + case +1: spelling = 3; break; // D# + case +2: spelling = 3; break; // D## + } + break; + + case 2: + switch (accidental) { + case -2: spelling = 1; break; // Ebb + case -1: spelling = 2; break; // Eb + case 0: spelling = 2; break; // E + case +1: spelling = 3; break; // E# + case +2: spelling = 3; break; // E## + } + break; + + case 3: + switch (accidental) { + case -2: spelling = 1; break; // Fbb + case -1: spelling = 1; break; // Fb + case 0: spelling = 2; break; // F + case +1: spelling = 2; break; // F# + case +2: spelling = 3; break; // F## + case +3: spelling = 3; break; // F### + } + break; + + case 4: + switch (accidental) { + case -2: spelling = 1; break; // Gbb + case -1: spelling = 1; break; // Gb + case 0: spelling = 2; break; // G + case +1: spelling = 2; break; // G# + case +2: spelling = 3; break; // G## + } + break; + + case 5: + switch (accidental) { + case -2: spelling = 1; break; // Abb + case -1: spelling = 1; break; // Ab + case 0: spelling = 2; break; // A + case +1: spelling = 3; break; // A# + case +2: spelling = 3; break; // A## + } + break; + + case 6: + switch (accidental) { + case -2: spelling = 1; break; // Bbb + case -1: spelling = 2; break; // Bb + case 0: spelling = 2; break; // B + case +1: spelling = 3; break; // B# + case +2: spelling = 3; break; // B## + } + break; + + } + + uchar vel = static_cast(getVelocity()); + // suppress any previous content in the first two bits: + vel = vel & 0xFC; + // insert the spelling code: + vel = vel | spelling; + setVelocity(vel); +} + + + +////////////////////////////// +// +// MidiMessage::getSpelling -- Return the diatonic pitch class and accidental +// for a note-on's key number. The MIDI file must be encoded with MIDIPlus +// pitch spelling codes for this function to return valid data; otherwise, +// it will return a neutral fixed spelling for each MIDI key. +// +// The first parameter will be filled in with the base-7 diatonic pitch: +// pc + octave * 7 +// where pc is the numbers 0 through 6 representing the pitch classes +// C through B, the octave is MIDI octave (not the scientific pitch +// octave which is one less than the MIDI ocatave, such as C4 = middle C). +// The second number is the accidental for the base-7 pitch. +// + +void MidiMessage::getSpelling(int& base7, int& accidental) { + if (!isNoteOn()) { + return; + } + base7 = -123456; + accidental = 123456; + int base12 = getKeyNumber(); + int octave = base12 / 12; + int base12pc = base12 - octave * 12; + int base7pc = 0; + int spelling = 0x03 & getVelocity(); + + // Table 5.1, page 101 in Beyond MIDI (1997) + // http://beyondmidi.ccarh.org/beyondmidi-600dpi.pdf + switch (base12pc) { + + case 0: + switch (spelling) { + case 1: base7pc = 1; accidental = -2; break; // Dbb + case 0: case 2: base7pc = 0; accidental = 0; break; // C + case 3: base7pc = 6; accidental = +1; octave--; break; // B# + } + break; + + case 1: + switch (spelling) { + case 1: base7pc = 1; accidental = -1; break; // Db + case 0: case 2: base7pc = 0; accidental = +1; break; // C# + case 3: base7pc = 6; accidental = +2; octave--; break; // B## + } + break; + + case 2: + switch (spelling) { + case 1: base7pc = 2; accidental = -2; break; // Ebb + case 0: case 2: base7pc = 1; accidental = 0; break; // D + case 3: base7pc = 0; accidental = +2; break; // C## + } + break; + + case 3: + switch (spelling) { + case 1: base7pc = 3; accidental = -2; break; // Fbb + case 0: case 2: base7pc = 2; accidental = -1; break; // Eb + case 3: base7pc = 1; accidental = +1; break; // D# + } + break; + + case 4: + switch (spelling) { + case 1: base7pc = 3; accidental = -1; break; // Fb + case 0: case 2: base7pc = 2; accidental = 0; break; // E + case 3: base7pc = 1; accidental = +2; break; // D## + } + break; + + case 5: + switch (spelling) { + case 1: base7pc = 4; accidental = -2; break; // Gbb + case 0: case 2: base7pc = 3; accidental = 0; break; // F + case 3: base7pc = 2; accidental = +1; break; // E# + } + break; + + case 6: + switch (spelling) { + case 1: base7pc = 4; accidental = -1; break; // Gb + case 0: case 2: base7pc = 3; accidental = +1; break; // F# + case 3: base7pc = 2; accidental = +2; break; // E## + } + break; + + case 7: + switch (spelling) { + case 1: base7pc = 5; accidental = -2; break; // Abb + case 0: case 2: base7pc = 4; accidental = 0; break; // G + case 3: base7pc = 3; accidental = +2; break; // F## + } + break; + + case 8: + switch (spelling) { + case 1: base7pc = 5; accidental = -1; break; // Ab + case 0: case 2: base7pc = 4; accidental = +1; break; // G# + case 3: base7pc = 3; accidental = +3; break; // F### + } + break; + + case 9: + switch (spelling) { + case 1: base7pc = 6; accidental = -2; break; // Bbb + case 0: case 2: base7pc = 5; accidental = 0; break; // A + case 3: base7pc = 4; accidental = +2; break; // G## + } + break; + + case 10: + switch (spelling) { + case 1: base7pc = 0; accidental = -2; octave++; break; // Cbb + case 0: case 2: base7pc = 6; accidental = -1; break; // Bb + case 3: base7pc = 5; accidental = +1; break; // A# + } + break; + + case 11: + switch (spelling) { + case 1: base7pc = 0; accidental = -1; octave++; break; // Cb + case 0: case 2: base7pc = 6; accidental = 0; break; // B + case 3: base7pc = 5; accidental = +2; break; // A## + } + break; + + } + + base7 = base7pc + 7 * octave; +} + + + +////////////////////////////// +// +// MidiMessage::getMetaContent -- Returns the bytes of the meta +// message after the length (which is a variable-length-value). +// + +std::string MidiMessage::getMetaContent(void) { + std::string output; + if (!isMetaMessage()) { + return output; + } + int start = 3; + if (operator[](2) > 0x7f) { + start++; + if (operator[](3) > 0x7f) { + start++; + if (operator[](4) > 0x7f) { + start++; + if (operator[](5) > 0x7f) { + start++; + // maximum of 5 bytes in VLV, so last must be < 0x80 + } + } + } + } + output.reserve(this->size()); + for (int i=start; i<(int)this->size(); i++) { + output.push_back(operator[](i)); + } + return output; +} + + + +////////////////////////////// +// +// MidiMessage::setMetaContent - Set the content of a meta-message. This +// function handles the size of the message starting at byte 3 in the +// message, and it does not alter the meta message type. The message +// must be a meta-message before calling this function and be assigne +// a meta-message type. +// + +void MidiMessage::setMetaContent(const std::string& content) { + if (this->size() < 2) { + // invalid message, so ignore request + return; + } + if (operator[](0) != 0xFF) { + // not a meta message, so ignore request + return; + } + this->resize(2); + + // add the size of the meta message data (VLV) + int dsize = (int)content.size(); + if (dsize < 128) { + push_back((uchar)dsize); + } else { + // calculate VLV bytes and insert into message + uchar byte1 = dsize & 0x7f; + uchar byte2 = (dsize >> 7) & 0x7f; + uchar byte3 = (dsize >> 14) & 0x7f; + uchar byte4 = (dsize >> 21) & 0x7f; + uchar byte5 = (dsize >> 28) & 0x7f; + if (byte5) { + byte4 |= 0x80; + } + if (byte4) { + byte4 |= 0x80; + byte3 |= 0x80; + } + if (byte3) { + byte3 |= 0x80; + byte2 |= 0x80; + } + if (byte2) { + byte2 |= 0x80; + } + if (byte5) { push_back(byte5); } + if (byte4) { push_back(byte4); } + if (byte3) { push_back(byte3); } + if (byte2) { push_back(byte2); } + push_back(byte1); + } + std::copy(content.begin(), content.end(), std::back_inserter(*this)); +} + + + +////////////////////////////// +// +// MidiMessage::setMetaTempo -- Input tempo is in quarter notes per minute +// (meta message #0x51). +// + +void MidiMessage::setMetaTempo(double tempo) { + int microseconds = (int)(60.0 / tempo * 1000000.0 + 0.5); + setTempoMicroseconds(microseconds); +} + + + +////////////////////////////// +// +// MidiMessage::setTempo -- Alias for MidiMessage::setMetaTempo(). +// + +void MidiMessage::setTempo(double tempo) { + setMetaTempo(tempo); +} + + + +////////////////////////////// +// +// MidiMessage::setTempoMicroseconds -- Set the tempo in terms +// of microseconds per quarter note. +// + +void MidiMessage::setTempoMicroseconds(int microseconds) { + resize(6); + (*this)[0] = 0xff; + (*this)[1] = 0x51; + (*this)[2] = 3; + (*this)[3] = (microseconds >> 16) & 0xff; + (*this)[4] = (microseconds >> 8) & 0xff; + (*this)[5] = (microseconds >> 0) & 0xff; +} + + + +////////////////////////////// +// +// MidiMessage::makeTimeSignature -- create a time signature meta message +// (meta #0x58). The "bottom" parameter should be a power of two; +// otherwise, it will be forced to be the next highest power of two, +// as MIDI time signatures must have a power of two in the denominator. +// +// Default values: +// clocksPerClick == 24 (quarter note) +// num32ndsPerQuarter == 8 (8 32nds per quarter note) +// +// Time signature of 4/4 would be: +// top = 4 +// bottom = 4 (converted to 2 in the MIDI file for 2nd power of 2). +// clocksPerClick = 24 (2 eighth notes based on num32ndsPerQuarter) +// num32ndsPerQuarter = 8 +// +// Time signature of 6/8 would be: +// top = 6 +// bottom = 8 (converted to 3 in the MIDI file for 3rd power of 2). +// clocksPerClick = 36 (3 eighth notes based on num32ndsPerQuarter) +// num32ndsPerQuarter = 8 +// + +void MidiMessage::makeTimeSignature(int top, int bottom, int clocksPerClick, + int num32ndsPerQuarter) { + int base2 = 0; + while (bottom >>= 1) base2++; + resize(7); + (*this)[0] = 0xff; + (*this)[1] = 0x58; + (*this)[2] = 4; + (*this)[3] = 0xff & top; + (*this)[4] = 0xff & base2; + (*this)[5] = 0xff & clocksPerClick; + (*this)[6] = 0xff & num32ndsPerQuarter; +} + + + +/////////////////////////////////////////////////////////////////////////// +// +// make functions to create various MIDI message -- +// + + +////////////////////////////// +// +// MidiMessage::makeNoteOn -- create a note-on message. +// +// default value: channel = 0 +// +// Note: The channel parameter used to be last, but makes more sense to +// have it first... +// + +void MidiMessage::makeNoteOn(int channel, int key, int velocity) { + resize(3); + (*this)[0] = 0x90 | (0x0f & channel); + (*this)[1] = key & 0x7f; + (*this)[2] = velocity & 0x7f; +} + + + +////////////////////////////// +// +// MidiMessage::makeNoteOff -- create a note-off message. If no +// parameters are given, the current contents is presumed to be a +// note-on message, which will be converted into a note-off message. +// +// default value: channel = 0 +// +// Note: The channel parameter used to be last, but makes more sense to +// have it first... +// + + +void MidiMessage::makeNoteOff(int channel, int key, int velocity) { + resize(3); + (*this)[0] = 0x80 | (0x0f & channel); + (*this)[1] = key & 0x7f; + (*this)[2] = velocity & 0x7f; +} + + +void MidiMessage::makeNoteOff(int channel, int key) { + resize(3); + (*this)[0] = 0x90 | (0x0f & channel); + (*this)[1] = key & 0x7f; + (*this)[2] = 0x00; +} + +// +// MidiMessage::makeNoteOff(void) -- create a 0x90 note message with +// The key and velocity set to 0. +// + +void MidiMessage::makeNoteOff(void) { + if (!isNoteOn()) { + resize(3); + (*this)[0] = 0x90; + (*this)[1] = 0; + (*this)[2] = 0; + } else { + (*this)[2] = 0; + } +} + + + +///////////////////////////// +// +// MidiMessage::makePatchChange -- Create a patch-change message. +// + +void MidiMessage::makePatchChange(int channel, int patchnum) { + resize(0); + push_back(0xc0 | (0x0f & channel)); + push_back(0x7f & patchnum); +} + +// +// MidiMessage::makeTimbre -- alias for MidiMessage::makePatchChange(). +// + +void MidiMessage::makeTimbre(int channel, int patchnum) { + makePatchChange(channel, patchnum); +} + + +///////////////////////////// +// +// MidiMessage::makeController -- Create a controller message. +// + +void MidiMessage::makeController(int channel, int num, int value) { + resize(0); + push_back(0xb0 | (0x0f & channel)); + push_back(0x7f & num); + push_back(0x7f & value); +} + + + +///////////////////////////// +// +// MidiMessage::makeSustain -- Create a sustain pedal message. +// Value in 0-63 range is a sustain off. Value in the +// 64-127 value is a sustain on. +// + +void MidiMessage::makeSustain(int channel, int value) { + makeController(channel, 64, value); +} + +// +// MidiMessage::makeSustain -- Alias for MidiMessage::makeSustain(). +// + +void MidiMessage::makeSustainPedal(int channel, int value) { + makeSustain(channel, value); +} + + + +///////////////////////////// +// +// MidiMessage::makeSustainOn -- Create sustain-on controller message. +// + +void MidiMessage::makeSustainOn(int channel) { + makeController(channel, 64, 127); +} + +// +// MidiMessage::makeSustainPedalOn -- Alias for MidiMessage::makeSustainOn(). +// + +void MidiMessage::makeSustainPedalOn(int channel) { + makeSustainOn(channel); +} + + + +///////////////////////////// +// +// MidiMessage::makeSustainOff -- Create a sustain-off controller message. +// + +void MidiMessage::makeSustainOff(int channel) { + makeController(channel, 64, 0); +} + +// +// MidiMessage::makeSustainPedalOff -- Alias for MidiMessage::makeSustainOff(). +// + +void MidiMessage::makeSustainPedalOff(int channel) { + makeSustainOff(channel); +} + + + +////////////////////////////// +// +// MidiMessage::makeMetaMessage -- Create a Meta event with the +// given text string as the parameter. The length of the string should +// is a VLV. If the length is larger than 127 byte, then the length +// will contain more than one byte. +// + +void MidiMessage::makeMetaMessage(int mnum, const std::string& data) { + resize(0); + push_back(0xff); + push_back(mnum & 0x7f); // max meta-message number is 0x7f. + setMetaContent(data); +} + + + +////////////////////////////// +// +// MidiMessage::makeText -- Create a metaevent text message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeText(const std::string& text) { + makeMetaMessage(0x01, text); +} + + + +////////////////////////////// +// +// MidiMessage::makeCopyright -- Create a metaevent copyright message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeCopyright(const std::string& text) { + makeMetaMessage(0x02, text); +} + + + +////////////////////////////// +// +// MidiMessage::makeTrackName -- Create a metaevent track name message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeTrackName(const std::string& name) { + makeMetaMessage(0x03, name); +} + + + +////////////////////////////// +// +// MidiMessage::makeTrackName -- Create a metaevent instrument name message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeInstrumentName(const std::string& name) { + makeMetaMessage(0x04, name); +} + + + +////////////////////////////// +// +// MidiMessage::makeLyric -- Create a metaevent lyrics/text message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeLyric(const std::string& text) { + makeMetaMessage(0x05, text); +} + + + +////////////////////////////// +// +// MidiMessage::makeMarker -- Create a metaevent marker message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeMarker(const std::string& text) { + makeMetaMessage(0x06, text); +} + + + +////////////////////////////// +// +// MidiMessage::makeCue -- Create a metaevent cue-point message. +// This is not a real MIDI message, but rather a pretend message for use +// within Standard MIDI Files. +// + +void MidiMessage::makeCue(const std::string& text) { + makeMetaMessage(0x07, text); +} + + +} // end namespace smf + + + diff --git a/midifile/src/Options.cpp b/midifile/src/Options.cpp new file mode 100644 index 0000000..2988b1d --- /dev/null +++ b/midifile/src/Options.cpp @@ -0,0 +1,1219 @@ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sun Apr 5 13:07:18 PDT 1998 +// Last Modified: Mon Jan 18 18:25:23 PST 2021 Some cleanup +// Filename: midifile/src/Options.cpp +// Web Address: http://midifile.sapp.org +// Syntax: C++11 +// vim: ts=3 noexpandtab +// +// Description: Interface to command-line options. +// + +#include "Options.h" + +#include +#include +#include +#include +#include +#include + + +namespace smf { + +/////////////////////////////////////////////////////////////////////////// +// +// Option_register class function definitions. +// + + +////////////////////////////// +// +// Option_register::Option_register -- Constructor. +// + +Option_register::Option_register(void) { + setType(OPTION_TYPE_string); + m_modifiedQ = false; +} + + +Option_register::Option_register(const std::string& aDefinition, char aType, + const std::string& aDefaultOption) { + m_modifiedQ = false; + setType(aType); + setDefinition(aDefinition); + setDefault(aDefaultOption); +} + +Option_register::Option_register(const std::string& aDefinition, char aType, + const std::string& aDefaultOption, const std::string& aModifiedOption) { + setType(aType); + setDefinition(aDefinition); + setDefault(aDefaultOption); + setModified(aModifiedOption); +} + + + +////////////////////////////// +// +// Option_register::~Option_register -- Destructor. +// + +Option_register::~Option_register() { + // do nothing +} + + + +////////////////////////////// +// +// Option_register::clearModified -- Clear any changes in the option value. +// + +void Option_register::clearModified(void) { + m_modifiedOption.clear(); + m_modifiedQ = false; +} + + + +////////////////////////////// +// +// Option_register::getDefinition -- Returns the initial definition. +// string used to define this entry. +// + +const std::string& Option_register::getDefinition(void) { + return m_definition; +} + + + +////////////////////////////// +// +// Option_register::getDescription -- Return the textual description +// of the entry. +// + +const std::string& Option_register::getDescription(void) { + return m_description; +} + + + +////////////////////////////// +// +// Option_register::getDefault -- Return the default value string. +// + +const std::string& Option_register::getDefault(void) { + return m_defaultOption; +} + + + +////////////////////////////// +// +// Option_register::getModified -- Return the modified option string. +// + +const std::string& Option_register::getModified(void) { + return m_modifiedOption; +} + + + +////////////////////////////// +// +// Option_register::isModified -- Return true if option has been +// set on the command-line. +// + +bool Option_register::isModified(void) { + return m_modifiedQ; +} + + + +////////////////////////////// +// +// Option_register::getType -- Return the data type of the option. +// + +char Option_register::getType(void) { + return m_type; +} + + + +////////////////////////////// +// +// Option_register::getOption -- return the modified option +// or the default option if no modified option. +// + +const std::string& Option_register::getOption(void) { + if (isModified()) { + return getModified(); + } else { + return getDefault(); + } +} + + + +////////////////////////////// +// +// Option_register::reset -- deallocate space for all +// strings in object. (but default string is set to "") +// + +void Option_register::reset(void) { + m_type = OPTION_TYPE_string; + m_definition.clear(); + m_defaultOption.clear(); + m_modifiedOption.clear(); +} + + + +////////////////////////////// +// +// Option_register::setDefault -- Set the default value. +// + +void Option_register::setDefault(const std::string& aString) { + m_defaultOption = aString; +} + + + +////////////////////////////// +// +// Option_register::setDefinition -- Set the option definition. +// + +void Option_register::setDefinition(const std::string& aString) { + m_definition = aString; +} + + + +////////////////////////////// +// +// Option_register::setDescription -- Set the textual description. +// + +void Option_register::setDescription(const std::string& aString) { + m_description = aString; +} + + + +////////////////////////////// +// +// Option_register::setModified -- Set the modified value. +// + +void Option_register::setModified(const std::string& aString) { + m_modifiedOption = aString; + m_modifiedQ = true; +} + + + +////////////////////////////// +// +// Option_register::setType -- Set the option type. +// + +void Option_register::setType(char aType) { + m_type = aType; +} + + + +////////////////////////////// +// +// Option_register::print -- Print the state of the option register entry. +// Useful for debugging. +// + +std::ostream& Option_register::print(std::ostream& out) { + out << "definition:\t" << m_definition << std::endl; + out << "description:\t" << m_description << std::endl; + out << "defaultOption:\t" << m_defaultOption << std::endl; + out << "modifiedOption:\t" << m_modifiedOption << std::endl; + out << "modifiedQ:\t\t" << m_modifiedQ << std::endl; + out << "type:\t\t" << m_type << std::endl; + return out; +}; + + + + +/////////////////////////////////////////////////////////////////////////// +// +// Options class function definitions. +// + +////////////////////////////// +// +// Options::Options -- Constructor. +// + +Options::Options(void) { + m_oargc = -1; + m_suppressQ = 0; + m_processedQ = 0; + m_optionsArgument = 0; + m_options_error_check = 1; + m_optionFlag = '-'; + + m_extraArgv.reserve(100); + m_extraArgv_strings.reserve(100); +} + + +Options::Options(int argc, char** argv) { + m_oargc = -1; + m_suppressQ = 0; + m_processedQ = 0; + m_optionsArgument = 0; + m_options_error_check = 1; + m_optionFlag = '-'; + + m_extraArgv.reserve(100); + m_extraArgv_strings.reserve(100); + + setOptions(argc, argv); +} + + + +////////////////////////////// +// +// Options::~Options -- Destructor. +// + +Options::~Options() { + reset(); +} + + + +////////////////////////////// +// +// Options::argc -- returns the argument count as input from main(). +// + +int Options::argc(void) const { + return m_oargc; +} + + + +////////////////////////////// +// +// Options::argv -- returns the arguments strings as input from main(). +// + +const std::vector& Options::argv(void) const { + return m_oargv; +} + + + +////////////////////////////// +// +// Options::define -- store an option definition in the register. Option +// definitions have this sructure: +// option-name|alias-name1|alias-name2=option-type:option-default +// option-name :: name of the option (one or more character, not including +// spaces or equal signs. +// alias-name :: equivalent name(s) of the option. +// option-type :: single charater indicating the option data type. +// option-default :: default value for option if no given on the command-line. +// + +int Options::define(const std::string& aDefinition) { + Option_register* definitionEntry = NULL; + + // Error if definition string doesn't contain an equals sign + auto location = aDefinition.find("="); + if (location == std::string::npos) { + std::cerr << "Error: no \"=\" in option definition: " << aDefinition << std::endl; + exit(1); + } + + std::string aliases = aDefinition.substr(0, location); + std::string rest = aDefinition.substr(location+1); + std::string otype = rest; + std::string ovalue = ""; + + location = rest.find(":"); + if (location != std::string::npos) { + otype = rest.substr(0, location); + ovalue = rest.substr(location+1); + } + + // Remove anyspaces in the option type field + otype.erase(remove_if(otype.begin(), otype.end(), ::isspace), otype.end()); + + // Option types are only a single charater (b, i, d, c or s) + if (otype.size() != 1) { + std::cerr << "Error: option type is invalid: " << otype + << " in option definition: " << aDefinition << std::endl; + exit(1); + } + + // Check to make sure that the type is known + if (otype[0] != OPTION_TYPE_string && + otype[0] != OPTION_TYPE_int && + otype[0] != OPTION_TYPE_float && + otype[0] != OPTION_TYPE_double && + otype[0] != OPTION_TYPE_boolean && + otype[0] != OPTION_TYPE_char ) { + std::cerr << "Error: unknown option type \'" << otype[0] + << "\' in defintion: " << aDefinition << std::endl; + exit(1); + } + + // Set up space for a option entry in the register + definitionEntry = new Option_register(aDefinition, otype[0], ovalue); + + int definitionIndex = (int)m_optionRegister.size(); + + // Store option aliases + std::string optionName; + unsigned int i; + aliases += '|'; + for (i=0; i 0) { + m_optionList[optionName] = definitionIndex; + } + optionName.clear(); + } else { + optionName += aliases[i]; + } + } + + // Store definition in register and return its indexed location. + // This location will be used to link option aliases to the main + // command name. + m_optionRegister.push_back(definitionEntry); + return definitionIndex; +} + + +int Options::define(const std::string& aDefinition, + const std::string& aDescription) { + int index = define(aDefinition); + m_optionRegister[index]->setDescription(aDescription); + return index; +} + + + +////////////////////////////// +// +// Options::isDefined -- Return true if option is present in register. +// + +bool Options::isDefined(const std::string& name) { + return m_optionList.find(name) == m_optionList.end() ? false : true; +} + + + +////////////////////////////// +// +// Options::getArg -- returns the specified argument. +// argurment 0 is the command name. +// + +const std::string& Options::getArg(int index) { + if (index < 0 || index >= (int)m_argument.size()) { + std::cerr << "Error: m_argument " << index << " does not exist." << std::endl; + exit(1); + } + return m_argument[index]; +} + +// Alias: + +const std::string& Options::getArgument(int index) { + return getArg(index); +} + + + +////////////////////////////// +// +// Options::getArgCount -- number of arguments on command line. +// does not count the options or the command name. +// + +int Options::getArgCount(void) { + return ((int)m_argument.size()) - 1; +} + +// Alias: + +int Options::getArgumentCount(void) { + return getArgCount(); +} + + + +////////////////////////////// +// +// Options::getArgList -- return a string vector of the arguments +// after the options have been parsed out of it. +// + +const std::vector& Options::getArgList(void) { + return m_argument; +} + +// Alias: + +const std::vector& Options::getArgumentList(void) { + return getArgList(); +} + + + +////////////////////////////// +// +// Options::getBoolean -- returns true if the option was +// used on the command line. +// + +bool Options::getBoolean(const std::string& optionName) { + int index = getRegIndex(optionName); + if (index < 0) { + return 0; + } + return m_optionRegister[index]->isModified(); +} + + + +////////////////////////////// +// +// Options::getCommand -- returns argv[0] (the first string +// in the original argv list. +// + +std::string Options::getCommand(void) { + if (m_argument.size() == 0) { + return ""; + } else { + return m_argument[0]; + } +} + + + +////////////////////////////// +// +// Options::getCommandLine -- returns a string which contains the +// command-line call to the program. Deal with spaces in arguments... +// + +const std::string& Options::getCommandLine(void) { + if (m_commandString.size()) { + return m_commandString; + } + + m_commandString = m_oargv[0]; + + int i; + for (i=1; isecond]->getDefinition(); + } +} + + + +////////////////////////////// +// +// Options::getDouble -- returns the double float associated +// with the given option. Returns 0 if there is no +// number associated with the option. +// + +double Options::getDouble(const std::string& optionName) { + return strtod(getString(optionName).c_str(), (char**)NULL); +} + + + +////////////////////////////// +// +// Options::getChar -- Return the first character in the option string; +// If the length is zero, then return '\0'. +// + +char Options::getChar(const std::string& optionName) { + return getString(optionName).c_str()[0]; +} + + + +////////////////////////////// +// +// Options::getFloat -- Return the floating point number +// associated with the given option. +// + +float Options::getFloat(const std::string& optionName) { + return (float)getDouble(optionName); +} + + + +////////////////////////////// +// +// Options::getInt -- Return the integer argument. Can handle +// hexadecimal, decimal, and octal written in standard +// C syntax. +// + +int Options::getInt(const std::string& optionName) { + return (int)strtol(getString(optionName).c_str(), (char**)NULL, 0); +} + +int Options::getInteger(const std::string& optionName) { + return getInt(optionName); +} + + + +////////////////////////////// +// +// Options::getString -- Return the option argument string. +// + +std::string Options::getString(const std::string& optionName) { + int index = getRegIndex(optionName); + if (index < 0) { + return "UNKNOWN OPTION"; + } else { + return m_optionRegister[index]->getOption(); + } +} + + + +////////////////////////////// +// +// Options::optionsArg -- Return true if --options is present +// on the command line, otherwise returns false. +// + +int Options::optionsArg(void) { + return m_optionsArgument; +} + + + +////////////////////////////// +// +// Options::print -- Print a list of the defined options. +// + +std::ostream& Options::print(std::ostream& out) { + for (unsigned int i=0; igetDefinition() << "\t" + << m_optionRegister[i]->getDescription() << std::endl; + } + return out; +} + + + +////////////////////////////// +// +// Options::reset -- Clear all defined options. +// + +void Options::reset(void) { + unsigned int i; + for (i=0; isetModified(aString); +} + + + + +////////////////////////////// +// +// Options::setOptions -- Store the input list of options. +// + +void Options::setOptions(int argc, char** argv) { + m_processedQ = 0; + + m_extraArgv.resize(argc); + m_extraArgv_strings.resize(argc); + int oldsize = 0; + + int i; + for (i=0; i& argv) { + m_processedQ = 0; + + int oldsize = (int)m_extraArgv.size(); + m_extraArgv.resize(oldsize + argv.size()); + m_extraArgv_strings.resize(oldsize + argv.size()); + + unsigned int i; + for (i=0; i tokens; + std::vector tempargv; + std::string tempvalue; + + tokens.reserve(100); + tempargv.reserve(100); + tempvalue.reserve(1000); + + char ch = '\0'; + + int length = (int)strang.size(); + for (int i=0; i0) && (strang[i-1] != '\\')) { + doublequote = !doublequote; + if (doublequote == 0) { + // finished a doublequoted section of data, so store + // even if it is the empty string + ch = '\0'; + tempvalue += (ch); + tokens.push_back(tempvalue); + tempvalue.clear(); + continue; + } else { + // don't store the leading ": + continue; + } + } + } else if (!doublequote && (strang[i] == '\'')) { + if ((i>0) && (strang[i-1] != '\\')) { + singlequote = !singlequote; + if (singlequote == 0) { + // finished a singlequote section of data, so store + // even if it is the empty string + ch = '\0'; + tempvalue += ch; + tokens.push_back(tempvalue); + tempvalue.clear(); + continue; + } else { + // don't store the leading ": + continue; + } + } + } + + + if ((!doublequote && !singlequote) && std::isspace(strang[i])) { + if (tempvalue.size() > 0) { + tempvalue += ch; + tokens.push_back(tempvalue); + tempvalue.clear(); + } + } else { + ch = strang[i]; + tempvalue += ch; + } + } + if (tempvalue.size() > 0) { + tokens.push_back(tempvalue); + tempvalue.clear(); + } + + // now that the input string has been chopped up into pieces, + // assemble the argv structure + + tempargv.reserve(tokens.size()); + for (int i=0; i<(int)tempargv.size(); i++) { + tempargv[i] = tokens[i]; + } + + // now store the argv and argc data in opts: + + // now store the parsed command-line simulated tokens + // for actual storage: + appendOptions(tempargv); +} + + + +////////////////////////////// +// +// Options:getType -- Return the type of the option. +// + +char Options::getType(const std::string& optionName) { + int index = getRegIndex(optionName); + if (index < 0) { + return -1; + } else { + return m_optionRegister[getRegIndex(optionName)]->getType(); + } +} + + + +////////////////////////////// +// +// Options::process -- Same as xverify. +// default values: error_check = 1, suppress = 0; +// + +void Options::process(int argc, char** argv, int error_check, int suppress) { + setOptions(argc, argv); + xverify(error_check, suppress); +} + + +void Options::process(int error_check, int suppress) { + xverify(error_check, suppress); +} + + + +////////////////////////////// +// +// Options::xverify -- +// default value: error_check = 1, suppress = 0; +// + +void Options::xverify(int error_check, int suppress) { + m_options_error_check = error_check; + int gargp = 1; + int optionend = 0; + + if (suppress) { + m_suppressQ = 1; + } else { + m_suppressQ = 0; + } + + // if calling xverify again, must remove previous argument list. + if (m_argument.size() != 0) { + m_argument.clear(); + } + + m_argument.push_back(m_oargv[0]); + int oldgargp; + int position = 0; + int running = 0; + while (gargp < m_oargc && optionend == 0) { + if (optionQ(m_oargv[gargp], gargp)) { + oldgargp = gargp; + gargp = storeOption(gargp, position, running); + if (gargp != oldgargp) { + running = 0; + position = 0; + } + } else { + if (m_oargv[gargp].size() == 2 && m_oargv[gargp][0] == getFlag() && + m_oargv[gargp][2] == getFlag() ) { + optionend = 1; + gargp++; + break; + } else { // this is an argument + m_argument.push_back(m_oargv[gargp]); + gargp++; + } + } + } + + while (gargp < m_oargc) { + m_argument.push_back(m_oargv[gargp]); + gargp++; + } + +} + + +void Options::xverify(int argc, char** argv, int error_check, int suppress) { + setOptions(argc, argv); + xverify(error_check, suppress); +} + + + + +/////////////////////////////////////////////////////////////////////////// +// +// private functions +// + + +////////////////////////////// +// +// Options::getRegIndex -- returns the index of the option associated +// with this name. +// + +int Options::getRegIndex(const std::string& optionName) { + if (m_suppressQ && (optionName == "options")) { + return -1; + } + + if (optionName == "options") { + print(std::cout); + exit(0); + } + + + auto it = m_optionList.find(optionName); + if (it == m_optionList.end()) { + if (m_options_error_check) { + std::cerr << "Error: unknown option \"" << optionName << "\"." << std::endl; + print(std::cout); + exit(1); + } else { + return -1; + } + } else { + return it->second; + } +} + + + +////////////////////////////// +// +// Options::optionQ -- returns true if the string is an option +// "--" is not an option, also '-' is not an option. +// aString is assumed to not be NULL. +// + +int Options::optionQ(const std::string& aString, int& argp) { + if (aString[0] == getFlag()) { + if (aString[1] == '\0') { + argp++; + return 0; + } else if (aString[1] == getFlag()) { + if (aString[2] == '\0') { + argp++; + return 0; + } else { + return 1; + } + } else { + return 1; + } + } else { + return 0; + } +} + + + +////////////////////////////// +// +// Options::storeOption -- +// + +#define OPTION_FORM_short 0 +#define OPTION_FORM_long 1 +#define OPTION_FORM_continue 2 + +int Options::storeOption(int gargp, int& position, int& running) { + int optionForm; + char tempname[4096]; + char optionType = OPTION_TYPE_unknown; + + if (running) { + optionForm = OPTION_FORM_continue; + } else if (m_oargv[gargp][1] == getFlag()) { + optionForm = OPTION_FORM_long; + } else { + optionForm = OPTION_FORM_short; + } + + switch (optionForm) { + case OPTION_FORM_continue: + position++; + tempname[0] = m_oargv[gargp][position]; + tempname[1] = '\0'; + optionType = getType(tempname); + if (optionType != OPTION_TYPE_boolean) { + running = 0; + position++; + } + break; + case OPTION_FORM_short: + position = 1; + tempname[0] = m_oargv[gargp][position]; + tempname[1] = '\0'; + optionType = getType(tempname); + if (optionType != OPTION_TYPE_boolean) { + position++; + } + break; + case OPTION_FORM_long: + position = 2; + while (m_oargv[gargp][position] != '=' && + m_oargv[gargp][position] != '\0') { + tempname[position-2] = m_oargv[gargp][position]; + position++; + } + tempname[position-2] = '\0'; + optionType = getType(tempname); + if (optionType == OPTION_TYPE_unknown) { // suppressed --options option + m_optionsArgument = 1; + break; + } + if (m_oargv[gargp][position] == '=') { + if (optionType == OPTION_TYPE_boolean) { + std::cerr << "Error: boolean variable cannot have any options: " + << tempname << std::endl; + exit(1); + } + position++; + } + break; + } + + if (optionType == OPTION_TYPE_unknown) { // suppressed --options option + m_optionsArgument = 1; + gargp++; + position = 0; + return gargp; + } + + if (m_oargv[gargp][position] == '\0' && + optionType != OPTION_TYPE_boolean) { + gargp++; + position = 0; + } + + if ((optionForm != OPTION_FORM_long) && (optionType == OPTION_TYPE_boolean) && + (m_oargv[gargp][position+1] != '\0')) { + running = 1; + } else if ((optionType == OPTION_TYPE_boolean) && + (m_oargv[gargp][position+1] == '\0')) { + running = 0; + } + + if (gargp >= m_oargc) { + std::cerr << "Error: last option requires a parameter" << std::endl; + exit(1); + } + setModified(tempname, &m_oargv[gargp][position]); + + if (!running) { + gargp++; + } + return gargp; +} + + + +////////////////////////////// +// +// Options::printOptionList -- +// + +std::ostream& Options::printOptionList(std::ostream& out) { + for (auto it = m_optionList.begin(); it != m_optionList.end(); it++) { + out << it->first << "\t" << it->second << std::endl; + } + return out; +} + + + +////////////////////////////// +// +// Options::printOptionBooleanState -- +// + +std::ostream& Options::printOptionListBooleanState(std::ostream& out) { + for (auto it = m_optionList.begin(); it != m_optionList.end(); it++) { + out << it->first << "\t" + << m_optionRegister[it->second]->isModified() << std::endl; + } + return out; +} + + + +////////////////////////////// +// +// Options::printRegister -- +// + +std::ostream& Options::printRegister(std::ostream& out) { + for (auto it = m_optionRegister.begin(); it != m_optionRegister.end(); it++) { + (*it)->print(out); + } + return out; +} + + +} // end namespace smf + + + diff --git a/resources/MIDI_LOGO.svg b/resources/MIDI_LOGO.svg new file mode 100644 index 0000000..6f954eb --- /dev/null +++ b/resources/MIDI_LOGO.svg @@ -0,0 +1,63 @@ + + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + diff --git a/resources/MIDI_TRAY.svg b/resources/MIDI_TRAY.svg new file mode 100644 index 0000000..6640ec5 --- /dev/null +++ b/resources/MIDI_TRAY.svg @@ -0,0 +1,44 @@ + + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + diff --git a/resources/keys.png b/resources/keys.png new file mode 100644 index 0000000..9b445d1 Binary files /dev/null and b/resources/keys.png differ diff --git a/resources/resources.qrc b/resources/resources.qrc new file mode 100644 index 0000000..292aa66 --- /dev/null +++ b/resources/resources.qrc @@ -0,0 +1,7 @@ + + + MIDI_LOGO.svg + MIDI_TRAY.svg + keys.png + + diff --git a/resources/shot.png b/resources/shot.png new file mode 100644 index 0000000..6fce2c5 Binary files /dev/null and b/resources/shot.png differ diff --git a/resources/xmidix.desktop b/resources/xmidix.desktop new file mode 100644 index 0000000..a30990f --- /dev/null +++ b/resources/xmidix.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=XMIDIX +GenericName=HARDWARE MIDI PLAYER +Comment=XMIDIX HARDWARE MIDI PLAYER +Type=Application +Terminal=false +Exec=xmidix +Icon=xmidix +MimeType=audio/midi;audio/x-midi +Categories=AudioVideo;Audio;Player diff --git a/test/test.mid b/test/test.mid new file mode 100644 index 0000000..eca4bd6 Binary files /dev/null and b/test/test.mid differ diff --git a/test/test2.mid b/test/test2.mid new file mode 100644 index 0000000..5525232 Binary files /dev/null and b/test/test2.mid differ diff --git a/test/test3.mid b/test/test3.mid new file mode 100644 index 0000000..bafbe93 Binary files /dev/null and b/test/test3.mid differ diff --git a/xmidix.cc b/xmidix.cc new file mode 100644 index 0000000..14fc774 --- /dev/null +++ b/xmidix.cc @@ -0,0 +1,43 @@ +#include "xmidix_player.h" + +#include +#include +#include +#include +#include + +typedef struct { + QList files; +} args_t; + +args_t parse_args(int argc, char **argv) { + QList files; + + for (int i = 1; i < argc; i++) { + auto arg = argv[i]; + if (QFileInfo::exists(arg)) { + auto url = QUrl::fromLocalFile(arg); + files.append(url); + } else { + spdlog::warn("file does not exist: {}", arg); + } + } + + return {files}; +} + +int main(int argc, char **argv) { + spdlog::set_level(spdlog::level::debug); + QApplication a(argc, argv); + QCoreApplication::setOrganizationName("xeyes.org"); + QCoreApplication::setOrganizationDomain("xeyes.org"); + QCoreApplication::setApplicationName("XMIDIX"); + + auto args = parse_args(argc, argv); + + xmidix_player w{nullptr, args.files}; + w.show(); + + bool result = a.exec(); + return result; +} diff --git a/xmidix.ui b/xmidix.ui new file mode 100644 index 0000000..c7eb254 --- /dev/null +++ b/xmidix.ui @@ -0,0 +1,272 @@ + + + MainWindow + + + + 0 + 0 + 526 + 252 + + + + + MS Mincho + 12 + + + + PointingHandCursor + + + XMIDIX + + + + ../../.designer/backup../../.designer/backup + + + + + + + + MS Mincho + 16 + + + + true + + + true + + + true + + + QAbstractItemView::InternalMove + + + QAbstractItemView::ExtendedSelection + + + + + + + + 0 + 0 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 1 + + + 0 + + + 0 + + + + + false + + + false + + + Qt::Horizontal + + + false + + + false + + + QSlider::NoTicks + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 10 + + + + + + + + + .. + + + + + + + + + + + .. + + + + + + + + + + + .. + + + + + + + + + + + .. + + + + + + + + + + + .. + + + + + + + + + + + .. + + + + + + + + + + + :/icons/MIDI_LOGO.svg:/icons/MIDI_LOGO.svg + + + + + + + + + + + :/icons/keys.png:/icons/keys.png + + + + + + + + + Qt::Horizontal + + + + 30 + 20 + + + + + + + + + + + + + xmidix_vu_array + QFrame +
xmidix_vu.h
+ 1 +
+
+ + + + +
diff --git a/xmidix_config.cc b/xmidix_config.cc new file mode 100644 index 0000000..2378759 --- /dev/null +++ b/xmidix_config.cc @@ -0,0 +1,78 @@ +#include "xmidix_config.h" + +xmidix_config::xmidix_config(QWidget *parent) + : QDialog(parent), ui{} { + ui.setupUi(this); + output_device = ui.output_device; + + connect(ui.midi_refresh_button, &QPushButton::clicked, + [this] { emit clients_update(); }); + connect(ui.buttonBox, &QDialogButtonBox::accepted, + this, &xmidix_config::accepted_handler); + connect(ui.buttonBox, &QDialogButtonBox::rejected, + this, &xmidix_config::rejected_handler); +} + +xmidix_client_info xmidix_config::get_selected_client() { + return clients[output_index]; +} + +void xmidix_config::reload_settings() { +// ui.systray_check->setChecked(settings.) +} + +void xmidix_config::set_selected_client(int client_id, int port_id) { + for (int i = 0; i < clients.size(); i++) { + auto client = clients[i]; + if (client.client_id == client_id && client.port_id == port_id) { + output_device->setCurrentIndex(i); + return; + } + } +} + +void xmidix_config::clients_updated(const std::vector &c) { + clients = c; + output_index = output_device->currentIndex(); + output_device->clear(); + + for (const auto &client : clients) { + auto txt = fmt::format("{}:{}\t{} - {}", + client.client_id, client.port_id, + client.client_name, client.port_name); + output_device->addItem(QString::fromStdString(txt)); + } + + output_device->setCurrentIndex(output_index); +} + +void xmidix_config::accepted_handler() { + bool updated = false; + if (output_device->currentIndex() != output_index) { + output_index = output_device->currentIndex(); + updated = true; + } + + if (ui.systray_check->isChecked() != systray) { + updated = true; + systray = ui.systray_check->isChecked(); + } + + if (ui.channel_status_check->isChecked() != chanbar) { + updated = true; + chanbar = ui.channel_status_check->isChecked(); + } + + if (updated) { + emit config_updated(); + } +} + +void xmidix_config::rejected_handler() { + output_device->setCurrentIndex(output_index); +} + +void xmidix_config::showEvent(QShowEvent *event) { + reload_settings(); + QDialog::showEvent(event); +} diff --git a/xmidix_config.h b/xmidix_config.h new file mode 100644 index 0000000..9de93b0 --- /dev/null +++ b/xmidix_config.h @@ -0,0 +1,55 @@ +#ifndef XMIDIX__XMIDIX_CONFIG_H_ +#define XMIDIX__XMIDIX_CONFIG_H_ + +#include "ui_xmidix_config.h" +#include "xmidix_seq.h" + +#include +#include +#include +#include +#include + +class xmidix_config : public QDialog { + Q_OBJECT + public: + explicit xmidix_config(QWidget *parent = nullptr); + xmidix_client_info get_selected_client(); + void set_selected_client(int client_id, int port_id); + + void set_systray(bool val) { + systray = val; + ui.systray_check->setChecked(val); + } + + void set_chanbar(bool val) { + chanbar = val; + ui.channel_status_check->setChecked(val); + } + + Ui::config_window ui; + + signals: + void config_updated(); + void clients_update(); + + public slots: + void clients_updated(const std::vector& c); + void accepted_handler(); + void rejected_handler(); + + protected: + void showEvent(QShowEvent *event) override; + + private: + void reload_settings(); + + std::vector clients; + + QComboBox *output_device; + int output_index = 0; + bool systray{true}; + bool chanbar{true}; +}; + +#endif //XMIDIX__XMIDIX_CONFIG_H_ diff --git a/xmidix_config.ui b/xmidix_config.ui new file mode 100644 index 0000000..5689482 --- /dev/null +++ b/xmidix_config.ui @@ -0,0 +1,206 @@ + + + config_window + + + + 0 + 0 + 407 + 118 + + + + + 0 + 0 + + + + + MS Mincho + 12 + + + + PointingHandCursor + + + XMIDI CONFIG + + + + .. + + + + + + 8 + + + + + + 0 + 26 + + + + OUTPUT DEVICE: + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 205 + 26 + + + + + 16777215 + 26 + + + + + + + + + 0 + 0 + + + + + 16777215 + 26 + + + + + + + + .. + + + + + + + + + + + + + + + + + SHOW CHANNEL STATUS BAR + + + true + + + + + + + SHOW SYSTEM TRAY + + + true + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + config_window + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + config_window + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/xmidix_file.cc b/xmidix_file.cc new file mode 100644 index 0000000..480d9f4 --- /dev/null +++ b/xmidix_file.cc @@ -0,0 +1,94 @@ +#include "xmidix_file.h" + +#include +#include +#include + +#define PARSER_BUF_SIZE 16 + +std::string decode_string(MidiEvent *event) { + auto txt = dynamic_cast(event)->getMetaContent(); + auto codec = QTextCodec::codecForName("Shift-JIS"); + auto decoded = codec->toUnicode(txt.c_str()); + + return decoded.toStdString(); +} + +void xmidix_file::handle_meta_event(MidiEvent *event, snd_seq_event_t *seq_event) { + if (event->isTempo()) { + snd_seq_ev_set_queue_tempo(seq_event, 0, event->getTempoMicro()); + return; + } + + if (event->isText()) { + texts.emplace_back(decode_string(event)); + return; + } + + if (event->isMarkerText()) { + markers.emplace_back(xmidix_marker_t{ + event->tick, + decode_string(event) + }); + return; + } + + if (event->isTrackName()) { + track_names[event->track] = decode_string(event); + return; + } +} + +xmidix_file::xmidix_file(const std::string &filename) { + MidiFile midifile; + if (!midifile.read(filename)) { + spdlog::warn("could not open MIDI file..."); + return; + } + + midifile.joinTracks(); + ppqn = midifile.getTicksPerQuarterNote(); + + snd_midi_event_t *parser; + snd_midi_event_new(PARSER_BUF_SIZE, &parser); + + auto midi_events = midifile[0]; // only support single track atm + for (int i = 0; i < midi_events.getEventCount(); i++) { + auto event = midi_events[i]; + + snd_seq_event_t seq_event; + snd_seq_ev_clear(&seq_event); + + snd_midi_event_reset_encode(parser); + auto result = snd_midi_event_encode(parser, event.data(), static_cast(event.size()), &seq_event); + if (result <= 0) { + spdlog::warn("error encoding midi event at tick = {}", event.tick); + continue; + } + + if (event.isMeta()) { + handle_meta_event(&event, &seq_event); + } + + seq_event.time.tick = event.tick; + events.emplace_back(seq_event); + } + + for (const auto& t : texts) { + spdlog::info("text = [{}]", t); + } + + for (const auto& m : markers) { + spdlog::info("marker = {}: [{}]", m.tick, m.name); + } + + for (const auto& [k, v] : track_names) { + spdlog::info("track = {}: [{}]", k, v); + } + + snd_midi_event_free(parser); +} + +xmidix_file::~xmidix_file() { + spdlog::debug("destructing xmidix_file"); +} diff --git a/xmidix_file.h b/xmidix_file.h new file mode 100644 index 0000000..2f7782b --- /dev/null +++ b/xmidix_file.h @@ -0,0 +1,35 @@ +#ifndef ARP__XMIDI_FILE_H_ +#define ARP__XMIDI_FILE_H_ + +#include +#include +#include + +#include +#include +#include +#include + +using namespace smf; + +typedef struct { + int tick; + std::string name; +} xmidix_marker_t; + +class xmidix_file { + public: + explicit xmidix_file(const std::string &filename); + ~xmidix_file(); + + std::vector events; + std::vector markers; + std::map track_names; + std::vector texts; + int ppqn; + + private: + void handle_meta_event(MidiEvent *event, snd_seq_event_t *seq_event); +}; + +#endif //ARP__XMIDI_FILE_H_ diff --git a/xmidix_piano.cc b/xmidix_piano.cc new file mode 100644 index 0000000..3bcd3f1 --- /dev/null +++ b/xmidix_piano.cc @@ -0,0 +1,129 @@ +#include "xmidix_piano.h" + +#include +#include +#include +#include + +#define TRIGGER_COLOR Qt::red +#define WHITE_COLOR Qt::white +#define BLACK_COLOR Qt::black +#define OUTLINE_COLOR Qt::black + +bool is_white_key(int idx) { + switch (idx % 12) { + case 1: + case 3: + case 6: + case 8: + case 10: + return false; + default: + return true; + } +} + +xmidix_piano::xmidix_piano(QWidget *parent, std::shared_ptr grad) + : QWidget{parent}, gradient{grad} { + setMinimumHeight(30); + setMinimumWidth(128 * 6); +// setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); +} + +void xmidix_piano::paintEvent(QPaintEvent *event) { + QPainter painter{this}; + painter.setPen(OUTLINE_COLOR); + + double white_width = (width() - 1) / 75.0; + double black_width = white_width * 0.60; + double black_adjust = black_width / 2.0; + double x = 0; + + std::map black_offsets; + + // draw white keys + for (int i = 0; i < 128; i++) { + auto velocity = key_state[i]; + auto color = velocity == 0 ? WHITE_COLOR : gradient->pixelColor(velocity, 0); + + if (is_white_key(i)) { + QRectF r(x, 0, white_width, height() - 1); +// painter.fillRect(r, on ? TRIGGER_COLOR : WHITE_COLOR); + painter.fillRect(r, color); + painter.drawRect(r); + x += white_width; + } else { + // save x position for next loop + black_offsets[i] = x - black_adjust; + } + } + + // draw black keys using offsets + for (const auto&[i, offset] : black_offsets) { + auto velocity = key_state[i]; + auto color = velocity == 0 ? BLACK_COLOR : gradient->pixelColor(velocity, 0); + +// QRectF r(offset, 0, black_width, height() / 2.0); + QRectF r(offset, 0, black_width, height() * 0.6); +// painter.fillRect(r, on ? TRIGGER_COLOR : BLACK_COLOR); + painter.fillRect(r, color); + painter.drawRect(r); + } +} + +xmidix_piano_array::xmidix_piano_array(QWidget *parent) + : QDialog{parent} { + setAttribute(Qt::WA_X11NetWmWindowTypeUtility); + setWindowTitle("XMIDIXPIANOX"); +// setFixedHeight(518); +// setFixedWidth(600); +// setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + + setLayout(new QVBoxLayout(this)); + + QLinearGradient grad(QPoint(0, 0), QPoint(127, 0)); + grad.setColorAt(1, Qt::red); + grad.setColorAt(0.75, Qt::yellow); + grad.setColorAt(0, Qt::green); + + auto img = std::make_shared(128, 1, QImage::Format_RGB32); + QPainter painter{img.get()}; + painter.fillRect(img->rect(), QBrush(grad)); + painter.end(); + + for (int i = 0; i < 16; i++) { + auto piano = std::make_shared(this, img); + layout()->addWidget(piano.get()); + pianos.emplace_back(piano); + } + + updateGeometry(); +} + +void xmidix_piano_array::midi_event(const snd_seq_event_t &event) { + auto type = event.type; + if (type == SND_SEQ_EVENT_NOTEOFF || type == SND_SEQ_EVENT_NOTEON || type == SND_SEQ_EVENT_KEYPRESS) { + auto data = event.data.note; + auto chan = data.channel; + + if (chan < 0 || chan >= 16) { + spdlog::warn("got invalid channel = {}", chan); + return; + } + + uint8_t velocity = type != SND_SEQ_EVENT_NOTEOFF ? data.velocity : 0; + pianos[data.channel]->set_key(data.note, velocity); + } +} + +void xmidix_piano_array::hideEvent(QHideEvent *event) { + QWidget::hideEvent(event); + position = pos(); +} + +void xmidix_piano_array::showEvent(QShowEvent *event) { + QDialog::showEvent(event); + move(position); +} diff --git a/xmidix_piano.h b/xmidix_piano.h new file mode 100644 index 0000000..dbda165 --- /dev/null +++ b/xmidix_piano.h @@ -0,0 +1,54 @@ +#ifndef XMIDIX__XMIDIX_PIANO_H_ +#define XMIDIX__XMIDIX_PIANO_H_ + +#include +#include +#include +#include + +class xmidix_piano : public QWidget { + public: + explicit xmidix_piano(QWidget *parent = nullptr, std::shared_ptr grad = nullptr); + + void clear() { + for (int i = 0; i < 128; i++) + key_state[i] = false; + + repaint(); + } + + void set_key(int key, uint8_t velocity) { + key_state[key] = velocity; + repaint(); + } + + protected: + void paintEvent(QPaintEvent *event) override; + + private: + uint8_t key_state[128] = {0}; + std::shared_ptr gradient; +}; + +class xmidix_piano_array : public QDialog { + Q_OBJECT + public: + explicit xmidix_piano_array(QWidget *parent = nullptr); + void clear() { + for (const auto &p : pianos) + p->clear(); + } + + public slots: + void midi_event(const snd_seq_event_t &event); + + protected: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + + private: + std::vector> pianos; + QPoint position; +}; + +#endif //XMIDIX__XMIDIX_PIANO_H_ diff --git a/xmidix_player.cc b/xmidix_player.cc new file mode 100644 index 0000000..1ceb6cf --- /dev/null +++ b/xmidix_player.cc @@ -0,0 +1,297 @@ +#include "xmidix_player.h" + +#include +#include +#include +#include +#include + +xmidix_player::xmidix_player(QWidget *parent, QList files) + : QMainWindow(parent), + ui{}, + seq{}, + config_window{this}, + model{this}, + progress_timer{this}, + tray_icon{QIcon(":/icons/MIDI_LOGO.svg"), this}, + settings{this}, + status{play_status::STOPPED}, + last_tick{0} { + + setAttribute(Qt::WA_X11NetWmWindowTypeUtility); + setWindowTitle(WINDOW_TITLE); + tray_icon.show(); + + model.replace(files); + + ui.setupUi(this); + ui.listView->setModel(&model); + ui.listView->installEventFilter(this); + + setup_actions(); + setup_connections(); + config_client_update(); + + load_settings(); +} + +void xmidix_player::load_settings() { + auto client = settings.value(SETTINGS_CLIENT); + auto port = settings.value(SETTINGS_PORT); + + auto client_id = client.isNull() ? 14 : client.toInt(); + auto port_id = port.isNull() ? 0 : port.toInt(); + + seq.connect(client_id, port_id); + config_window.set_selected_client(client_id, port_id); + + auto tray = settings.value(SETTINGS_SYSTRAY); + auto tray_visible = tray.isNull() || tray.toBool(); + tray_icon.setVisible(tray_visible); + config_window.set_systray(tray_visible); + + auto chanbar = settings.value(SETTINGS_CHANBAR); + auto chanbar_visible = chanbar.isNull() || chanbar.toBool(); + ui.vus->setVisible(chanbar_visible); + config_window.set_chanbar(chanbar_visible); +} + +void xmidix_player::setup_action(QAction *a, const QKeySequence &s) { + a->setShortcut(s); + addAction(a); +} + +// to satisfy c++20 pedantry +inline constexpr int operator|(Qt::Modifier m, Qt::Key k) { return (int) m | (int) k; } +void xmidix_player::setup_actions() { + setup_action(&play_action, Qt::Key_Space); + setup_action(&load_action, Qt::CTRL | Qt::Key_O); + setup_action(&stop_action, Qt::CTRL | Qt::Key_Space); + setup_action(&conf_action, Qt::CTRL | Qt::Key_P); + setup_action(&back_action, Qt::CTRL | Qt::Key_Left); + setup_action(&forw_action, Qt::CTRL | Qt::Key_Right); + setup_action(&shuf_action, {}); +} + +void xmidix_player::setup_connections() { + connect(&seq, &xmidix_seq::song_complete, + this, &xmidix_player::song_complete); + connect(&seq, &xmidix_seq::song_looping, + [] { spdlog::info("song looping."); }); + + connect(&progress_timer, &QTimer::timeout, + this, &xmidix_player::tick_update); + + connect(this, &xmidix_player::status_update, + this, &xmidix_player::status_updated); + connect(this, &xmidix_player::clients_updated, + &config_window, &xmidix_config::clients_updated); + connect(this, &xmidix_player::song_change, + this, &xmidix_player::play); + + connect(&config_window, &xmidix_config::clients_update, + this, &xmidix_player::config_client_update); + connect(&config_window, &xmidix_config::config_updated, + this, &xmidix_player::config_updated); + + connect(ui.listView, &QListView::activated, + this, &xmidix_player::playlist_activated); + connect(ui.slider, &QSlider::sliderReleased, + this, &xmidix_player::slider_seek); + + connect(ui.play_button, &QPushButton::clicked, + &play_action, &QAction::trigger); + connect(ui.load_button, &QPushButton::clicked, + &load_action, &QAction::trigger); + connect(ui.stop_button, &QPushButton::clicked, + &stop_action, &QAction::trigger); + connect(ui.config_button, &QPushButton::clicked, + &conf_action, &QAction::trigger); + connect(ui.backward_button, &QPushButton::clicked, + &back_action, &QAction::trigger); + connect(ui.forward_button, &QPushButton::clicked, + &forw_action, &QAction::trigger); + connect(ui.shuffle_button, &QPushButton::clicked, + &shuf_action, &QAction::trigger); + + connect(&play_action, &QAction::triggered, + this, &xmidix_player::play_button_click); + connect(&load_action, &QAction::triggered, + this, &xmidix_player::load_button_click); + connect(&stop_action, &QAction::triggered, + this, &xmidix_player::stop_button_click); + connect(&conf_action, &QAction::triggered, + this, &xmidix_player::config_button_click); + connect(&back_action, &QAction::triggered, + [&] { emit song_change(model.previous_item()); }); + connect(&forw_action, &QAction::triggered, + [&] { emit song_change(model.next_item()); }); + connect(&shuf_action, &QAction::triggered, + [&] { model.shuffle(); }); + + connect(&tray_icon, &QSystemTrayIcon::activated, + [&] { this->setVisible(!this->isVisible()); }); + connect(ui.piano_button, &QPushButton::clicked, + [&] { pianos.setVisible(!pianos.isVisible()); }); + + connect(&seq, &xmidix_seq::midi_event, + ui.vus, &xmidix_vu_array::midi_event); + connect(&seq, &xmidix_seq::midi_event, + &pianos, &xmidix_piano_array::midi_event); +} + +xmidix_player::~xmidix_player() { + +} + +void xmidix_player::play_button_click() { + switch (status) { + case play_status::PLAYING: + emit status_update(play_status::PAUSED); + seq.stop(); + break; + case play_status::PAUSED: + emit status_update(play_status::PLAYING); + seq.unpause(); + break; + case play_status::STOPPED: + emit song_change(model.current_item()); + break; + } +} + +void xmidix_player::stop_button_click() { + emit status_update(play_status::STOPPED); + seq.stop(); +} + +void xmidix_player::load_button_click() { + auto cwd = settings.value(SETTINGS_CWD).toString(); + auto files = QFileDialog::getOpenFileUrls( + this, "OPEN MIDI FILES", + QUrl::fromLocalFile(cwd), + "MIDI FILES (*.mid *.midi)"); + + if (!files.empty()) { + auto f = QFileInfo(files[0].toLocalFile()); + settings.setValue(SETTINGS_CWD, f.dir().canonicalPath()); + + model.replace(files); + } +} + +void xmidix_player::playlist_activated(const QModelIndex &index) { + model.set_index(index.row()); + emit song_change(model.current_item()); +} + +void xmidix_player::play(const QUrl &url) { + auto path = url.path().toStdString(); + + xmidix_file midi(path); + if (midi.events.empty()) { + spdlog::warn("bad midi file = {}", path); + return; + } + + auto events = midi.events; + auto last_event = events[events.size() - 1]; + last_tick = last_event.time.tick; + spdlog::info("last tick of song = {}", last_tick); + + spdlog::info("playing = {}", url.path().toStdString()); + emit status_update(play_status::PLAYING); + seq.load(midi); + seq.play(); + + setWindowTitle( + fmt::format("{} - {}", WINDOW_TITLE, url.fileName().toStdString()).c_str()); + + auto idx = model.current_index(); + ui.listView->clearSelection(); + ui.listView->setCurrentIndex(idx); + ui.listView->scrollTo(idx); +} + +void xmidix_player::config_button_click() { + config_window.show(); +} + +void xmidix_player::config_updated() { + auto client = config_window.get_selected_client(); + settings.setValue(SETTINGS_CLIENT, client.client_id); + settings.setValue(SETTINGS_PORT, client.port_id); + seq.connect(client.client_id, client.port_id); + + auto tray = config_window.ui.systray_check->isChecked(); + settings.setValue(SETTINGS_SYSTRAY, tray); + tray_icon.setVisible(tray); + + auto chanbar = config_window.ui.channel_status_check->isChecked(); + settings.setValue(SETTINGS_CHANBAR, chanbar); + ui.vus->setVisible(chanbar); +} + +void xmidix_player::config_client_update() { + emit clients_updated(seq.enumerate_clients()); +} + +void xmidix_player::status_updated(play_status s) { + ui.vus->clear(); + pianos.clear(); + + status = s; + switch (status) { + case play_status::STOPPED: + setWindowTitle(WINDOW_TITLE); + ui.play_button->setIcon(QIcon::fromTheme("media-playback-start")); + ui.slider->setValue(0); + ui.slider->setEnabled(false); + progress_timer.stop(); + break; + case play_status::PLAYING: + ui.play_button->setIcon(QIcon::fromTheme("media-playback-pause")); + ui.slider->setEnabled(true); + progress_timer.start(std::chrono::seconds{1}); + break; + case play_status::PAUSED: + ui.play_button->setIcon(QIcon::fromTheme("media-playback-start")); + progress_timer.stop(); + break; + } +} + +void xmidix_player::song_complete() { + if (status == play_status::PLAYING) { + spdlog::debug("song complete."); + emit song_change(model.next_item()); + ui.vus->clear(); + } +} + +void xmidix_player::tick_update() { + if (!ui.slider->isSliderDown()) { + auto tick = seq.get_tick(); + auto percent = (static_cast(tick) / last_tick) * 100; + ui.slider->setValue(static_cast(percent)); + } +} + +void xmidix_player::slider_seek() { + auto pos = ui.slider->sliderPosition(); + auto tick = (static_cast(pos) / 100) * last_tick; + seq.seek(static_cast(tick)); +} + +bool xmidix_player::eventFilter(QObject *watched, QEvent *event) { +// if (event->type() == QEvent::KeyRelease) { +// auto e = reinterpret_cast(event); +// if (e->key() == Qt::Key_Delete) { +// auto indices = ui->listView->selectionModel()->selectedIndexes(); +// for (auto idx : indices) { +// model->removeRow(idx.row()); +// } +// } +// } + return QObject::eventFilter(watched, event); +} diff --git a/xmidix_player.h b/xmidix_player.h new file mode 100644 index 0000000..73493e0 --- /dev/null +++ b/xmidix_player.h @@ -0,0 +1,92 @@ +#ifndef ARP__XMIDI_PLAYER_H_ +#define ARP__XMIDI_PLAYER_H_ + +#include "ui_xmidix.h" +#include "xmidix_vu.h" +#include "xmidix_seq.h" +#include "xmidix_file.h" +#include "xmidix_piano.h" +#include "xmidix_config.h" +#include "xmidix_playlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define WINDOW_TITLE "XMIDIX" +#define SETTINGS_CLIENT "client" +#define SETTINGS_PORT "port" +#define SETTINGS_CWD "cwd" +#define SETTINGS_SYSTRAY "systray" +#define SETTINGS_CHANBAR "chanbar" + +enum class play_status { + STOPPED, PLAYING, PAUSED +}; + +class xmidix_player : public QMainWindow { + Q_OBJECT + + public: + explicit xmidix_player(QWidget *parent = nullptr, QList files = {}); + ~xmidix_player() override; + + public slots: + void play(const QUrl &url); + + void config_client_update(); + void config_updated(); + + void playlist_activated(const QModelIndex &index); + void load_button_click(); + void play_button_click(); + void stop_button_click(); + void config_button_click(); + + void status_updated(play_status status); + void song_complete(); + void tick_update(); + void slider_seek(); + + signals: + void song_change(const QUrl &url); + void clients_updated(const std::vector &c); + void status_update(play_status s); + + protected: + bool eventFilter(QObject *watched, QEvent *event) override; + + private: + void load_settings(); + void setup_connections(); + void setup_actions(); + void setup_action(QAction *a, const QKeySequence &s); + + play_status status; + unsigned int last_tick; + + Ui::MainWindow ui; + xmidix_config config_window; + xmidix_seq seq; + xmidix_playlist model; + xmidix_piano_array pianos; + + QTimer progress_timer; + QSettings settings; + QSystemTrayIcon tray_icon; + + QAction play_action; + QAction load_action; + QAction stop_action; + QAction conf_action; + QAction back_action; + QAction forw_action; + QAction shuf_action; +}; + +#endif //ARP__XMIDI_PLAYER_H_ diff --git a/xmidix_playlist.cc b/xmidix_playlist.cc new file mode 100644 index 0000000..5f7816c --- /dev/null +++ b/xmidix_playlist.cc @@ -0,0 +1,83 @@ +#include "xmidix_playlist.h" + +#include +#include +#include +#include + +xmidix_playlist::xmidix_playlist(QObject *parent) : QAbstractListModel(parent) { +} + +int xmidix_playlist::rowCount(const QModelIndex &index) const { + return items.size(); +} + +QVariant xmidix_playlist::data(const QModelIndex &index, int role) const { + if (!index.isValid()) + return {}; + + if (role == Qt::DisplayRole) { + auto url = items[index.row()]; + QFileInfo info(url.toLocalFile()); + return info.dir().dirName() + "/" + info.fileName(); + } + + return {}; +} + +void xmidix_playlist::append(const list_t &l) { + beginResetModel(); + items.append(l); + endResetModel(); +} + +void xmidix_playlist::replace(const list_t &l) { + beginResetModel(); + items.clear(); + items.append(l); + endResetModel(); +} + +void xmidix_playlist::clear() { + beginResetModel(); + items.clear(); + endResetModel(); +} + +item_t xmidix_playlist::current_item() { + if (items.empty()) return {}; + + return items[idx]; +} + +item_t xmidix_playlist::next_item() { + if (items.empty()) return {}; + + idx = idx == items.size() - 1 ? 0 : idx + 1; + return items[idx]; +} + +item_t xmidix_playlist::previous_item() { + if (items.empty()) return {}; + + idx = idx == 0 ? items.size() - 1 : idx - 1; + return items[idx]; +} + +void xmidix_playlist::shuffle() { + beginResetModel(); + std::random_device rd; + std::mt19937 g(rd()); + + std::shuffle(items.begin(), items.end(), g); + endResetModel(); +} + +bool xmidix_playlist::removeRows(int row, int count, const QModelIndex &parent) { + beginRemoveRows(parent, row, row + count); + for (int i = 0; i < count; i ++) { + items.removeAt(row + i); + } + endRemoveRows(); + return true; +} diff --git a/xmidix_playlist.h b/xmidix_playlist.h new file mode 100644 index 0000000..888bdf3 --- /dev/null +++ b/xmidix_playlist.h @@ -0,0 +1,38 @@ +#ifndef XMIDIX__XMIDIX_PLAYLIST_H_ +#define XMIDIX__XMIDIX_PLAYLIST_H_ + +#include +#include +#include +#include + +using item_t = QUrl; +using list_t = QList; + +class xmidix_playlist : public QAbstractListModel { + Q_OBJECT + public: + explicit xmidix_playlist(QObject *parent = nullptr); + + [[nodiscard]] int rowCount(const QModelIndex &index) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + + void append(const list_t &l); + void replace(const list_t &l); + void clear(); + void shuffle(); + void set_index(int index) { idx = index; } + + QModelIndex current_index() { return index(idx); } + item_t current_item(); + item_t next_item(); + item_t previous_item(); + + list_t items; + + private: + int idx{0}; +}; + +#endif //XMIDIX__XMIDIX_PLAYLIST_H_ diff --git a/xmidix_seq.cc b/xmidix_seq.cc new file mode 100644 index 0000000..f3a43ad --- /dev/null +++ b/xmidix_seq.cc @@ -0,0 +1,229 @@ +#include "xmidix_seq.h" + +#include + +#define DEFAULT_TEMPO 500000 + +xmidix_seq::xmidix_seq(int client, int port) + : QObject{nullptr}, seq{nullptr}, thread{nullptr}, port_ref{-1}, + last_tick{0}, division{1}, client_id{-1}, port_id{-1}, tempo{DEFAULT_TEMPO} { + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_OUTPUT, 0) < 0) { + spdlog::error("could not open sequencer."); + return; + } + + snd_seq_set_client_name(seq, "XMIDI OUTPUT"); + snd_seq_create_simple_port( + seq, "XMIDI DATA", + SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, + SND_SEQ_PORT_TYPE_MIDI_GENERIC); + + snd_seq_nonblock(seq, true); +} + +xmidix_seq::~xmidix_seq() { + spdlog::debug("destructing xmidix_seq."); + stop(); + snd_seq_close(seq); + spdlog::debug("xmidix_seq destroyed."); +} + +void xmidix_seq::connect(int client, int port) { + if (client_id != -1) { + snd_seq_disconnect_to(seq, 0, client_id, port_id); + } + + port_ref = snd_seq_connect_to(seq, 0, client_id = client, port_id = port); +} + +void xmidix_seq::thread_loop() { + spdlog::info("xmidix_seq thread starting.. # events = {}", events.size()); + +start: + unsigned start_from = 0; + if (resume) { + start_from = index_for_tick(last_tick); + spdlog::debug("found resume tick {} at index {}", last_tick, start_from); + resume = false; + } + + spdlog::debug("beginning song"); + snd_seq_event_t event; + current_tick = last_tick = events[start_from].time.tick; + + for (current_index = start_from; current_index < events.size(); current_index++) { + if (stopped) { + spdlog::info("xmidix_seq: stopped == true, breaking."); + break; + } + + { + std::lock_guard lock(seek_mutex); + event = events[current_index]; + last_tick = current_tick; + current_tick = event.time.tick; + } + + if (current_tick > last_tick) { + snd_seq_drain_output(seq); + + auto delta = current_tick - last_tick; + auto mspp = tempo / division; + auto duration = std::chrono::microseconds{delta * mspp}; + + if (duration > std::chrono::seconds{10}) { + spdlog::warn("next tick is > 10s... something might be wrong: {}s", + std::chrono::duration_cast(duration).count()); + duration = std::chrono::seconds{10}; + } + + std::this_thread::sleep_for(duration); + } + + send_event(event); + } + + if (!stopped && repeat) { + spdlog::info("looping..."); + emit song_looping(); + goto start; + } + + if (!stopped) { + emit song_complete(); + } + + all_notes_off(); + spdlog::info("xmidix_seq thread complete."); +} + +void xmidix_seq::load(const xmidix_file &file) { + panic(); + events = file.events; + division = file.ppqn; + resume = false; +} + +void xmidix_seq::play() { + if (!stopped) return; + + thread = std::make_unique(&xmidix_seq::thread_loop, this); + stopped = false; +} + +void xmidix_seq::unpause() { + if (!stopped) return; + + resume = true; + play(); +} + +void xmidix_seq::stop() { + snd_seq_drop_output_buffer(seq); + snd_seq_drop_output(seq); + all_notes_off(true); + + stopped = true; + resume = false; + + if (thread != nullptr && thread->joinable()) { + spdlog::info("waiting for midi thread to finish.."); + thread->join(); + spdlog::info("midi thread finished."); + thread = nullptr; + } +} + +void xmidix_seq::panic() { + all_notes_off(true); + stop(); +} + +void xmidix_seq::send_event(snd_seq_event_t ev) { + if (ev.type == SND_SEQ_EVENT_TEMPO) { + snd_seq_ev_queue_control_t data = ev.data.queue; + tempo = data.param.value; + spdlog::debug("got tempo event = {}", tempo); + return; + } + + snd_seq_ev_set_source(&ev, port_ref); + snd_seq_ev_set_subs(&ev); + snd_seq_ev_set_priority(&ev, 0); + snd_seq_ev_set_direct(&ev); + + int result; + if ((result = snd_seq_event_output(seq, &ev)) < 0) { + spdlog::warn("error sending event at tick {}: {}", ev.time.tick, result); + } + + snd_seq_drain_output(seq); + emit midi_event(ev); +} + +void xmidix_seq::all_notes_off(bool kill) { + send_to_all(MIDI_CTL_ALL_NOTES_OFF, 0); + if (kill) { + send_to_all(MIDI_CTL_ALL_SOUNDS_OFF, 0); + } +} + +void xmidix_seq::send_to_all(int cc, int v) { + for (int i = 0; i < 16; i++) { + snd_seq_event_t e; + snd_seq_ev_clear(&e); + snd_seq_ev_set_controller(&e, i, cc, v); + send_event(e); + } +} + +std::vector xmidix_seq::enumerate_clients() { + std::vector clients; + + snd_seq_client_info_t *c_info; + snd_seq_client_info_malloc(&c_info); + snd_seq_client_info_set_client(c_info, -1); + + snd_seq_port_info_t *p_info; + snd_seq_port_info_malloc(&p_info); + + while (snd_seq_query_next_client(seq, c_info) == 0) { + auto c_id = snd_seq_client_info_get_client(c_info); + auto c_name = snd_seq_client_info_get_name(c_info); + + snd_seq_port_info_set_client(p_info, c_id); + snd_seq_port_info_set_port(p_info, -1); + + while (snd_seq_query_next_port(seq, p_info) == 0) { + auto p_id = snd_seq_port_info_get_port(p_info); + auto p_name = snd_seq_port_info_get_name(p_info); + auto caps = snd_seq_port_info_get_capability(p_info); + + if (caps & SND_SEQ_PORT_CAP_WRITE) { + clients.emplace_back(xmidix_client_info{ + c_name, c_id, p_name, p_id + }); + } + } + } + + snd_seq_port_info_free(p_info); + snd_seq_client_info_free(c_info); + return clients; +} + +int xmidix_seq::index_for_tick(unsigned int tick) { + for (int i = 0; i < events.size(); i++) { + if (tick <= events[i].time.tick) { + return i; + } + } + return 0; +} + +void xmidix_seq::seek(unsigned int tick) { + std::lock_guard lock(seek_mutex); + all_notes_off(true); + current_tick = last_tick = tick; + current_index = index_for_tick(tick); +} \ No newline at end of file diff --git a/xmidix_seq.h b/xmidix_seq.h new file mode 100644 index 0000000..b26509e --- /dev/null +++ b/xmidix_seq.h @@ -0,0 +1,76 @@ +#ifndef XMIDI__XMIDI_SEQ_H_ +#define XMIDI__XMIDI_SEQ_H_ + +#include +#include + +#include +#include +#include +#include + +#include "xmidix_file.h" + +Q_DECLARE_METATYPE(snd_seq_event_t); +[[maybe_unused]] static const int typeId = qRegisterMetaType(); + +typedef struct { + std::string client_name; + int client_id; + std::string port_name; + int port_id; +} xmidix_client_info; + +class xmidix_seq : public QObject { + Q_OBJECT + public: + explicit xmidix_seq(int client = 14, int port = 0); + ~xmidix_seq() override; + + void connect(int client = 14, int port = 0); + void load(const xmidix_file &file); + void play(); + void stop(); + void panic(); + void unpause(); + void set_repeat(bool r) { repeat = r; } + void seek(unsigned int tick); + + std::vector enumerate_clients(); + [[nodiscard]] unsigned get_tick() const { return last_tick; } + + signals: + void song_complete(); + void song_looping(); + void midi_event(const snd_seq_event_t &event); + + private: + void thread_loop(); + void send_event(snd_seq_event_t ev); + void all_notes_off(bool kill = false); + void send_to_all(int cc, int v); + int index_for_tick(unsigned int tick); + + snd_seq_t *seq; + + std::unique_ptr thread; + std::vector events; + + bool stopped{true}; + bool resume{false}; + bool repeat{false}; + + int port_ref; + int client_id; + int port_id; + + unsigned int current_index; + unsigned last_tick; + unsigned current_tick; + unsigned tempo; + unsigned division; + + std::mutex seek_mutex; +}; + +#endif //XMIDI__XMIDI_SEQ_H_ diff --git a/xmidix_vu.cc b/xmidix_vu.cc new file mode 100644 index 0000000..a87a378 --- /dev/null +++ b/xmidix_vu.cc @@ -0,0 +1,92 @@ +#include "xmidix_vu.h" + +#include +#include +#include +#include +#include + +xmidix_vu::xmidix_vu(QWidget *parent, std::shared_ptr grad) + : QWidget{parent}, on{false}, velocity{0}, gradient{grad} { +// setFixedWidth(10); +// setFixedHeight(10); +// setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setMinimumWidth(11); + setMinimumHeight(16); + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + setContentsMargins(0, 0, 0, 0); +} + +void xmidix_vu::paintEvent(QPaintEvent *event) { + QPainter painter{this}; + + if (on) { + painter.fillRect(rect(), gradient->pixelColor(velocity, 0)); + } + + QPainterPath path; + path.addRect(rect()); + painter.strokePath(path, QPen(Qt::black, 2, Qt::SolidLine)); + + QFont font{"Fixed", 10}; + font.setStyleStrategy(QFont::NoAntialias); + painter.setFont(font); + + painter.drawText(rect(), Qt::AlignCenter, QString("%0").arg(velocity, 2, 16, QLatin1Char('0')).toUpper()); +} + +void xmidix_vu::trigger(bool note_on, uint8_t note, uint8_t note_velocity) { + on = note_on; + velocity = note_velocity; + repaint(); +} + +xmidix_vu_array::xmidix_vu_array(QWidget *parent) + : QFrame{parent} { + setLayout(new QHBoxLayout(this)); + layout()->setContentsMargins(0, 6, 0, 0); + layout()->setSpacing(1); + layout()->setAlignment(Qt::AlignCenter); + + QLinearGradient grad(QPoint(0, 0), QPoint(127, 0)); + grad.setColorAt(1, Qt::red); + grad.setColorAt(0.75, Qt::yellow); + grad.setColorAt(0, Qt::green); + + auto img = std::make_shared(128, 1, QImage::Format_RGB32); + QPainter painter{img.get()}; + painter.fillRect(img->rect(), QBrush(grad)); + painter.end(); + + for (int i = 0; i < 16; i++) { + auto vu = std::make_shared(this, img); + layout()->addWidget(vu.get()); + vus.emplace_back(vu); + } + + updateGeometry(); +} + +void xmidix_vu_array::midi_event(const snd_seq_event_t &event) { + auto type = event.type; + if (type == SND_SEQ_EVENT_NOTEOFF || type == SND_SEQ_EVENT_NOTEON || type == SND_SEQ_EVENT_KEYPRESS) { + auto data = event.data.note; + auto chan = data.channel; + + if (chan < 0 || chan >= 16) { + spdlog::warn("got invalid channel = {}", chan); + return; + } + + bool on = type != SND_SEQ_EVENT_NOTEOFF; + uint8_t velocity = type != SND_SEQ_EVENT_NOTEOFF ? data.velocity : 0; + + vus[data.channel]->trigger(on, data.note, velocity); + } +} + +void xmidix_vu_array::clear() { + for (const auto &vu : vus) { + vu->trigger(false); + } +} diff --git a/xmidix_vu.h b/xmidix_vu.h new file mode 100644 index 0000000..35a7288 --- /dev/null +++ b/xmidix_vu.h @@ -0,0 +1,42 @@ +#ifndef XMIDIX__XMIDIX_VU_H_ +#define XMIDIX__XMIDIX_VU_H_ + +#include +#include +#include +#include + +#include +#include +#include + +class xmidix_vu : public QWidget { + Q_OBJECT + public: + explicit xmidix_vu(QWidget *parent = nullptr, std::shared_ptr grad = nullptr); + void trigger(bool note_on, uint8_t note = 0, uint8_t note_velocity = 0); + + protected: + void paintEvent(QPaintEvent *event) override; + + private: + std::shared_ptr gradient; + bool on; + uint8_t velocity; +}; + +class xmidix_vu_array : public QFrame { + Q_OBJECT + public: + explicit xmidix_vu_array(QWidget *parent = nullptr); + void clear(); + + public slots: + void midi_event(const snd_seq_event_t &event); + + private: + std::vector> vus; + +}; + +#endif //XMIDIX__XMIDIX_VU_H_ diff --git a/xmidixed.cc b/xmidixed.cc new file mode 100644 index 0000000..34bfdce --- /dev/null +++ b/xmidixed.cc @@ -0,0 +1,317 @@ +#include "xmidixed.h" + +#include +#include + +#include +#include + +std::string type_to_name(snd_seq_event_type_t type) { + switch (type) { + // snd_seq_result_t + case SND_SEQ_EVENT_SYSTEM: + return "SYSTEM"; + case SND_SEQ_EVENT_RESULT: + return "RESULT"; + + // snd_seq_ev_note_t + case SND_SEQ_EVENT_NOTE: + return "NOTE"; + case SND_SEQ_EVENT_NOTEON: + return "NOTEON"; + case SND_SEQ_EVENT_NOTEOFF: + return "NOTEOFF"; + case SND_SEQ_EVENT_KEYPRESS: + return "KEYPRESS"; + + // snd_seq_ev_ctrl_t + case SND_SEQ_EVENT_CONTROLLER: + return "CONTROLLER"; + case SND_SEQ_EVENT_PGMCHANGE: + return "PGMCHANGE"; + case SND_SEQ_EVENT_CHANPRESS: + return "CHANPRESS"; + case SND_SEQ_EVENT_PITCHBEND: + return "PITCHBEND"; + case SND_SEQ_EVENT_CONTROL14: + return "CONTROL14"; + case SND_SEQ_EVENT_NONREGPARAM: + return "NONREGPARAM"; + case SND_SEQ_EVENT_REGPARAM: + return "REGPARAM"; + case SND_SEQ_EVENT_SONGPOS: + return "SONGPOS"; + case SND_SEQ_EVENT_SONGSEL: + return "SONGSEL"; + case SND_SEQ_EVENT_QFRAME: + return "QFRAME"; + case SND_SEQ_EVENT_TIMESIGN: + return "TIMESIGN"; + case SND_SEQ_EVENT_KEYSIGN: + return "KEYSIGN"; + + // snd_seq_ev_queue_control_t + case SND_SEQ_EVENT_START: + return "START"; + case SND_SEQ_EVENT_CONTINUE: + return "CONTINUE"; + case SND_SEQ_EVENT_STOP: + return "STOP"; + case SND_SEQ_EVENT_SETPOS_TICK: + return "SETPOS_TICK"; + case SND_SEQ_EVENT_SETPOS_TIME: + return "SETPOS_TIME"; + case SND_SEQ_EVENT_TEMPO: + return "TEMPO"; + case SND_SEQ_EVENT_CLOCK: + return "CLOCK"; + case SND_SEQ_EVENT_TICK: + return "TICK"; + case SND_SEQ_EVENT_QUEUE_SKEW: + return "QUEUE_SKEW"; + case SND_SEQ_EVENT_SYNC_POS: + return "SYNC_POS"; + + // event date type = none + case SND_SEQ_EVENT_TUNE_REQUEST: + return "TUNE_REQUEST"; + case SND_SEQ_EVENT_RESET: + return "RESET"; + case SND_SEQ_EVENT_SENSING: + return "SENSING"; + + // event date type = any + case SND_SEQ_EVENT_ECHO: + return "ECHO"; + case SND_SEQ_EVENT_OSS: + return "OSS"; + + // snd_seq_addr_t + case SND_SEQ_EVENT_CLIENT_START: + return "CLIENT_START"; + case SND_SEQ_EVENT_CLIENT_EXIT: + return "CLIENT_EXIT"; + case SND_SEQ_EVENT_CLIENT_CHANGE: + return "CLIENT_CHANGE"; + case SND_SEQ_EVENT_PORT_START: + return "PORT_START"; + case SND_SEQ_EVENT_PORT_EXIT: + return "PORT_EXIT"; + case SND_SEQ_EVENT_PORT_CHANGE: + return "PORT_CHANGE"; + + // snd_seq_connect_t + case SND_SEQ_EVENT_PORT_SUBSCRIBED: + return "PORT_SUBSCRIBED"; + case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: + return "PORT_UNSUBSCRIBED"; + + // event type = any (fixed) + case SND_SEQ_EVENT_USR0: + return "USR0"; + case SND_SEQ_EVENT_USR1: + return "USR1"; + case SND_SEQ_EVENT_USR2: + return "USR2"; + case SND_SEQ_EVENT_USR3: + return "USR3"; + case SND_SEQ_EVENT_USR4: + return "USR4"; + case SND_SEQ_EVENT_USR5: + return "USR5"; + case SND_SEQ_EVENT_USR6: + return "USR6"; + case SND_SEQ_EVENT_USR7: + return "USR7"; + case SND_SEQ_EVENT_USR8: + return "USR8"; + case SND_SEQ_EVENT_USR9: + return "USR9"; + + // snd_seq_ev_ext_t + case SND_SEQ_EVENT_SYSEX: + return "SYSEX"; + case SND_SEQ_EVENT_BOUNCE: + return "BOUNCE"; + case SND_SEQ_EVENT_USR_VAR0: + return "USR_VAR0"; + case SND_SEQ_EVENT_USR_VAR1: + return "USR_VAR1"; + case SND_SEQ_EVENT_USR_VAR2: + return "USR_VAR2"; + case SND_SEQ_EVENT_USR_VAR3: + return "USR_VAR3"; + case SND_SEQ_EVENT_USR_VAR4: + return "USR_VAR4"; + + // NOP + case SND_SEQ_EVENT_NONE: + default: + return "NONE"; + } +} + +QStandardItem *get_channel_item(const snd_seq_event_t &event) { + int channel; + switch (event.type) { + case SND_SEQ_EVENT_NOTE: + case SND_SEQ_EVENT_NOTEON: + case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_KEYPRESS: { + auto data = event.data.note; + channel = data.channel; + break; + } + + case SND_SEQ_EVENT_CONTROLLER: + case SND_SEQ_EVENT_PGMCHANGE: + case SND_SEQ_EVENT_CHANPRESS: + case SND_SEQ_EVENT_PITCHBEND: + case SND_SEQ_EVENT_CONTROL14: + case SND_SEQ_EVENT_NONREGPARAM: + case SND_SEQ_EVENT_REGPARAM: + case SND_SEQ_EVENT_SONGPOS: + case SND_SEQ_EVENT_SONGSEL: + case SND_SEQ_EVENT_QFRAME: + case SND_SEQ_EVENT_TIMESIGN: + case SND_SEQ_EVENT_KEYSIGN: { + auto data = event.data.control; + channel = data.channel; + break; + } + + default: + return nullptr; + } + return new QStandardItem(QString("%0").arg(channel)); +} + +QStandardItem *get_data_item(const snd_seq_event_t &event) { + switch (event.type) { + case SND_SEQ_EVENT_NOTE: + case SND_SEQ_EVENT_NOTEON: + case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_KEYPRESS: { + auto data = event.data.note; + return new QStandardItem( + QString("note: %1 velocity: %2") + .arg(data.note, 4) + .arg(data.velocity)); + } + + case SND_SEQ_EVENT_CONTROLLER: + case SND_SEQ_EVENT_PGMCHANGE: + case SND_SEQ_EVENT_CHANPRESS: + case SND_SEQ_EVENT_PITCHBEND: + case SND_SEQ_EVENT_CONTROL14: + case SND_SEQ_EVENT_NONREGPARAM: + case SND_SEQ_EVENT_REGPARAM: + case SND_SEQ_EVENT_SONGPOS: + case SND_SEQ_EVENT_SONGSEL: + case SND_SEQ_EVENT_QFRAME: + case SND_SEQ_EVENT_TIMESIGN: + case SND_SEQ_EVENT_KEYSIGN: { + auto data = event.data.control; + return new QStandardItem( + QString("param: %1 value: %2") + .arg(data.param, 3) + .arg(data.value)); + } + + case SND_SEQ_EVENT_SYSEX: + case SND_SEQ_EVENT_BOUNCE: + case SND_SEQ_EVENT_USR_VAR0: + case SND_SEQ_EVENT_USR_VAR1: + case SND_SEQ_EVENT_USR_VAR2: + case SND_SEQ_EVENT_USR_VAR3: + case SND_SEQ_EVENT_USR_VAR4: { + auto data = event.data.ext; + auto len = data.len; + auto p = static_cast(data.ptr); + + QString str(""); + for (int i = 0; i < len; i++) { + str += QString::asprintf("%02X ", p[i]); + } + return new QStandardItem(str); + } + +// case SND_SEQ_EVENT_RESET: { +// return nullptr; +// } + + default: + return nullptr; + } +} + +QList event_to_row(const snd_seq_event_t &event) { + auto tick = new QStandardItem(QString("%0").arg(event.time.tick)); + auto type = new QStandardItem(type_to_name(event.type).c_str()); + + return {tick, type, get_channel_item(event), get_data_item(event)}; +} + +xmidixed::xmidixed(QWidget *parent) + : QMainWindow(parent), + ui{std::make_unique()}, + model{std::make_unique()} { + setAttribute(Qt::WA_X11NetWmWindowTypeUtility); + ui->setupUi(this); + + tree = ui->treeView; + tree->setSortingEnabled(false); + + open_action = std::make_unique(this); + open_action->setShortcut(QKeySequence("Ctrl+o")); + addAction(open_action.get()); + + jump_action = std::make_unique(this); + jump_action->setShortcut(QKeySequence("Ctrl+j")); + addAction(jump_action.get()); + + filter_action = std::make_unique(this); + filter_action->setShortcut(QKeySequence("Ctrl+f")); + addAction(filter_action.get()); + + connect(ui->open_button, &QPushButton::clicked, + open_action.get(), &QAction::trigger); + connect(ui->jump_button, &QPushButton::clicked, + jump_action.get(), &QAction::trigger); + connect(ui->filter_button, &QPushButton::clicked, + filter_action.get(), &QAction::trigger); + + connect(open_action.get(), &QAction::triggered, + [&] { + auto file = QFileDialog::getOpenFileName(this, "OPEN MIDI FILE", "", "MIDI FILES (*.mid *.midi)"); + xmidix_file midi(file.toStdString()); + load(midi); + }); + connect(jump_action.get(), &QAction::triggered, + [] { spdlog::info("jump clicked"); }); + connect(filter_action.get(), &QAction::triggered, + [] { spdlog::info("filter clicked"); }); +} + +void xmidixed::load(const xmidix_file &file) { + model->clear(); + model->setHorizontalHeaderLabels({"TICK", "MSG", "CH", "DATA"}); + + for (const auto &event : file.get_events()) { + model->appendRow(event_to_row(event)); + } + + tree->setModel(model.get()); +} + +int main(int argc, char **argv) { + QApplication a(argc, argv); +// xmidix_file f("/home/david/code/xmidi/test/test3.mid"); + xmidix_file f("/home/david/Desktop/FALCOM_MIDI/FALCOM_MIDI_ARCHIVE/e4g03p.mid"); + xmidixed window; + + window.load(f); + window.show(); + + return a.exec(); +} \ No newline at end of file diff --git a/xmidixed.h b/xmidixed.h new file mode 100644 index 0000000..f2239c3 --- /dev/null +++ b/xmidixed.h @@ -0,0 +1,28 @@ +#ifndef XMIDIX__XMIDIXED_H_ +#define XMIDIX__XMIDIXED_H_ + +#include "ui_xmidixed.h" +#include "xmidix_file.h" + +#include +#include +#include + +#include +#include +#include + +class xmidixed : public QMainWindow { + public: + explicit xmidixed(QWidget *parent = nullptr); + void load(const xmidix_file &file); + private: + std::unique_ptr ui; + std::unique_ptr model; + std::unique_ptr open_action; + std::unique_ptr jump_action; + std::unique_ptr filter_action; + QTreeView *tree; +}; + +#endif //XMIDIX__XMIDIXED_H_ diff --git a/xmidixed.ui b/xmidixed.ui new file mode 100644 index 0000000..ab8df82 --- /dev/null +++ b/xmidixed.ui @@ -0,0 +1,89 @@ + + + MainWindow + + + + 0 + 0 + 640 + 480 + + + + XMIDIXED + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + + + OPEN + + + + + + + JUMP + + + + + + + FILTER + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + + + + + + + +