From 51f705d718233f006b7a0ebb32a9cb51cf54ef8b Mon Sep 17 00:00:00 2001 From: BEN ENGLISCH Date: Sun, 10 Apr 2022 14:57:43 -0500 Subject: [PATCH] XMIDIX --- .gitignore | 4 + CMakeLists.txt | 70 + LICENSE | 674 ++++++ README.md | 52 + cmake/Midifile.cmake | 11 + cmake/XMIDIXED.cmake | 22 + midifile/LICENSE.txt | 23 + midifile/include/Binasc.h | 159 ++ midifile/include/MidiEvent.h | 71 + midifile/include/MidiEventList.h | 80 + midifile/include/MidiFile.h | 322 +++ midifile/include/MidiMessage.h | 176 ++ midifile/include/Options.h | 156 ++ midifile/src/Binasc.cpp | 1969 +++++++++++++++++ midifile/src/MidiEvent.cpp | 284 +++ midifile/src/MidiEventList.cpp | 624 ++++++ midifile/src/MidiFile.cpp | 3395 ++++++++++++++++++++++++++++++ midifile/src/MidiMessage.cpp | 1934 +++++++++++++++++ midifile/src/Options.cpp | 1219 +++++++++++ resources/MIDI_LOGO.svg | 63 + resources/MIDI_TRAY.svg | 44 + resources/keys.png | Bin 0 -> 577 bytes resources/resources.qrc | 7 + resources/shot.png | Bin 0 -> 60030 bytes resources/xmidix.desktop | 10 + test/test.mid | Bin 0 -> 18585 bytes test/test2.mid | Bin 0 -> 26155 bytes test/test3.mid | Bin 0 -> 120548 bytes xmidix.cc | 43 + xmidix.ui | 272 +++ xmidix_config.cc | 78 + xmidix_config.h | 55 + xmidix_config.ui | 206 ++ xmidix_file.cc | 94 + xmidix_file.h | 35 + xmidix_piano.cc | 129 ++ xmidix_piano.h | 54 + xmidix_player.cc | 297 +++ xmidix_player.h | 92 + xmidix_playlist.cc | 83 + xmidix_playlist.h | 38 + xmidix_seq.cc | 229 ++ xmidix_seq.h | 76 + xmidix_vu.cc | 92 + xmidix_vu.h | 42 + xmidixed.cc | 317 +++ xmidixed.h | 28 + xmidixed.ui | 89 + 48 files changed, 13718 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/Midifile.cmake create mode 100644 cmake/XMIDIXED.cmake create mode 100644 midifile/LICENSE.txt create mode 100644 midifile/include/Binasc.h create mode 100644 midifile/include/MidiEvent.h create mode 100644 midifile/include/MidiEventList.h create mode 100644 midifile/include/MidiFile.h create mode 100644 midifile/include/MidiMessage.h create mode 100644 midifile/include/Options.h create mode 100644 midifile/src/Binasc.cpp create mode 100644 midifile/src/MidiEvent.cpp create mode 100644 midifile/src/MidiEventList.cpp create mode 100644 midifile/src/MidiFile.cpp create mode 100644 midifile/src/MidiMessage.cpp create mode 100644 midifile/src/Options.cpp create mode 100644 resources/MIDI_LOGO.svg create mode 100644 resources/MIDI_TRAY.svg create mode 100644 resources/keys.png create mode 100644 resources/resources.qrc create mode 100644 resources/shot.png create mode 100644 resources/xmidix.desktop create mode 100644 test/test.mid create mode 100644 test/test2.mid create mode 100644 test/test3.mid create mode 100644 xmidix.cc create mode 100644 xmidix.ui create mode 100644 xmidix_config.cc create mode 100644 xmidix_config.h create mode 100644 xmidix_config.ui create mode 100644 xmidix_file.cc create mode 100644 xmidix_file.h create mode 100644 xmidix_piano.cc create mode 100644 xmidix_piano.h create mode 100644 xmidix_player.cc create mode 100644 xmidix_player.h create mode 100644 xmidix_playlist.cc create mode 100644 xmidix_playlist.h create mode 100644 xmidix_seq.cc create mode 100644 xmidix_seq.h create mode 100644 xmidix_vu.cc create mode 100644 xmidix_vu.h create mode 100644 xmidixed.cc create mode 100644 xmidixed.h create mode 100644 xmidixed.ui 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 0000000000000000000000000000000000000000..9b445d1795c53f412caa99d923f4597cc7aed4e3 GIT binary patch literal 577 zcmV-H0>1r;P)EX>4Tx04R}tkv&MmKpe$i(@I4uA{G(pkfAzR@C$L&DionYs1;guFuC+YXws0h zxHt-~1qVMCs}3&Cx;nTDg5U>;qmz@OiqZeZ`vy53uO2Tt|-NVP%yBN>%KKJM7R&y2ud?N7-Gt3I{I`PzI z#o)Y89A-sXB|aw}G3kQDk6c$=e&bwlSm2pqGo6|v4ik%oHdfl06-|wJk~perI^_!) zmsQSNoV8MwHSft^7|Q9(OI)WJK>`a{gai=^s@OmY6~t)ONwJWk{kVsJ(D6&;QpmLd zMvi%up+R>1;D7MDTPr^??k0ueK*x)1e+&V^U7%63?eAmTZkz!AXW&Y2`O9@+`jhlp zON$->J=?&=bxTwBfXf|V;K`6p*^&G-g?t`(KcjET0)4kY*Q(oFb04P4N+bgSO1m2fv1r*N P00000NkvXXu0mjfN^ + + MIDI_LOGO.svg + MIDI_TRAY.svg + keys.png + + diff --git a/resources/shot.png b/resources/shot.png new file mode 100644 index 0000000000000000000000000000000000000000..6fce2c5fb731f0cb679f09c65e56d2c42c52d287 GIT binary patch literal 60030 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%0DgB1uP2ht19Zf^ih2i3?P1);4!q#47d_c4C(?@li zUnjpEmvdx2E(6yDDoubZWtskN@meV)yoDB@*9T0H68^Xy2ko<1m0cP8fHrdA>Wlx- z*)O1IZI|X@TK}@J{z79lO$y+?( zd1U>}Of)PWM{CBwy4#t|*L{~ajl1ikr8ohKQOEjG(K8I+h?zM$D2>YgNPZ;rhX;Ae zH?*HDY`Ec5i@~BK{E@?DFc+*L2H1TC)I74eo|1-FdUthQN|KRTyg#M~;oXn$sY45@ z{GN>RrzNcqsQ7&`l#V6LIj z7akc!m@0Rws?#LyQ=Wb{6XtIg3hSqax!^JHYquREvhzatwUyOA=%AqJbrZ;+UtFz7 zh{S-u_iUEdl;8YHb3-i+#3xC|3rmfb3Kj5Xr;E@E({2rw`Zxtf~XV4X6bA664p zWB8f4sb*2;?hagGaa%|4?ryKXyE*QkBg#njQ7gFMl}@9k@Vdnp}A>(KwNYN?TKnb3W1mKtg}-gH=7JAZw$hA=zd#Cj;e51JUXL%Z4)`D zp|Iorx87UXMaRm}W0_}EN?ic$f&)xRXxI-U4V_Bn z4**|odLdm^T^+tM@#^}V?@6t6V-@ueqmLdVC#iBlXN0@;yG+8;j|%AD_>1W1xRr7^ z8((yL*?*ILXZz)zMlkmKV3t3<0u~DL=Q@5c%mkL}em`YdL4h3ZuB$&IAx2imAC*~d z8_^y)r1O^s6s&sxL2W~7JsgV z;fm6Qbk<&}wGMnz@!LQ{ZbJBqj)hjxlu9+H0TOpe+T5a zJTsqqxWR~7d30=FP>E_Z#HEy+kilU;GGwXACv$iEL4xh$dY!Q@76R94hEV)XklcVK zHZy!Q5-_yK?+#&Xwioz1`?;B?<#3q#jF|7D5G8bp`jV#Ubmbt>Zs*%(pSF4GRTAC_ z9}%N94g&CwH1g&`*atkuU(P>)C%yCl57g?mdeHgs336J+wb{?LN2F4v^lqiainoK8 zp_11klrT}`if{}_z?#F8n7Lo?tjBRH3MI^KzjbW#CHMZY0$?M*i{R>Q$1w*R8OzFY#x@^&V3l z?KB2q0I1<|zIf7d0NcaX!P&D*%h;1889xkOUKP)}ep8+~;7m9F95wz>#i{M&Fr-E` z@}$_J9?3ENzNPAi^g|)TQZiQf@JN!pR!f<_hljlPH|pSUFM-);H|F+g{s~fSKL_2F zE2$no73ec!I<*iG8{8Aujz1Z|%7TtZadzm&vGvLMe2AIJs4|1L4EK*lg{LaXLZbk6 zD5UUUS8q0D0MN&5rP10&hdba`Qp>gIfm#6Ayo{GzjDb3yc@YE@$Nb<6b2vuy(9~y= zJ4W1yT4gO8ig|IO)_#9zg0iIh!hb&!Ov+{^nU@p*nmg_j?R#`y&85Z5Oe>pH`SCmmm2TFT}U5=GEu zXQ1CTd4mv?xS_Zo`$1?E0r};W5iG7l9$f&j$~(0jeD9q5CHwFW`f=H5!N5m|A&hr~2&C|?RSAnm`Ya>~IkOg& zh;(nhDNndI4*TuHWRe87Js?C!wu=o0$|e_d#l}8RPWi`5`O)OXkq$S=_PMbm>f^G~ z;{tJ5(=wwTS$mnOPZiSvej025*Tas@Fx6r7`#nU#yuTm4)W!UfK8_jpVU7t`}xK8mn$lG>V$GwCg(nme81BFt1y2sRM+rN7@+6#oTcCPIM4fa z!%V?2F<+8d??>^u{2SeT{0`9El=fbhu!y3Vslh*?3-NO;QO`m$%*6BW>!GbZX4UJU zJ%%(Kaxp+zbwhDYO`BTFVMJDYGA(W^7K$Lqxs{RE$8B@}qSN!Pb*q0Yqq(FYK5WiP zz1`9qbhJFC;GS3O2XO+41?Y0wq$EIFTCQB~Zm>7^x&cN(V}8PKicp4}*7pY?K*2YNBgRvd0h zr#HLH?Mrc#Rr0#jtPjsvgn3?sUb76Ej62t(d>mbBaoS2JasNZdX<>t%*;)zx@kq1u zwBgY9d8*yj(4*?H%QOfji9L6l(j#l7nNAVlIaW@t`@CaS$3*7w)K=(h1Wk(4cST5f z=5wuRj*zR>1bkDH??C|A`pg^%dASw{$3AWU^s=N-32NhZ%FYpDFxAirM};uCIl*c; zbAhtHe_*bsY$Af1O|48rzfudKzB1wOx>@FP*!Qdr*nH?{Y`zuDb|e?5zJ2t0%V9Ws zb8||Fx+=J0+*^H|p>^&)s`aH=rH$D?U{m$e!1`kSQ$~~{gN562uV3!uQn~1oC?e$om0fR7KXBfZ zi{^~TpZzuN;{Z#L>m$Q}K90;=3ukTU^uhouwTUI5_xR-GU(;Siz`VNKe)-r#hR}nz zI{?zk?EZNU=;N>JLAsO`I=a-5zu|u4l&~0X7?ejcSK+SL^KEwUr4;LX6mh-U%%_LT z6?*_b>V%>*yJP>WcFM5XOTPRGky#mU>HN#2AT(0PfkoSW)>?wlMc2>}S>WlVf)~0v zeWcg9zFUo}UEgJlL#E=F-N05*Zcet#PN$d52ULLGN~`GG>+adp+Wkdsv9o!^jf&)3 zv+vr);m>6~JOIip!P^c{CRNiKH3M|pk^00Xn9n89I>@wNTm|h>&z>-&Ryp5DP7f=4 zeyli@<#Q=~LVV~jTNd2l^Yx2x3O=VP7gxC;NLUVn!PP|GG~ueM+M4O*<*m`X9fn#0 zG_J4iQs~O^l!B|9LF9*|BI&I}U2=YH9OGX?qk*BalEZ9s+<|c z>um9(bOy!V>mzbU`Io$|S{EVtyJf!Pr&l=tpbven&!5bQYy#~}Gk7vJ_GBOO@a_~& zOP#4^UcU((bB#&4+{up&e7!+nI}uJV_drF1{GDFlsBiQd27_}uTO>KxQ`MF|CB#wE zu<>)YcZta7as#$L@D~bvavgI8;cW{mntV%@OLe}AAq0SBF5UCZV!l4Z90E*VA81`{ z!Wf^njS?1Ct7xPoEK`?)kUIS=T1mcdw>7gmyJ-@VZXc4O)_}bj0ffnIcMmNU@@ssz z9lP32XY1@Y_4n)rjjFv7*3R2F008{(j|Seh22fF-g%9q@$la=C^3!p}vR*T<@aw^n z?gHLS_UUGz6%2cUK>yx_6+gMW;&?gINM8)NK~Y5$qeik#JRG0%S5f}j0!~G-Q)7QL zkg*M?ii~Sqjfr2?>+(vvH}Xar4)V7Mwv|S&?)_zASVcJsllE!6+c~@Z2VSQ$`PJte zAt5eF?v0FVu3L3+aFo2KVgU!iehZWw8uO>yZx=-d)tu~#GIMmsj1fq?U)lZYxT3WF zFlZ$mfj~jewe=OKj?P!U5j!|Q8s(mU>ZR}Lym4Aja(TpUv;s!P?VksUQX^^#~bX!p`o4)xQj6la(ed)(1^r8s?tn$*=Q z`?MSsGyd>8iIDCv7KG-Ic?~|ZPuPx;E@*TnBn>_GIgYw$x9Dj!lIomC-wKfFfAI}A z69V7A>oRzjITQAV3r$9&?X8>E*fux+3csvXnE9oT+xKbEW(M|q4SGLAhoaq5<{F_q z#MAGbSv(?Qa_98+09Lq|*+uE*G~@^Bo`654GtOCrYb(Q&-z3&$Yn#GuW8=CRW)ei@ z_=$aQkWSEzr7i5u)VlqC!Z}j!a%aX$^#D3JwOjMUhQ1MQ#ff^mF}FtVxm~RADcF8D z82DYH&;6%sHm@KSW0^VIL6sgxS*ph1Swd*pWvjqxBZnr7mR55m!$Rqs=kmVfSMeTk zR+LjUo(EKLs0^q-cG75bCEV&rH?ETOdb77Hr_bVjpJj=t-zQo8JCdefrMzy6Aak!! zzoIR^)W@#xBKfbh_T4jqHi9EVjjOl6v!NwT#G7NVJar|T;h=+{cTihz z=^=e1L!ZL@sso5h;S1 zIXE@chH)C(ldOx?gfAD3Xd&B_W{Pkq`@S*ZnS39w4mHxZ6ODtDd?H=vh^W8!yhEe1 z+Df?^PGtPbbh~NCsJn(R>f^-j?|)VdE=x`y;FpKn?_4npHqyjyvd_KV?=b^EVay4( zo~5v#T)aK3Q?%YbX_T~QmT}N?w!eBFXG94KP5DpN`#gCZZHalR#SQih_!0E7!Xv_O zm*CGlW2A^u=^5S>mn1VX5B|16mydusjC4OG1XLAo4CeE#SoxgPp>JJgG0Wq>v8wag z_&m&Q!ld`e6J$2+$+yO7`z{pUs>7VRjGy+k5oP~7xI;z z9n|Ld`U1GXeI76$S0eT#U^@tPWX=|<3+e)|OV6HR40E#%kQy0j5n(pwFw&||<)r3rKGoF6QTY^(c@@b*(FQ+ho(*Phe$YE@55+HhQ091o7m zLI?cjHiq=7s}9kQ-5L>Ltp8EwYu0zt1m0id%fxgt5B3Cmghpbmd@+qW8Z%F!#a*2bFX3TkDqpLCMUM3_=Qnsft2W#r5W)R;x*gb3Zelee)36a$|nZB zi>wBgM)Orohq3qq2ufP)zn-KkJ7fK_4V^Fv8%U_D^5l#)V`!lOWu*rpT7S$Pl_#ph z8W>xoPk+z}q1PW@5Es1r(I` zO<+%$;1~R$R-90vW`#617JSNZvL%vNC} zc3sV{)yKY&9NJ?NAq_x4#77sI$#@2q=q{?MM2jg?PUpK&#$vC+5Fkb;qJyaEFe-Z8KJvsHkcna6mZf%bAx*B+yqbCQ-hQDZRV z^M{V2JLhVjP*`-Vklz)+`g-;nyT3%vlD}9~Q;xX=d3zO{8=3ipMt5oia|cHAElxQd z0P30(c=210oHz=d8>9n9j(D8&R-gUY8I(f-_-DFB(UlsP8vPKvz1ggEdqxo8+v~+*DM!HjYkL zsE7OySFMcYWNuov-^z$9&ZaIS^vJgq`!K=;5T$LxWlqm?}Yw4P1b4BYx`^;WmZ zboTmo}R8@es7kAYCs%Uu{_W(dE{eB%KD14>Y#!CbkYBuM9kBM*y=pmpDsoLx zj!t!Gq85pX4?jEsd_$G#dZVijBy-DZ1-yS(=8 zClEvy~+9INM?&{nwQsf zre(B`uMj&j9t$Yyfb@O9%DV7nIobyAg$fJSidbL2KAl}?%iSaD2!;&prLdPZjbfG( zXgf%B{tZ=F@q4sc6{WN);`LGL2jh)Ob*vQ~_qq|g4a}p{)n8=~pP@)bb!sWfigPCQ zi>KAK74bo?UXk6Oxne1)?zu|mLB)jAoFj+G>i9CDi`)3Kg&sewZ?MZO%@U~k!w<=L znN%Q39(Swv{(zV3u_k57{mPh0cS!xG3>q2YE6qB)m@Zu<(wYWIO+vV?lPsa}uG5pG z$~|u6A=L-37gH$5SsX+l4~?vZGuGOA7D|uPFDiD5VGKh@H^$&ycT9Koh7N};gQgS> zti|)0JXBUe>bP9a$R(lhqwz(uD24%D|c8)_>&(EvdmRK%y%F>D!A}vKJ<{4rb z_Ev|AWw#FdwHgwQ93A2=`SitinbC4uO0T-Llf5qZrS&sIG*_H1aCX0xbe_Y)tOQVm zgu*Im%f=lsEa&jZIJ_KHCOXgO^TaYP-)NZ(ZHx_hoIuY{Yk3~mIQC-PVp+QEf?TCh z`pAU>$Ru#HvI!cvg}mq0n?r@TCqJbzfF;ID70Wl8f>R0STP+^26T+|;Y(k&gV) z4u9hRZxHv4>L_ZY?~?PV=yX@bRM72hK19e%VfpmHGV*CP!vZn>UslAZCvIZ^Cc0U zXMyS&n*^DY&M)$3x~eylUg`w)w#MnGfcwlPYH56i`c=a-FB+aIAvuRWoooKwJVvcTuLdA%Fj=M{?{Qx6$3j_Y`p-Nlg(=RbvT3;uHm z*EA5O6n<4mxdPqJ6#W)B2qzAhpH{ZdDN8y!aKAfQPtw&I(BV^3XEm$UdMNu+WRAWT#Se(iO<$zkcV#TFi#+3g@`vJ!sn<;F=OSY3`r_mACLD9uLz%5;{%rBlR+ zM3#^}soqj5Chg`@rT4|m2vL3epL4UGTWz8@VitIQ4|*?F6{9%nrdpMr|85y z{#!j={`p5Z$S1j9vSpB;gPFOnn;X6ilkjqzZuRlcTolMa>mqROP|FN-vFRroqAa-J zfP~0*Y6o$36UOKasWb&k!&5{aTnh*eHZWemTdl=8td>E`ms7id$yGZPz!K%faeMUg z@^WVg?8&7M3&q^AxEa-kDflqC8I(GZ_J!nIQtV{u)$R!X!&O(C&xgH-UtjL~9Cn`> z0Ywa~@*3ok>>S1~yMO(-B_iaq(`emLiA_O)x$QiP=1vV2GX0ev8|z(?=n3nP{}ybk zq#7hR0gU4g`_#v16?MI*-??;y_u!bDY)b#f{w_*qO<+nD1QEcv^j0}!vKacwFY@nD zia9Q*xewH5N$tat(OfCQ@zvtVJ7@W(^8H9J359+;-_%h&-YHNfU!$WM0;P-fxooxNKBb>^eOhEOHeAWB1r53#lL2>BlAbdQ>NDzfbUfr3nsO z5poDj%y|Ryn%h<&3)dNtMSu)=*y7XNwRN%c{_)Ae?}>B>e{kD1iTkjhnYY5@&cziD z{bRR7;z$@88DRFkETOSRuXEEkraO!oz8VTctkt}Vozc#3cYUwd_OyDjwKuA*qBPiR z$yv6>A++ptYFg2LxV(^Kf*`8dj9+5eTs}$3-2PxcJ1%V1(zdI$k>Z}Fj%Y$It<~zK zsmg-X(UA1AQ){?q@$CG1OzhH+%w<*nyd~741PMp)a(3Z4ovAj{I2+rw#7MBD=riTQ3rX=;!L(He-IqmD&rlsH3cOJB3r6C z*@0V{1gIVJ@MhIA_%v*HE$YPET*cjSjoHljmS8^YS+6$P$wha^*v}U=YPjCEVdIto z%bV6px-aD4HX}|t4-v8m1+^x_W0Q`(oX<+cAkGCEB0VpXYG;~)k58U57h|ye`Ekcw z;#=M(B=Z@=?GM^sTmzDIdR{#=+c{$0!p%ZcYb;zE-6hyRnKuCC@ad zEw`_T`>=Pq_~bt+>|S#x=yuXD zmyw9xK^(tN0VM{9}K=bpIJa{_G zU|1EIvP8fw4N&3=&JrT8$eDw*))NTM^={z3%Ju)vkGg&13=d6*&!S&%g8Y zi*8r&b@$-kuwO82oTakJptc(VrO!v*<5nbJlH#IggMLqmA4vcmO&&q7 zFC%Mr?H607N+*+eN2QUkE)eZoGani|*#SglJg-A*iR=xFB)oPfKRNDzp3*>f_XO>6 zC4GcjD|5QiN}Xy4NmbO;^@ZF-p|@?nMWN}vG7U7~TdeP|&s-UT8pvM7gTaz|ALaKm%k9{MhOX|BAy|m3%YR`^t;? zT~1)N;unkx*kR8{AivV(2URsl2sBXz-XLYX7Av+Qq5NU0k9OlmUYb=n7lx?4?XBz1 zHN>fUWr24p)&gn(nB9ua8+-Y2@kWa*y~PWn|#j z!4o6-U5il%qZt3k%cH9(+}*5At!#!PY8R6p(}>Z=Tv@IleSL7vB&s}jQy*6oMn zojMer9(v)Jb-Hlg^lvjuvv5tjM5+WEei5EPgLKGJje_V4`<%ifO+K*A-eEnk!gxlGD1q%_8Hp6OAzjqvs{sES3_RO0YO{uBQjN62cQ%NP*4@ zsFGC!9&CPA>c8KP)?Jlh-geXRyhFxJkH`I7WPyuZUd2&dybX0Hv!6aQ2!Rz_79BBV z3_9zRJ?auy6jb_N-^4dlkbDoCh>8sj4l^KHuEe_}H3<9kl>$YD#9;LEd~TahBWN~3 zJ_9`Q!A4=cS8yXdP_;W%GFoVs{|NR9PhSwqdg;?T|0N2U@go;f^cSs?rUyzE7Q`3H-u^9z5;;NU$gV!sR(845&3NmEZ?%uy zMODG5gUi_mn>c+BVrS|{_@r)?EFaN(lJL^X=y$`G*M{n+OG(_UYcL_!iY=zY&<&|@ zkl1MV)x~S7K-qu&b6-%;QzbM#+nxkG|BC@JJAhL&TCAv&N6GfsT}cK7r%`XH;f^u# z=G-Qz^MjM5z#TGpWo^4NnPF4ke)}WD?qMvcFyaljjIg2o*%I7&xk-&Ywv;Z$P62+0 zhE2eV9fbMFj|mfQg54jG%473z@u2DBj8AUp{F0^!XYZ~x6u*4&#mDly`S1ZZgpzFU z=#SX2ueaVn234qv*iFea6=H`uuYza%k}V+VeQ5o^7Xv=Rc-Hj|n;(wY=VP*l{0*t! zZNk1=)u?|v-5}plWfGdc#jutfIAI1_C>${T6XTQ=i+FT zvki{GqCELD6pyTnUQ|`K!hE7hNUKn`T*DFz3E;Jv7!5eEY`6VvglYPe!D}dU5d(RGw$fPyECg+Kg5g*7c*exbiPAG(ylFTzdX;z z7sl6lJ9u-yTFV+Ts_Y*ooNLd$o-Y|pB&xmc zoV7nax+L1;jIOd`guJT)YH>D&9Y3t;I&J)61lzp79v<~BwjdS`%?3)OKgR?SDi|QEzXfZaMIZB?y51|Ep$-!o+eGEejh02&^!Aa>#byq`iP19Q2WFHee zCT`3Or%DBhLUVlgyx3Q+2xYai|88KCv_l!^o$B~uQwQ<1A-9!o8VM8UDkZ6;DRG`X zB(;W;G;d9s)In~&jx27lbIB1;AbRk+X`kddGR9I;2jS5v6M8l+=UL#;&qiLPL?Ys= zs%hKjn(Bv!N!aQhaH!j}I3Ba)ZDDUiB~BMx(SHsA_&RY2#DsB1MAGp(UnD~7i(n?B zhuOfCYf<7%^d(D`O{M|F%4RbSJD|1^Xk}Yh*w3Fb0+Be=hDneR zoZ@&1p#y@x=*nl^L8vMP{_lXO$0#y97xOG%jrGBRQx9da!zJ}w4M&q*z=!rlWr;G|@ zBhjA-Nd?GS?LRo4SBr!9%@*fZtO-g{tBX}uj--v`p#Q;>ZAvQ32@$0)(z~O-E>$~O z`NM;Az$U$o$^WkTwd!qkMm{<|Zzapu8k!9~akqD^`?R8$F>BB(Tni(vPzP-ZcC{L64tIyvO?)d(2q;`Be zOY}a0{qDe)M~HW0)@9#Tnx7#%IE?rO&`~TMr3JDpZn1WZHt=dE$}u1#`e`Nl3h{cz z&j0P{K7syt1$LBHa#vYTLTwpxE7>xs@h zr15_I$>P;pvVS{SY8@%aSk~-;P_sLP>dWmh5?fUeDjl@&KVk3>(ti~O$1odZmqSh)GL36E<6SmWtdy3LBTq&J(i1uMCvyBL@ zxHP}T0V>^QOH9wa{V3b8yWuGQu56R+@4g`sJQgh26z&lfV$$c}aO$dJ3DU@n43XDcq_a$)y-UYi ze*zNIS-~ZQ0emA@E}wZP5||N)5l?UppH7MSW>e|}XHM18DXcwNbzPeD_I)|6tQfdw z>W8X%So2wagpQ=fx~x_P``oVYUhsX= zR=cxG+wh*zbM^N-6^RuS_b;v}`0IpiGm)O(_oe)#7v9lO;%Fb9c?r?{y}}6;5x8&UGq1mEJyWCgd0Ev8 zOuVVC%lP~*7t;@Pm9mI%gYp8MlemW(^gS+acCoz+O(%1qy^pxcWEFQKFkf;S5=2@u``EWQ|7Zcf%U zP!Q{@gNB9Dm86=-og)hua1IFL? z7nUq=j@xZsby?8vWYt(1sg6;h ziio0$)#6j6MY%ajal96f5egPP6mN@ZFwafhoGx`Tw}YZvoeQcA#!VV8HGUfH-KNvP zL(wAO1SMj&y<|J>)vvg1V8ZKMPNL-NVzoUT9fhM-rz^CXZM9)3A_9ntggmy7@X{g# zeYQn9>N#k?gXN1ao26+2RzBt%6Tv6VmtC}H;e})vN^C{!d&-HsFmsXEtEW{56+{9y zA?N_TB|V^$vADI*vx7XitEg&PTbZ(Ag?%GaUf*5EL|UrCaMEsB$u1{vW^p7Flvb_n zdE{B%DC$tb4h%YWH}|WP^{DY&{n;V~|8>f_rT*r+u-#`!*zGhT|5D#l!PO$LWq>9*ja@Ldr*w5fA;(HvJEKv?a~m}h0|6tk zLYMmwVw6Y{!9AL;?Y;}}XR@%P?Py0YR&c)^83*z*T?CDlnH6 z9854i;}4Ev)9RjBOE3DouyK(RElNbq9cwH-m?bN8*A?B?y_Nu2n$;F8V z54;>&NtT@^1qWwTA`}UgDNwBBxd(Pu-X*sPz02GF%?g`!0KUR}&C6SpRY7?&>wa=e zdP@EK>99~hyUQ}9W)$L-A0AqlB<~AJ@bPCP)^~~7HU-xEU*MqDPD!9Lm8)j?KQz58 zN6BaboR=QhC&JI0vmF=qt$%qHvAN$*_r!>S9Upb8!BtB_6(Mh5(e~>3mz4coNjzFY zv&(FA$W4X}7eydO+GF_n8rJ_)zfreOYq5vGTyH+>*Dv6&hJMGkVYejcPEyHmMfne% zLn~P$-@vy@$qJY5 z973i#NkpN7Yx>%yf|iPPS%aR}lAbVI5N&5@Z;gP)BzM}5eQSP_Bm|(uzO?25IJLPA4d)W78 z`^JbxW6>ju%*qR%@$=FwgFfGSdpc`)wBdg+z-ET?7L+8&V9s)wblTONRVLVQ{ybIvWOIFi8~uf0Ko(mP z|KsaHbj9Brk}nRsp{XU;Z5XUH{KC#8nlxQsK8@(cA082;VpI4(yQO8dPrcmlKGeUJ z-Aua4QI5q}$0!jX1WSBhb$%^q(|VJ7VU%KUb90eWHC>ZuPJLWi70)*r%pANKn>E)Z zg^W43wdD^pc%HyCymegw?~irA)3~htq-1F-LcUVR_I*hE!HVTy^d*rJ^;9SGArb2s z?hf`+s?`P6b?1lcoh()fz==p0>IV%K>Ajs*z{M|D$2)3DLct5^J?6mD#`=vIFN)G} zO;Iz>gIFmV^3O(UIWYj^!;460%t7!uF~y%g01|5Xp8@$#z2;Ydj}+*H^JuT=hwgNo2!L^GrSSCx!f?Y4>!zF#u*CJb(eHeW+0Ol!3p z3lPF%tqvwzRSY%&X2q^=edo}39&ZXm$gS8YGt>}+8kmnYT?Q3gI1$6=$)%trd1#sM zA3mP+4^5}c4n1jV5%F<0K%vN>z{?V(En`^4{+s*kMM7e}+5xR=nZRX(z zz*hep)PlX;*Mt2kwD_mi>M5WNfc-#AAWymJht(qp2!Gbk@oFb5q|xzKT-7`apFKF9 zWdna-Dbi|kk46JAy?V@Z#C^+pdVKwzkW5@>UwEUVa#REHwryKjXbFvhchP0!D*1{L z5sNPW*Rweqq%FPdLsQA?oqx_-tdL0edw9n0A9hDIognJD;2udYJL&3evxL|b3#wf= znh0+j!N#M6C!}tx4thXPIP4l1NPj)?s3%Qg>ep$>Yo$Z&18)Y~Q~k3tue#qR8RqzN zpTWarqSFS(&jm=2DRs*pxQ$+f(f705xg^gD=9iJDi~cF;_pEK zmU&)X!=%;roL{4R6|}X^X`HU-O#^*#Q+a zc%QzjJ54Kyjjm`?83hB>ij!M+&UCO4()PzzzMP3trWofARi7|)$geuT{1s&jc2SQu zEC*{|2Ke7#;rs7vEe+MP^-qQUorlsdAK=I@e`q z-8D6lpaNkV(ygyj!ee@1$`aIV!r6lt>^YJIBzZ4S#2Em&)}bqkKYwv&PSr#jgB*NO z5IplB(E#cQLflwc?C&!f70d;R+^%b|g7F}Nw{`qckM-B*Uy(KKz61j0#hcXzko z7Th7YLxKi(w*+^04ek)!CAho01U+2MKi-S6AI_S*kDzeaj`dRnXRx~qyqh78r% zUvA5B?@NF_rD@YLyK{+k`|P6_Cltd-Lhcx0+O)UgT*0A)!;RQ8$k#*baMHreHDcDQ?J6z*YCF}0-`t{ zt7^xWj?NUFuNM|TQa852wHiNy8XDzsy`zs&Wah|unuQgA6Pbezi88cABx#MO5?+QW zFSGDo|F)p-b%1OWimLxAyMHrx@uq+ENhO+E*5;)CjNNNy=zE}fjax$s98E6FgpIkA z11AP?G+egL1UDFQ*Bzet1EwgkGAvYKT}d=OH^}-13iZQVdX5NNR3x_1!|j8)s@BiZ z$e?-mg4r=})ZNf{-#>UK6xb78wYJ}jeq%!wef}1T&1pvU3JEs~$nb-+ zaHU8XqTZn9M3)Rb_sfUXn{00zV`8C(qE^U!9nT^_ILS7OrAmT6`SQ_{z|W_>h~XHP zXXZ2r?1u%%U@BclRLAPU8F2LXszWX8W*ke32iE((&grEjx;*BKSLrRMOF3o_b$3j8 z)HF9ApUq##crFTf-I=dYOtXy}}##%EMhD64}a7Cp4fv#iD&cRnL;7pzIIvsVu`?m^e1!udLxQ7EW z)Q^wxN3vMb(7yR82G?RD2X`@nMd2&Iyh3VweXn)>_7?0oUq#Swtgrm!30W;J_C5;~ zYzk)Q1+I}N#-03t09U3lfm7FV{@@`NY-U;zEgNWJ7uKhF} zky8y^aq|w!wk?neJX*+f)wOt+eq@U^0{a7y6yITIh~z~j%f(nf2zxd zb9d~ZJA~wkK7zXm1Y3;rpAI&uHf;1yUcC6}HSz9u<48O%eP1fwY_CdRXz8GSG!37f zI~n3J4<`-^`??j>J@azJhQXzw_*!4C&+ zik}X@X4u2QNINqcXcT|VPA_3Nh3Km@XaS097D;I4Y%)$gnq@4GC)gY|%7j&T2Pc9u zpH^nv6n4_=6?1;Jf|i(XB9ji&`QH#$8F7VVy+7*E81#D&v>rEydXQ>SmvI2TW+xqt z>Pub3Pq*9=U?R$39Gu#IFZ>c*58{p~fA_)oEfgN=RHvPMHhVHGqTiR#2x4PrpR5nO zDl>Q9bbjY{loC8W=+vDoxIJ3Nq}go6Uh#A=8ceQi%jd7r!uS75y%DtC;#9wUJK~vl zRh+M)5O)&4P22|+{&`}_qa>{#D*eyXImWh4!_Wm?ivqaKt;+Mh?GF!I0Z+Lue6y0t zoc*tZ?I&_Q1UjA($nwETOb2!g7vQRNw6pL!76Q7>p?qzVl~+p{z+BJ{G_qPgK~UF1 zhaG3_H*-KsA&a%hm<$C)pOI*i?c)xLZ$f>yyt5c~v~iP=<_=2`L(-$1!qzC)Hp|CDWw%{BAsiI9dQjy^+WCbpp4dDtj~mpI9)Y>pRaWaTha^v*O_S z*LuW&I76tKku?;Si0qGbaRwGTQwY}M93H? zU|ruH#YWe7dwUyAlS*e2HDeukyY-s?{JCxRvyva>YMSA!=Iyf|3Y^vMuv{+qaSl{<$)o&(8 zRZg&QXaT-lmY4Wq;8(q}pQtPg-@M`3;1Z*Bn8{O)@gpY8i8lH0b+@miVr*%zFyGhK z6_SB*WR`A<){yykJ5274DDd@+7HFWUE=5mr^OLiyld`r%cP!dyEv_J%%IaoEk{aCt zR{Bh$NGM`*p^3^1_v@^^<|~aYKFjDIu~6&fE^%i8rY-MDMEl^|KCe078I9Yu-FZCb z_|%cu%7rVhaAF)mGCti55R}3|{ZOAb_i;A(MaFk_&mArjHAW?UsWVUP|9%2Va3m!Z z!C2Pa`YhW;6qq7>ME)%#E;KE3brig!@%J3R*VgW zGxWqTj2KPiA;=HMG?nB+!UJNBab0dM8tha~JV}_NaPOc}wMX75DK0J4{-m~w)F*}M)Es8_G)kT@v%n|& z0=@OI&dJetw=6=7(j&5Si7o%vD!^f9O;14+0jJ72wGX%m!-RCbr_bfluqdu!J+N-A zaZjj9Pb`zeM`c+sXqtx!{l1x;Z26aJvFMj){l-8|T}CS(3-PqcIcvzbAA6W}91u$iJjERf_4@xK~o)sVc`t@ef zpa}#~SeajK<0eEZ0}9AY@1ZtE>%qoekSDwTQ}!$6`!^xz04s&XxF$mJYokp6H(ZvD z_o7n%zc}rXe2SBZm<>ac?JYjM&TW)PfGA6-l}u7S$6ZEv*ZZ~~in_-G-6;)$+^q2mR2+^y3f``zOcsTwQY;?E zQZx<5dT>^JHQe?^=!0hd?H{}PnXpUPm1C#jk#up-jbc+uBN16L5fvU9mYH#30u#-j-RBFzFM37oijiqc+jYjK)M>C#@1nHM`- zMp94B#uL1tM-!x9HWRuZ)tN@somJclHPu(0il{72hJG-O(i9khutGgXf;7W;rmf-* zRPHxq6&Xn_?Q1u`Yx-EheNe=!F2}daJzv4lEg#~;5-zcrfy<2-xh4AB*Yhy~!7C#X zI)IgT&YZ|CpOkG%W9sV*oIhS%zME|*4+-)1sFGQ^p7a-Ec#|4L`;r8vuHyOS>I|-Te(8Y6pk(b1|d;eu3i4^!J z%bft3=bscJ%6rc>S>y*R?`|%RSOt|J+&0DnWJF9=8|BhDcL3UvxmhQZnGAUN?O~)y9 z%d=j@AuJr}dwdiD52d5(_7joFNMXXBVWEkNf_AOk7wO#XyUpkk^w4~n?Y`O@pBghX z(q?IQ{L^WJKMO9+YIfrq2fhx1vD@^$wtf_;SG{;)!_0F%;J=^!%orY7DjY{~F#)%Q&iK z6lwBG-)6yM_6dGUK{`7?knG9ImKt_d- zC;$$dS&-aTj*~srnY~33=ITAdQQXGR=}Px6yedDfD>=!+WmnvYP&OpOVF>j{{;41n zmYhQD-InXa*U(UW$>|}7G=_goC&X%te{izwQL;U?vFJW^t=#aEO7wik>LkGw61qX# zZnHQ~F70oddInn{!APug?LvTMeZa(dQzzxQmX~~u)2f7WeP`6|p$>#e>~(YGt<>RS zBlX)xWm6fl-3bgZ5Mr9-aP-o&ij6NZW@DE@YZR5cLK5@Jl8*WHDU)}?v9Y%#RA=R6 zw{QS!1ov99zC@%&1)fLFJiv2~4&H zT?+}#knyQMG8oZcT4y2$*4PfpZ34$Rn?>N&6(=hG;AuXU~K zd^{PM1kKI;iRB{rR;H48$VkOD)JMLoH_})RsC`FIogI8wMIhz;t$35pC9jNqE7>}@ zFxyhtSm^^t;OD)ZdEJt&f_LZ%k z#CQ4fZ4V42M)&pln&z8b0dPKz(c-7tP~BN~A-UdPEq!WcyrX0|Hkznm4(1*kXGA1} z@1NZ_w{Mrnwtg8QZ?!U)J1^CmB{l?cKvI#bo#%b*@u|4YiBzUiP|4JJlnumF?@lsF zSfadO$5DkV@WEoxc8#4u{J$wpLNA*h5|Xqbh4eqDCYvbDJfCXr$XAI(EDNcPX6wC9syJd_-8FNW*mD5HBUhc@=C#c2{I?@N>tt`QQ#hy?s z^eU8pM6|+zy89r!0i_|r={qn%yW1%FW{m_0L#IJ5yhU1dL-51<^`lF$=YwK)ESoucS1#369aK>$@?5FqYA^>|!gUkvB>i18< z?bC2o8rBj#I zo|VzNi=w(n@Jo``lvO!9!b39J<<@$+SwaIV(EA>BI zfTwlAHM?Kr_Rk$1wIo)=hR+EN`cK0wv2Oi=5lBpfk+vt6u8u9mCkrPIb(GuXTXBWv zU6gAqhL(BA?ADuyU0m{(T<7;|tLu6taKovBR7{!kE;yu=jC!n844%Jbb1#>5RhkJN zb}b|-C6$xpagCegrT~Iyjm3J1g;%iyro7|^!s@Xcf-cLn+G>rxQrYZ~NO9rr$jP(% za)>Yx7EHe!9L-ATx)V@mHyb4I@^opasx!7Chh(J{n@_$~Ub8It{PQS`s+yzv= zvI>4o_)=hwgF2p}`cprhMKwMIbVaeQYQ&(Ib5mfVW6eWi!C)@5aouh*{ zuYB0#_SBheP@czJH{T$})Cf7JI56jKQ?9z{OzT*-&^do{uwc!oc#omYc)H@W(K%QV zz@)$+8DHfHbhPSCksdj5$mqF1@ERht2biWAptWc4LtyciD-+SjS8MHbt9!%Dd7j47 zS{T)FE*%)gel55@wYl70Rg7e$u`l3UgaqK#W0#Cu!)af4_7%&^v$K5izMO+iq7t|o znKs)5`qOT=P1}pNl~tTZ^cWiHwa((rF4fjUk`JFyH%f8KQJi4w_0Ox7?Epp3i>uMn zVR>C%Rn1kp{OcCqg216P{@9$2$e<(>iN-SOC~NgR#)W9<;F**e*OxFwFkw5VK8q`) z^^wyt5$kk(?dMh>6Y)nPn)pm zzm*H*?3f94Yv=pBZn%-iq)s|(H)-?hjn7@l}38pGN&6KESqw3LG52Ab! zZw$va;zMp*bx)^;p{7dj57fSFLsM#hfnep&8W-nevTG3X3kJdh>sRmYb>V-h+>JWmM)$f*)=N&#w}~>`5G@h z7e5EmNX;g<({`_BXRlC%C2a;XZ{^9$GzIm#QG`KQDJ|Y|OZP~(=(nkCh+z?!<|VlA z{Uvp0wRZNI6{5H=@0=sI@%f?qH3^C-dt?``7Or*yAMke58;i%8-%4oMjl$JcbSrE5 z7n5qRg+ZGJA5U~r0g=`=wH6%`|IUN_S{x4;`?wU2W=HWVT z_zbTz6)KD>Tj)5`$^4_Tn%qNN_iJhrbMhmnv#iSLJois55fnh1pK!Jd(d9?x26&qp z!k4cuz&>{k5|qU&dNf~^X@CG;*z0qy13MkutFV)MtbA^?L~ICnEzVZ$?h}TSNvda; ziF@MniAmS)F%dJ~CXbw6H7Dt~RKNqTIz!Ih!Yn2f`}5w$*=$t0OL)n5(%qNAtcR4pqhBb*?= zR^jkBEo`!PY~05wJ(YD(VQ-^YSBcW)87n8-p=r+P4ytow>kC4N$Jf_DcG(%Y=Wy6G zxQu!3t(yLb`skykGxN7j(Ca31uv)?AuL`alwx=NT2s7l*0JpC+u;~AlHkA%!xFqR(hvCLeJuEVseACv zz(FiuJ_gs~7~g@Vilx9}XLc*c^N)tMGF>lvk9~7$T$bhIiMcXTmdfv!nCr{9)gb2l zF+BZ7#tdZAgQ0fFjX~J?&2?PAJLXgWTUf-L_VX1}2$%abI$^PVdb>G&{S6SMB=LNa zH@?@$0ZphD2dn;rme!C1%1Atw?{`c3(xVhEa;Zc5QaGX(jSc^m^!?-fgw-kRC=&;% zC2cEr`CL2as=VW)_{O`l-`QDko8~Z+&d$>0A;_qSiTnk1d0JHY5~=36P2q#WrJPaQ zd&ka2q@?kA1`STCpYGj`YuV9?N&v{N{|4a%ms!VxeqbloBaV%$R`*=1WD{AP(hcnbds zq+<6{h3IB2c4k(^gQW!2L3=wszUz>9-mc=5I@N+5`(N{9N$J51JdAWCe_*l3dOPL#T_SR|49%S1B>rj0{hmIY60v<+VafSL1p$Btxh4%IIOT%{-*lvu0W zRNlt$NLxo_X7F;E(C3Y6^0Nu$%MZ(JtDv1~#NrmnOW`m$SvdJ4=dOMB&+fpJ-++;y z0wefKwl(WX9r8TtZif?sJg<1uw{&X;3MFz z=G#h|;bkt4c{*VV84UyFfSaP%R(GVJW1>d&yq>uGo6lfbwS67M> zZ7MUk2$JG(EQ+0kvWdmx(o`9QAC6i8Yl|X}swLN(5q74(=fzDa-(YF!n)4@B*`F3v z>-BSH$HfK79ux`*jJ}m|>G8S0)F9rJqE{Cvs6FqPr?B|M`br+Fml}Lbgx9noqXv=q zZ@V$dmXr3lm$$OYO38}&aK=h~lF|`PjzuPwLPc4=Ywew;x5#a5wAnB>^JmYtR6$U`>SqhXR9 zU}{AENk#p3Zr-7|cunEy^71BIviXFNjEI1UZXCL&daC}UT4dSfWVPMFM=@x~wkBsv zF+5Bry`H#XXW>>g5s6zaKJe;$#%!4^bxn1Rq?1`fd;Lms=C-+NBrO9c)zp|D;H{0w zj7}&0#CAp}MwcsrsY%6{RMecQBrId=jurOIF62DD!gOF<{BZUB>{p~eO7yWNRVo07Xb*PZMd^H5-I(lxcRc* zTG^yd@f_ir&z)XibsL=y_wB$47X>AiL^b!?n2&|>QXymvqc^7EA?0=Kx~N!5v6wWg z)?n<5CfAnhdhD1~KJ#Be7~-Zz7fw69vwezY0-`5RtzOhh4%-Y;afu|uTgZl^2-_n; zL>g^&&xaAfH=12c>~BZ$wI$Nt4e~8cW@?T*@M~5dT@VZ|^5{*riXw$Y>=fM{wv#L|^UCPGntpbimHveB5C=%R-tZgA%&(qu9GEDYw+fAy#dq zNe$<}Msb70P; zTsX?TAr=E_j>HB^6K#{d4}*pd-Dx)Qyx|}16_rh~aG40&7a%lPDV*GCrS#=f%b=r1 z$9-Ql4yItDjI?s=M%N;8`sJDcu#&n*{}1x+i0&PnFe)zn`?7%y4n>z8b3+d)IobAO zHOY!Ek!d_EIaLMjw%ztsP4$1cI0W^99Yee(_oAo8cC>!R%1qC-ZoRWMawH*4v z$wymTz+>W%@Gs{Re?y2Lh?cKV46hM%wN5sY*|3u0_)E4;ioH}OJ{Gs!ZKNQ=NT{G; zGD=xt4}uR5MXg1=NlI zDhxh)-B)#A7t@`j6894ZD@kbTipmfD(kIW08m3IhSFPWx1!6e@qBvuE0X2_%w$<0; zv6Xz7ryb?JpfX+-HNr}{v^_WaF^15iR``l~#e|HL5E=S|ij${@g8P<49wvfGOnT?0 zntIwwTYJ;<$?m#I7{CK9nlMNai(U_3*M0Nh2NP`_mhJ(y4(sZXl|xkk++}{LSb`rr zjajLMd!KyDSG^zxCso3=x>JohMr3*XtWVYkmKFimIG-5Uf6}}{LVRDY5OE@jrJo_Z zJsQgPA_>u7XNTcva8g1a=qN<`ZUc>o*901@T#k!om;q6s{-KJuyB%VAJNklS*jHc5 z>--60g?$WPLn%`CzGo}4(&d_Hg7P~4bZMCnGTCzZpn%6w$%Q&qJvC#ts7dRP3fIOF zwR?UXYxJ^CnE+oP-NxSka-pZ#ClXaroi(_B=RCjET#4yIL8*DKtK+KX9D5=z6|lC6 z@GWjvE`pXH&~6>2I!xA_c#(J90Ki#oDjK=&>?DB%DJCN;QNWRGq+g*UNoLr{n7pE~ z>_{D6dIMx}KP{@gqm71vDjiw4=i!t|j4^rF(-0pB_EfThgQk6xp{_7!dnlnrQ$s5s z<2~LzZGMAKf0_V7SswRsQmM0_4*#j^)41=JrbeZ|(6k-6C==&sED?EtWu+J2i6R`V z(RA8<>+E?ye;dWom=?R=ctwJ>kWW{ww#M7OyV0<-`@HaY*saekhG^Q*O0pIMt4$7WrCw{vaCB}y#M&Jt-A@H zm&fTwGhQp>&W$|w2HQMbTX4uK{B3G%Qg|WXDqTH&+Kd(63SOCE((7nvb@5~dMp15rfH0QY?~PX9Z6%e?KN(Xn)Y z*wE<#b2}-CvO+bB-?gowYPa+J0Gy*hz%)JIaN5LdHGDP6hsEN(>Tv7%Jc(y>^C1UY z_fA293EvRIAH8qoJT6Pz8nV>}aUqsQqddSkNXybrTqe5PLG725V|N+u2yLlV1(wotM5{;LMDRP6H$86zX5 z^JBQiDb}`BvwBL4WD*@w8n74Ifoc;bvJmu-_e%2zb5 z^`hI@M}(lsaH|fk%w}f}$ZS1C{{h-#hPPkCTr!I>^NWd1NLSZa+zK7Fxjofnz;@6y zXK)$6)S_cZ1XmT-Z4D(%sa?lny=;eg?rh{M&5y!rY4dC(#XjnG2I_&*?M^$pa z!ac0E;&d7=+MgR#u-prKLsW6740=ecwwTwxLUG7)e=hcY@mmnJ8vMEXD2-swesH{5 zomio(j7)`0%8?Fv?B_*{7p;KYCUN8;7?pKg+=TgXi?g0C{GPm4^dMZdF5#_z)F~fy zExr3&occS&Wds|KMl?dZv6(p%4Nu;)-Q~6+>9DJMcYH8x?^-u7I3z4-M5=~|c69&A zS-s9Wto?pzl5T1yKTcfJI<6Kd;L(!HmX`iudH@=#tB8w-vWWUft?F%AI=Y2ZUiyr( zgURhn>9Ht@8920Z&Pfsnsr903sqs*R$miw0ik8vzq#%fcVNeP6d~WeL>%|LF7n#>p zmv*YJjy&9i%)~+Haq5Q@IZtc9XXSnOB8DR^oVVPo54pB^9O^1uYcRg7KHIX&I^kDw zz=`drDi4$)xC^%*M#F+T3ch{HH`f58aS* zy~Rwiq7F=RU`=>$4!I^2qCn2AkdV}_wsza3{^MpXKs-n2dgq5X~TWzs9tPF>0%s>Rlvl}0L3yXfoqtAeHhPi_ekM0E*7QE~7Tu%xjz_AQ^D$|=T zYb#DoL&;edcJF)pwolty3SEp(R?7a2MJv#+{O0?Q$bQP*tgDkV5aLk?o^dc%W{sBS_?(R4crk&B0DNL}7`a z4^DGdGV(eds~9^~#?PI8OvRcyWV#eJM={;_Lt0%irDMGt~=TE zxWw?g9dGdwF?BnCIYLmd9>WB&IK_KD>_OUp@1Ga$QeErsY&kPcU>&9^UvRhX8_et zajDUs4Fs&An(>p|lwrO4=c!HcsJ zQtUWX9e}edh}yXDgi-pjlr~YInKXgQXx9%fEi~xG623ID~sI9QLGY#_s&P z`lcU^{itAkb67)1J@3;wmUlcJV_nAmnTw70;|koc(j(EIFW0lYSyvS=`U(*d8=zN| zKXU!0BkCIR7?Wb3Sx)xc#kh=gffq6$)Eh}2Sb zL6M!I*06OwZ?dq7&qX$j$YRB<3r(V6l_&8eT>-b36T6o`@|1S#?kNm?v0{0-pMy?r|sGt z_t_qWeJO2djOJZSg{L^v8mpFv^YUD2Pi0yCb*^IOdi7t_)n*Ovn|tTXy-c!qDnTt- z5%wdkMAq_UO((XI*%~fa)h3%W*i9>J|91@iSxkmtPeAaOUh5R<*WH}HN}UAzo#E3R zf*p)LEBE_r!KY=iI2G&4o-TYqE&3jMKzrNoOQ0Vp$HqTGw}nHWf4^#fnD)+X*dfE9?bzi^z6`>Tf~&ak4!%Bt6SslVxIYj4ar6^l~iy2+BJfM_a;OMhe$G4?Ens7n&ZI6VcGlDl+E z*y?5ZtjY0an{dm__|3jvi{^OM1HTG^tat(Oa{$4u;KelK67W{>y!}Ilr7nkyugFom zeuBrwq1Y}J99vx3(pLOnp77*eg;aI;Ru$X9!i)Wn4T*9UadRq|9h5>|YevX6}`mmVBdQBP9e z*VVHqV<;!*jDX9LK!A+~KY&|gG^e-a*HDMc6RD9Di zY!_CDz(|5(gW+|4W0Q5iso?$OBgnr>jw&q6PPP7Yibr#=7@`O&_*{?WZjzXVZxLr2-P;)u$+ z>&f=K{^Ph{`}0y(15xpD@0W_P+MfH6^!EoB|N*RAPOZ(f7404UhYPUsFAQGPtO)juUi=RR-e7!A9f@33 zUZ)xwinyQ1QZmx&cwpid5rRxwmteMiX^??wYFSid+xjPaM%l6K+t!m#sUYf|zb~+m z>G5c#c>Q+({KK%%-AH#0YQTOgMNOaN>|WURe8)wLPv&&}EmCFHge4H;#z$%4_cpj@ z6&1y1-8^XqWpl3A_M3U#%(Q)}1gXDmj55)EQsLf(>Du$^_{*t;zGvN~&rQmlK#R3- zk?nW*$tugxR9Jf%@^y zvbA7qYwN4K1$=^CTJRh^Ish-J0i`SAPWRfvedTvCDz_buIr%_#@A05o6%ZBph- z8K63Nyz)mwm+z;L6Gz*GSu2RzffkAm7msU_+1aXQ>q1iD2!`w9P)`#dSF^?9qjqOz zgIG5N?YWVLvO3)w7>ZpMrHf|T#-udm@-imX$#8#|nFUVUgA!5s4Mc3Gm$D?T!yK2YO@Tk%G=&D@P z4UsiwDKd-Pfr!8uEb-xht*{5bMbS(nxsudhl7ON?e7xP`Or7Fi)VsI$Z@%50!7J)b zd6gHQNL$Av+!gNr4khr#n(HSPmsBN@hG1T>tfXvav#0MDJNJ3h=DP#18g-j(fo=k! znOAmhf|Sy2%5h7Y3E#8Me*xZ;z5JO;K&X1+frY4+_tViVl`P2!X|>%T{j2_o?{SX(%2 z%TH)d_%OG8dnN^QTb715dvRQcR=91?YRFK>NR8g}-M*$oOft*sw(sDb6O$U=cxKaX z0G|LmC)?3yW3h%$%_V_EEkidlwGWH?rlOF8T0i=5jQ%i*ai6(A-U-5USA!)TIt*;P z3QEYu&*rMKe_~0%R?skj2*5WUP^{{&QJJ6~NE2eVe=5!zLKzYUVrxkbO!HV@Cn@)M zfjyu#+c`wz1>6#bt+!b}-FK`V9DJlu&u!8N>yAmMHx;ol@%7TqpHh5<#GyO&8JvF;P9cjDf^)5ildExA?#yI$<(OGkWGq= zF_XK|ylI>e%0AX^dOk9;?Cj>|hW1PuxohuizpMr%u_>op?EC#Y4NXj){bA|l=3zvu z^(L3cv1c&?+V0U%>0Cc>V~-h}y>_IRzVUtZw5XY6)ZFwRZz4q#W7C+!DOxY9CKmqw z*%EwJEo=F_3|I=MEWs(YMy}P(-|eI=wzT^8#Y3XOGkPjmyuIhfh#T9e+uXY2(OD5Q zrCt$1_(9_XORB@o^DcM&~OixXb#bmSb zQRwQ*jqismR*RN#$(iUWoiRYFAKz7FrY0h60i9+BVhv;!bcZ=ov^Di^sN^PC?xkY36@LG3c0x zK;Fg5mEJNS&GuvUM6E{G^BnSTLjXrH+>r}_kDW?(jcK`p9n-JGQbkl)a=EdV1RG-aQwMON-`m~fuceEHtI7!gc$v& z#Mf+4khV7z^Ax}rH2O}|1N9vC^6s}{?&lz6`sb718R#cXSFF#* z%PP_p7QcoV+q+mn5`W%h28-EqvLd0ONQ0mbacGRu{<(j#BiQejp34OPC14$Glx=TFu=%(!D ziP<5oPsbbZ>BHlonG1PghWD0)g7$+{FMhu2s0)dY$-LT@iUntFvQgJge?}+*0X? z>1%l~!(L{c-5V?5;w!6SQx6XY%OiJWRW$CPud{`txg(8D)MiCtmf+u`) zf%wJ(x{DVed;im9@Dbd>LY*egsom(@VQ@cOu=gcE_82g}8-iVPIh=np{6H%PY(lnR zFB@og#GE6sg>fNhz#t8%0ghXRj%tH1>T}j3bcNP{nA36Zu|fQx?avFVSn>4EUBJ?okmQ#s z3v6v7pW{aIXqGGRG=vH<$5ITMa_%VGZy)K;J&ER8mk_+xKWt{#AjGW%!!~rX;IdOc zAXXUAE#3l*;@uX#UfH)CFUPhR4o0G`pXpK`jX&4C4(Ms9JtBVzkS)s3mqe)zqMy8Y z3r|uT7Bn-LRQ|RW!n{7hAgXZ^Y55Ob4ah){P3*zD6XU_n4sMNJ%dvTS#?P6>&$PRp z5rNQD|Cv7c-d;cXeSs$Y83QEWyxYZkaIS3IA=m};xNJ3;;$BI=e=Qb>;{TuvFJ~iI zM*K5@`E&i>$idalv4BbmIF#>@?cyeV@WP3;v~B!6*W5$b=22fCbb`7{&y5$KIs2)K z@4f^s$1=m(`G5U{yS%)N_s;>GM2M4Fi2-Rp?{6;SA~ij6s++PrZjUs1zn>d3cwY^q z27|$J;)Xh&IyyS^B%+1WvnuLvAH}4lLxr<{F(5-Bm)PQ)R*eZD*#B=tGLluoEOmE) z{oJ$!JE;@6^FC|16Tc_svyF|7@y_VX%uHU7sEmQ7tn5WH%jhVBv2nhp=AwCBYK|~a z6klDZPEZf$=@&+Tg(gMYW3I9s^owGbI1)Kr+z@y@1dUS5BM37#7L`gqSwqtet$P&s zg&#q8M+8H_7ek1bT%9U)n73AbpOS{u`f2jrK7vyn#`jl5#2&;?@ z_0{!x^x z6tX$U7a0d9B{2~bbJvmn?GKay9Td^^Cr&2PU_^)8V;p?79*ZCzrkSfhyztzjegga? zQ*m^L1_to&MSfjFK?$S84bChsYAPzqYlEQ5kiVQxgojz+pZe|37S#(51^#HWdF>@o zAd;Wq#HdJ5oE@aWFVr=Ag~K_8rv73j8&rYVtxBp6rOR=2ygWSd(#HShDufIau%@Pe z3iB-#9iaK8pIhe+77#}!7!|@tBYV1%ACZUS5ASPCJv3LSh$pZ9`seLYG6L|S#(*4` z0X1k8f9j6`!e6LUwY7Bg=T=s7zR+$Gyms_VU_IEM#}{2E3;yaF6C10fu0FrKym#cg zw$|SK(ERuB->)UF>4dRFk%b0wzff0KS63$JSXetFOITW4H>ue((9z*-iU3JEMgCpG zK!E+XRz_Od{KSMUc&@zhuJP>5RybI`(89(-ojReZ$?XiV(}jW>2NV@5Uuc|Y)o76< z&_9Z~yEjjMpE0jHK5qCx^6lHV0~{P%gx7>NLeMDibU+Fn9egVc;Kt|gz%>2e7wcfv z5#Tly)W}Z|=|3mG19GDNO`stWOCGsRE_HEcARIh;oj6Ze;E~W2=Gb&`PV1wE#QP-5 z)3U}HqnBXuGcgLtC;KP46aj&*mQ};#_jnfV0O5)6IR*wfTw}B9zEg@k(nux;fJ7DH zW=%*M-1!DbnL`~pDi7?1aBA3b<>W@ofm>BoxbcxdVzPW7j@6-k+NIV4B8Kb2p}rklh|4ByoUVxAwk2X zIwB2u+1u-RHC^4HY6~YMHO;nR143H@ zovf^y!2ig=E)7Sc*bIw+r}HUFk>8fS^!4?*M$W(Xd+>DBI?$oeR^1hqa1;h}!F3pz z!UM0LWX+A24MkPnz5C*X{AIa9+m$gacez=kCSo+r+i45$TJJxaa0?=3{(aDdJ6P?g z5tf}DYkQENFNi=~qEeqku`70MS6BahFE=ATA;Bj9wZWOs{=UA+cM3M{BsA(YC7NyJ zxVeaao)Yrg3O;X6QU&hE;9YH{7nI;|kUrEJ{@rg)iAg@k`ITOOMTlR!tm*8ONzjre ze}j^uVsLPax5jY3+h%!r+5dB~!XKKa;GF~6+1u8T+@ksAXHxs< z-fh>n|FQUaz_E<*C~>pFet1cChYt{)lG99w$qrA-`E#Tsxd^p2`{;HuufW&+2Eq8!YX)wWMgn^3iMZ#>D zIOV*NIvi(Td726K;uh2!3yV>9rp#b4p1{h*#>PJD=j(WZc~>5ki{{^Y&8q0OMeAat z9&q|fm0(CC*%yr>ICu%I!-q5C%JE{peH2i`emrS9yafy*3e0_HSJ%L1Uh&?I40sUn zRP)Klp=LqD3%DH``uNBGY0JzTI)40^u3YP~2+x+LYpQIuR*vF^%s`1mxS<=HaxQYX z=h$gVee?-_%xL&jrNT92GP z0V8%sHVigCK3=oj60mzO-0)+umfU77t@W~p)aVF7K|xBNEmbQchwP>Zt5pRr5Vpj> zZMq9zK015Uw!r>eB(N5>)__jCpzVs$t8TWWw#CY>8Q|OA*chI?Ug91MwwpqLC*>a5 z%GBqB!7od?Mg*P(F5>%wlPeN1SVSBFF5AAEKl9n4BK~!{ z!hgpN@%21b7URkTA>%44Dm~3iPQyilkS6OcY`%Z5{I$>3 z&|JUlj_&UM`yHmYgMkI_Qvv&k`N0&40R+(3uh>fHk-12aT$-iK;Zc3|p<*Hc+)wMtnZ~r|vUn>iKL#7#AwX#$&HgJ(JX!0rr z76-7{GHAmr2tSH7#XSL|TNveVIb9 zsWp}dx`{+h05ugTJqt6J2@dzv7csTBr=*9>2DJZ2w{2ZXhh2g`x)(sd`SkvjEy@H z3V5*k_IcD<%5p_(x~6m=nd6@2+LOWq=rNkXY(B#5joz;VW(3)k_r_~BOv71}bDTc~ zyW#KpHE*?Ey)L3OhzEYJ@!qP)`?GtB(&HBm#CpJ9O6|_)XBHU+HVbmQ_wn~Fqy~wz z`95DuMZB>f^h=D};x2ESLm=31_>#VWWtK$cFRh@CnAHcS&3m-gW)K{mVW3@h_3Y~) z4sY9Z=g}jLPMzh_mwRZ$vy8&LBQu(DTS&Y5+H>-ywQNkz&>+wy7?|{o zHk{Bc?(nGM1`nP4&Qp=?$wg<@vS~#ic@C3$2-@y9MaqR`_bfVQJTsFHjudC)7WD_Q zh3&Q*gzo@<<92p<7KA^`2$x$8`wK=jlWDkp-kBxCsYY0Z9uxM0rLEpdH`BTE&@JaF zlhK7`KFb_>M8Bc9p!`65>fs#j@)BNv&;=HE)G{vvAu>Fujd}jwR>#GT%(uMst+)u) zB$#xnJLg>l8U3r}aOOUHjeoU?mDAABptgi+N1f_%siB)gLXx-xyn5h%GfyBhFj*2T zd>3kI6pFZFc){x>Dp1vyq}x%qdt9)0bRp5#BS; zo2-a;x$atNxu}Wi9q$PZ%P6NuL`Z;d$jcBT(Ut+TfvTN_XH(3=z(&Mi{H7l3q-V?K zx9+T{be08pQl*U5@Rqg#Pn-J>8yY4AuUvr7-TZ2k4_VNRDwG!2V-s#6+9ysJPx z=I3l7?zf&5oi#Me3w;KZh|F6;6j&hp?x`>kLEKpcFEPr-)Vqe%fIDh1`HLWwU(J~#4YZaHYtawmYa=M zspL_mLw3UKsFcq^9T1(WgyfLws_l+789Yq!UhaOhWeuB_KVc+m09U`<>{~8L2ZSQYX%QsRW)c%v>BuM%)6fbazdk&O@TZO=~>8 zT_YP38n8^)OLX%>nNCii4_3J~>vX?1CX%i;4psmr2^m|&tu=*reEpN9z2j(e>9+>IZ43pFRo$EJCiaCTg?%%;^A#LteHXRGohQK9Ru03#Te+B z^#T=pwAj0-Q{6msHoanrywK2M)&jE%I^x$x;!$e1?s#i~R8Xjk?h%J-Y}J!h&wi)S zbqRfAn_$QqcK0CxrA^g6EQwcmd@a&^doY2xMWfb?wX~ z5nPN{V<#aI6k)3Wh2;(1k!!&a)2rO#CVRj(1h7j%Z&t8uyyVj#p-gv9YY`$3XyBP` zo$n&dODsVMxZoZsdHf2Ywf{caE={PaxICk0=g9Ad7|fBPZzi{yF)@}vcwcKgi6dM} z1aoE5Q?Co@8d$DfXaqpseEH6R6!aQxv)(BvkpI1MWjtE+?O;B4kT>MF_UYHo!8heW zOSQs4Z23e?gQ|}O0yA0tT5yhXo0IhzI+B3o4(w`GeT4Ja!Rv|sIId>9hF>%E?u?}> zIP1znTl|(k%kLC!K1n0R0|==%;(p@sHEwJ&@OJooA(6jq$fmo`JR6e)G7MD2e6Xw5 zL+g<0!;dw3FC_-V{Jlsx{ka35J7sV-1zEz>7*l2;pcF*$rT!<|It$5YM+RyW3-Tsv zah`fWVHB*Y=)ro_(w~PJ{I&eY<{SKQ*uil{-ua4kc)u8nLWz3t8R~7^b}+=3`PUrd>(E!bsY#pj_}trc?S(ul&h>yxQOoLq(BdNA@RN)R%#2%2KF;!LQp$M81_XBSny zN%N!QUbQ9Vc9b!TDF<0Jx19IOvd;ku5)K~0*k-?N806bF9=4?&HNgV4Ng{#(Vh+GN zXghr)YeuADB{-O20MqH5hslGKnNp{pOcUlZR+v(51~-nU^xJ4eO@!B(;7O5J(yv z{b)VPu_I?9CRsEok$=jZ%%hyr8>VR14o*(5$lNt-j5l-Mz)umH2FvIo?ywoWW03c~ z^jcq9rVjgD0N1h%AdW!)UQ^sr0{9~%AERF43|OotXM(!E%y}E%Xh4Pq0e|6JJi4x~ z$?y11b3h<%0HPXYhIDryc~rVl0Z?k!1AAnObeAsqe=GyU>h|MfMY}>W4*AsT1?8vn zdPU~==&N{ioyJ(FDTmwPI1vJ876 z<&YtDP8S3>1M%MYoU-N01F0kKY;oyZ0nhOjneve<=2DX53md=SJZDh4;tx7|54Yi z1M&1puQ|S@Z?qMR*!@7Yn`1ldVjk&E-Z!cEe!&HoVl12J5GH)5?sl@cS@XN$&l_paR_`?pCK5_va0T$#mPEMHQ3 z1u1GGjNCB{h@?X#5Pe|DK1}K2EI3QWVTIoVEV=#BgAc0MS#DFwQ>3JSmYtp9E0f~?PCwRH|tpLn#L z$|UwC^J8RRrnx>jkg9}g_Cw5`IUQ*PX^$T;XWGeTP1i1!AvPMzKW)Jp4Ov=Juc~q{ z`xsy_oqhAC<^_vrKQM5<7)lMm>Nf#rXcS$i<@t*xqW+uZB6(`ey0Y;?y_S=5)(7C?YPMrT&w*okpwKuR_+`vR@sP(IK9)J!O1vLT_WT_n zOCSGG86MoD@-u(NSqJ6MrWAG(5`y#5jS>LhI4#y~=#xn4gWguJsG`$qzt)!1r`lbd zKQf0pn_zML1T!J{fFU!`8bZ^j72u3)nA8hkx@8}DEYO6?lz94F&v6cQL*s#I2eTo} z6W%A!;R#`|0OrNBk|WjPY(i6{_(OiP*$Im6bJH$ z>&CE8cI}O|zu+qwjw;QWOgAFtO|E*~Y5<9MikMOc^XAzp-X{<9I-_`4_f!|1YS^o3 zeSoS&cn6XDEP;Og{A*crtsOof!6NYuxX#bR@}LM6W?CKU@q7yx_nvsKVO!weB)7hDvnQ6^0(4jq&1^BBszI`2= z=7n!ZtqnrrXR?2-gg*qt)n|NSO!qj7f6lQzZED9 zkonOd)GYSXTo5>tiqv=Gl(WGUvFm3A1WS&euuG$b zNwe|yA#rEAtN|1`Tc{}m#Vmy;zKtvjREPKYJXjV63~L$_v;PjUl$g*e2D`asiebskS_t-Yj)7I$k>hiKN9V`^Z)<= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eca4bd65205510d693a68004c8aad1e795598adb GIT binary patch literal 18585 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 O&hl?gy41H_>i+>+(Y7Z5 literal 0 HcmV?d00001 diff --git a/test/test2.mid b/test/test2.mid new file mode 100644 index 0000000000000000000000000000000000000000..5525232fecaa8aa11b32f3a1bfa63b2ec170e44b GIT binary patch literal 26155 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 xyh_ +#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 + + + + + + + + + +