diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/GPL.txt b/GPL.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/GPL.txt @@ -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/LICENCE.txt b/LICENCE.txt new file mode 100644 index 0000000..d767c5a --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,6 @@ +RetroWar Copyright 2016-2018 Richard Smith + +The code to the RetroWar-Common library is distributed under the GPL V3. + +(Note this does not apply to the other RetroWar packages, which are proprietary +and all rights reserved.) diff --git a/THIRD-PARTY.txt b/THIRD-PARTY.txt new file mode 100644 index 0000000..971aee7 --- /dev/null +++ b/THIRD-PARTY.txt @@ -0,0 +1,21 @@ +Third Party Software may impose additional restrictions and it is the +user's responsibility to ensure that they have met the licensing +requirements of PhantomJS and the relevant license of the Third Party +Software they are using. + +Files with additional authors/copyrights/licenses: + +Asset: cgwg's CRT shader +File: android/assets/shaders/crt-cgwg-fast.glsl +Copyright (C) 2010-2011 cgwg, Themaister +License: GPL V2 + +Asset: IBXM music player +Files: +Copyright: (c)2017 mumart@gmail.com https://github.com/martincameron/micromod +License: BSD 3-clause license + +Asset: Blargg audio library +Files: +Copyright: (C) 2003-2007 Shay Green http://www.slack.net/~ant/libs/audio.html +License: Lesser GPL V2.1 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5363f3e --- /dev/null +++ b/build.gradle @@ -0,0 +1,95 @@ +apply plugin: "kotlin" + +sourceCompatibility = 1.6 +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +sourceSets.main.java.srcDirs = ["src/"] + + +repositories { + flatDir { + dirs '../libs' + } +} + +configurations { + ktlint +} + +task ktlint(type: JavaExec) { + main = "com.github.shyiko.ktlint.Main" + classpath = configurations.ktlint + args "src/**/*.kt" +} + +check.dependsOn ktlint + +task ktlintFormat(type: JavaExec) { + main = "com.github.shyiko.ktlint.Main" + classpath = configurations.ktlint + args "-F", "src/**/*.kt" +} + +dependencies { + compile 'io.sentry:sentry:1.7.3' + compile 'org.slf4j:slf4j-simple:1.7.21' + compile "com.code-disaster.steamworks4j:steamworks4j:1.6.2" + compile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion" + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" + compile "net.dermetfan.libgdx-utils:libgdx-utils:$gdxUtilsVersion" + compile "net.mostlyoriginal.artemis-odb:contrib-core:2.2.0" + compile "net.mostlyoriginal.artemis-odb:contrib-eventbus:2.2.0" + compile "net.mostlyoriginal.artemis-odb:contrib-plugin-profiler:2.2.0" + ktlint 'com.github.shyiko:ktlint:0.27.0' + compile "net.onedaybeard.artemis:artemis-odb:2.1.0" + compile "net.onedaybeard.artemis:artemis-odb-serializer-kryo:2.1.0" + compile("com.esotericsoftware:kryo:4.0.2") + compile("com.esotericsoftware:kryonet:2.22.0-RC1") { + exclude module: 'kryo' + } + compile "com.badlogicgames.gdx:gdx:$gdxVersion" + compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion" + compile "com.badlogicgames.gdx:gdx-controllers:$gdxVersion" + compile "com.badlogicgames.ashley:ashley:$ashleyVersion" + compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" + compile 'com.beust:klaxon:0.30' + // https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all + compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.0-rc-3' + compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0' + +} + +apply plugin: 'org.jetbrains.dokka' + +dokka { + outputFormat = 'javadoc' + outputDirectory = "$buildDir/javadoc" + includeNonPublic = false + skipEmptyPackages = true + jdkVersion = 8 + + packageOptions { + prefix = "uk.me.fantastic.retro.music" + suppress = true + } + packageOptions { + prefix = "uk.me.fantastic.retro.menu" + suppress = true + } + + packageOptions { + prefix = "uk.me.fantastic.retro.network" + suppress = true + } + packageOptions { + prefix = "de.golfgl.gdxgameanalytics" + suppress = true + } +} + +jar { + manifest { + attributes 'Implementation-Version': version + } +} \ No newline at end of file diff --git a/src/de/golfgl/gdxgameanalytics/GameAnalytics.java b/src/de/golfgl/gdxgameanalytics/GameAnalytics.java new file mode 100755 index 0000000..1972afa --- /dev/null +++ b/src/de/golfgl/gdxgameanalytics/GameAnalytics.java @@ -0,0 +1,688 @@ +package de.golfgl.gdxgameanalytics; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.Preferences; +import com.badlogic.gdx.utils.*; + +import java.lang.StringBuilder; +import java.util.HashMap; +import java.util.Map; + +import static uk.me.fantastic.retro.GlobalsKt.log; + +/** + * Gameanalytics.com client for libGDX + *

+ * Created by Benjamin Schulte on 05.05.2018 based up on example implementation + * https://s3.amazonaws.com/download.gameanalytics.com/examples/GameAnalytics+REST+API+example.java + *

+ * (That's the reason why this code looks like made by a server dev - it acutally is. Improvements welcome) + */ + +public class GameAnalytics { + protected static final String URL_SANDBOX = "http://sandbox-api.gameanalytics.com/v2/"; + protected static final String TAG = "Gameanalytics"; + private final static String sdk_version = "rest api v2"; + private static final int FLUSH_QUEUE_INTERVAL = 20; + private static final String URL_GAMEANALYTICS = "https://api.gameanalytics.com/v2/"; + private static final int MAX_EVENTS_SENT = 100; + private static final int MAX_EVENTS_CACHED = 1000; + + // possible TODO: Timer fires on foreground thread, building and compressing content should be done in background + protected Timer.Task pingTask; + + protected String url = URL_GAMEANALYTICS; + protected boolean flushingQueue; + private String game_key = null; + private String secret_key = null; + //dimension information + private String platform = null; + private String os_version = null; + private String device = "unknown"; + private String manufacturer = "unkown"; + //game information + private String build; + //user information + private String user_id = null; + private String session_id; + private int session_num = 0; + private String custom1; + private String custom2; + private String custom3; + //SDK status - this is false when not initialized or initializing failed + private boolean connectionInitialized = false; + private int nextQueueFlushInSeconds = 0; + private Queue waitingQueue = new Queue(); + private Queue sendingQueue = new Queue(); + private int failedFlushAttempts; + private long timeStampDiscrepancy; + private long sessionStartTimestamp; + private Preferences prefs; + + /** + * initializes and starts the session. Make sure you have set all neccessary parameters before calling this + * This can be called but twice, but if it is called when a session is still ongoing, it just resets the session + * start time. + *

+ * Call this on game start and on resume. + */ + public void startSession() { + if (sessionStartTimestamp > 0 && connectionInitialized) { + log(TAG, "No new session started. Session still ongoing"); + sessionStartTimestamp = TimeUtils.millis(); + return; + } + + if (game_key == null || secret_key == null) + throw new IllegalStateException("You must set your game key and secret key"); + + if (platform == null) + setPlatform(GwtIncompatibleStuff.getDefaultPlatform(Gdx.app.getType())); + + if (os_version == null) + throw new IllegalStateException("You need to set a os version"); + + if (prefs == null) + log(TAG, "You did not set up preferences. Session and user tracking will not work without it"); + + loadOrInitUserStringAndSessionNum(); + + session_id = GwtIncompatibleStuff.generateUuid(); + + submitInitRequest(); + // start session is called if request is successful + } + + private void loadOrInitUserStringAndSessionNum() { + if (prefs != null) { + user_id = prefs.getString("ga_userid", null); + session_num = prefs.getInteger("ga_sessionnum", 0); + } + + if (user_id == null || user_id.isEmpty()) { + log(TAG, "No user id found. Generating a new one."); + user_id = GwtIncompatibleStuff.generateUuid(); + + if (prefs != null) + prefs.putString("ga_userid", user_id); + } + + session_num++; + + if (prefs != null) { + prefs.putInteger("ga_sessionnum", session_num); + prefs.flush(); + } + } + + private int loadAndIncrementTransactionNum() { + if (prefs == null) + return 0; + + int transactionNum = prefs.getInteger("ga_transactionnum", 0); + transactionNum++; + prefs.putInteger("ga_transactionnum", transactionNum); + prefs.flush(); + return transactionNum; + } + + /** + * gets called every second by pingtask + */ + protected void flushQueue() { + log("flushQueue"); + if (!connectionInitialized || flushingQueue) + return; + + // countdown to flush + if (nextQueueFlushInSeconds > 0) { + nextQueueFlushInSeconds -= 1; + return; + } + + if (waitingQueue.size == 0 && sendingQueue.size == 0) + return; + + flushingQueue = true; + nextQueueFlushInSeconds = FLUSH_QUEUE_INTERVAL; + + StringBuilder payload = new StringBuilder(); + Json json = new Json(); + json.setOutputType(JsonWriter.OutputType.json); + + synchronized (waitingQueue) { + while (sendingQueue.size < MAX_EVENTS_SENT && waitingQueue.size > 0) + sendingQueue.addLast(waitingQueue.removeFirst()); + + log(TAG, "Sending queue with " + sendingQueue.size + " events"); + payload.append("["); + for (int i = 0; i < sendingQueue.size; i++) { + payload.append(json.toJson(sendingQueue.get(i))); + if (i != sendingQueue.size - 1) + payload.append(","); + } + } + payload.append("]"); + + final Net.HttpRequest request = createHttpRequest(this.url + game_key + "/events", payload.toString()); + //Execute and read response + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + synchronized (waitingQueue) { + sendingQueue.clear(); + } + + int statusCode = httpResponse.getStatus().getStatusCode(); + String resultAsString = httpResponse.getResultAsString(); + + if (statusCode == 200) + log(TAG, statusCode + " " + resultAsString); + else + log(TAG, statusCode + " " + resultAsString); + + failedFlushAttempts = 0; + flushingQueue = false; + } + + @Override + public void failed(Throwable t) { + failed(); + } + + @Override + public void cancelled() { + failed(); + } + + private void failed() { + log(TAG, "Could not send events in queue - probably offline"); + // lengthen the time to the next waitingQueue flush after a fail, but not more than 180 seconds + failedFlushAttempts = Math.min(failedFlushAttempts + 1, 180 / FLUSH_QUEUE_INTERVAL); + nextQueueFlushInSeconds = FLUSH_QUEUE_INTERVAL * (failedFlushAttempts + 1); + log(TAG, "Next flush attempt in " + nextQueueFlushInSeconds + " seconds"); + flushingQueue = false; + } + }); + } + + private Net.HttpRequest createHttpRequest(String url, String payload) { + final Net.HttpRequest request = new Net.HttpRequest("POST"); + request.setUrl(url); + String hash = GwtIncompatibleStuff.setHttpRequestContent(request, payload, secret_key); + request.setHeader("Accept", "application/json"); + request.setHeader("Content-type", "application/json"); + request.setHeader("Authorization", hash); + return request; + } + + private void addToWaitingQueue(AnnotatedEvent event) { + while (waitingQueue.size > MAX_EVENTS_CACHED) + waitingQueue.removeFirst(); + + waitingQueue.addLast(event); + } + + private void submitStartSessionRequest() { + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "user"); + synchronized (waitingQueue) { + addToWaitingQueue(event); + } + } + + public void submitDesignEvent(String event_id) { + if (!isInitialized()) + return; + + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "design"); + event.put("event_id", event_id); + synchronized (waitingQueue) { + log(TAG, "Queuing design event"); + addToWaitingQueue(event); + } + } + + public void submitDesignEvent(String event_id, float value) { + if (!isInitialized()) + return; + + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "design"); + event.put("event_id", event_id); + event.putFloat("value", value); + synchronized (waitingQueue) { + log(TAG, "Queuing design event"); + addToWaitingQueue(event); + } + } + + /** + * Submits a payment transaction to GameAnalytics + * + * @param itemType category for items + * @param itemId identifier for what has been purchased + * @param amount in cents + * @param currency see http://openexchangerates.org/currencies.json + */ + public void submitBusinessEvent(String itemType, String itemId, int amount, String currency) { + if (!isInitialized()) + return; + + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "business"); + event.put("event_id", itemType + ":" + itemId); + event.putInt("amount", amount); + event.put("currency", currency); + event.putInt("transaction_num", loadAndIncrementTransactionNum()); + synchronized (waitingQueue) { + log(TAG, "Queuing business event"); + addToWaitingQueue(event); + } + } + + public void submitProgressionEvent(ProgressionStatus status, String progression01, String progression02, + String progression03) { + submitProgressionEvent(status, progression01, progression02, progression03, 0, 0); + } + + public void submitProgressionEvent(ProgressionStatus status, String progression01, String progression02, + String progression03, int score, int attemptNum) { + if (!isInitialized()) + return; + + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "progression"); + + String event_id = getStatusString(status) + ":" + progression01; + if (progression02.length() > 0) { + event_id += ":" + progression02; + } + if (progression03.length() > 0) { + event_id += ":" + progression03; + } + event.put("event_id", event_id); + + if (status == ProgressionStatus.Complete || status == ProgressionStatus.Fail) { + if (attemptNum > 0) + event.putInt("attempt_num", attemptNum); + if (score > 0) + event.putInt("score", score); + } + synchronized (waitingQueue) { + log(TAG, "Queuing progression event"); + addToWaitingQueue(event); + } + } + + private String getStatusString(ProgressionStatus status) { + switch (status) { + case Start: + return "Start"; + case Fail: + return "Fail"; + default: + return "Complete"; + } + } + + private void createResourceEvent(ResourceFlowType flowType, String virtualCurrency, String itemType, + String itemId, float amount) { + if (!isInitialized()) + return; + + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "resource"); + + String event_id = getFlowTypeString(flowType) + ":" + virtualCurrency + ":" + itemType + ":" + itemId; + event.put("event_id", event_id); + event.putFloat("amount", amount); + synchronized (waitingQueue) { + log(TAG, "Queuing resource event"); + addToWaitingQueue(event); + } + } + + private String getFlowTypeString(ResourceFlowType flowType) { + switch (flowType) { + case Sink: + return "Skink"; + default: + return "Source"; + } + } + + /** + * submits an error event + * + * @param severity + * @param message + */ + public void submitErrorEvent(ErrorType severity, String message) { + if (!isInitialized()) + return; + + if (message.length() > 8000) + message = message.substring(0, 8000); + + AnnotatedEvent event = new AnnotatedEvent(); + event.put("category", "error"); + event.put("severity", getSeverityString(severity)); + event.put("message", message); + synchronized (waitingQueue) { + log(TAG, "Queuing error event (" + message + ")"); + addToWaitingQueue(event); + } + } + + private String getSeverityString(ErrorType severity) { + switch (severity) { + case info: + return "info"; + case debug: + return "debug"; + case error: + return "error"; + case critical: + return "critical"; + default: + return "warning"; + } + } + + /** + * closes the ongoing session. Call this on your game's pause() method + *

+ * This is failsafe - if no session is open, nothing is done + */ + public void closeSession() { + //TODO should get saved for next time, but works well on Android (not on Desktop though) + if (sessionStartTimestamp > 0 && connectionInitialized) { + AnnotatedEvent session_end_event = new AnnotatedEvent(); + session_end_event.put("category", "session_end"); + session_end_event.putInt("length", (int) ((TimeUtils.millis() - sessionStartTimestamp) / 1000L)); + + //this will not work if queue is full. But in that case, the message will probably never get sent + addToWaitingQueue(session_end_event); + flushQueueImmediately(); + } + sessionStartTimestamp = 0; + } + + public void flushQueueImmediately() { + nextQueueFlushInSeconds = 0; + flushQueue(); + } + + /** + * send init request + */ + protected void submitInitRequest() { + Json json = new Json(); + json.setOutputType(JsonWriter.OutputType.json); + + String event = "[" + json.toJson(new InitEvent()) + "]"; + + final Net.HttpRequest request = createHttpRequest(url + game_key + "/init", event); + + connectionInitialized = false; + timeStampDiscrepancy = 0; + + //Execute and read response + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + connectionInitialized = httpResponse.getStatus().getStatusCode() == 200; + String resultAsString = httpResponse.getResultAsString(); + + if (connectionInitialized) { + log(TAG, httpResponse.getStatus().getStatusCode() + " " + resultAsString); + // calculate the client's time stamp discrepancy + + sessionStartTimestamp = TimeUtils.millis(); + try { + JsonValue response = new JsonReader().parse(resultAsString); + long serverTimestamp = response.getLong("server_ts") * 1000L; + timeStampDiscrepancy = serverTimestamp - TimeUtils.millis(); + log(TAG, "Session open. Time stamp discrepancy in ms: " + + timeStampDiscrepancy); + } catch (Exception e) { + // do nothing + } + + submitStartSessionRequest(); + flushQueueImmediately(); + + // add automated task to flush the qeue every 20 seconds + // FIXME if this is called while lockscreen is on, task is not working. Mostly a problem when + // testing with adb + if (pingTask == null) + pingTask = Timer.schedule(new Timer.Task() { + @Override + public void run() { + flushQueue(); + } + }, 1, 1); + } else + log(TAG, "Connection attempt failed: " + httpResponse.getStatus().getStatusCode() + " " + + resultAsString); + } + + @Override + public void failed(Throwable t) { + cancelled(); + } + + @Override + public void cancelled() { + connectionInitialized = false; + log(TAG, "Could not connect to GameAnalytics - suspended"); + } + }); + } + + /** + * @return if events are sent to gameanalytics after a successful login + */ + public boolean isInitialized() { + return connectionInitialized; + } + + /** + * @return current time on server. Only valid after successful initialization, so check {@link #isInitialized()} + * before trusting this value + */ + public long getCurrentServerTime() { + return TimeUtils.millis() + timeStampDiscrepancy; + } + + public void setGameKey(String gamekey) { + this.game_key = gamekey; + } + + public void setGameSecretKey(String secretkey) { + this.secret_key = secretkey; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(Platform platform) { + switch (platform) { + case Windows: + this.platform = "windows"; + return; + case WebGL: + this.platform = "webgl"; + return; + case iOS: + this.platform = "ios"; + return; + case MacOS: + this.platform = "mac_osx"; + return; + case Android: + this.platform = "android"; + return; + case Linux: + this.platform = "linux"; + return; + } + } + + public String getPlatformVersionString() { + return os_version; + } + + /** + * @param os_version must match [0-9]{0,5}(\.[0-9]{0,5}){0,2}$. Unique value limit is 255 + * @return if your version String matched the expected regex. + */ + public boolean setPlatformVersionString(String os_version) { + this.os_version = os_version; + boolean matches = os_version.matches("[0-9]{0,5}(\\.[0-9]{0,5}){0,2}"); + return matches; + } + + public String getGameBuildNumber() { + return build; + } + + /** + * @param build buildnumber of your game. This is a string, so you can also add build type information + * (e.g. "1818_debug", "1205_amazon", "1.5_tv") - but be aware, limit of unit strings is 100 + */ + public void setGameBuildNumber(String build) { + this.build = build; + } + + /** + * @param device device information. Unqiue value limit is 500 + */ + public void setDevice(String device) { + if (device.length() > 30) + device = device.substring(0, 30); + + this.device = device; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + /** + * @param prefs your game's preferences. Needed to save user id and session information. All settings will be + * saved with "ga_" prefix to not interphere with your other settings + */ + public void setPrefs(Preferences prefs) { + this.prefs = prefs; + } + + /** + * @param custom1 value for custom dimension. 50 different values supported at max, max length 32 + */ + public void setCustom1(String custom1) { + this.custom1 = custom1; + } + + /** + * @param custom2 value for custom dimension. 50 different values supported at max, max length 32 + */ + public void setCustom2(String custom2) { + this.custom2 = custom2; + } + + /** + * @param custom3 value for custom dimension. 50 different values supported at max, max length 32 + */ + public void setCustom3(String custom3) { + this.custom3 = custom3; + } + + public enum ProgressionStatus {Start, Fail, Complete} + + public enum ResourceFlowType {Sink, Source} + + public enum ErrorType {debug, info, warning, error, critical} + + /** + * Gameanalytics does not allow free definition of platforms. I did not find a documented list of supported + * platforms, but these ones work + */ + public enum Platform { + Windows, Linux, Android, iOS, WebGL, MacOS + } + + private class InitEvent implements Json.Serializable { + @Override + public void write(Json json) { + json.writeValue("platform", platform); + json.writeValue("os_version", platform + " " + os_version); + json.writeValue("sdk_version", sdk_version); + } + + @Override + public void read(Json json, JsonValue jsonData) { + // not implemented + } + } + + private class AnnotatedEvent implements Json.Serializable { + private Map keyValues = new HashMap(); + private String sessionId; + private int sessionNum; + + public AnnotatedEvent() { + //this is stored + keyValues.put("client_ts", (Long) getCurrentServerTime() / 1000L); + this.sessionId = session_id; + this.sessionNum = session_num; + } + + @Override + public void write(Json event) { + event.writeValue("platform", platform); + event.writeValue("os_version", platform + " " + os_version); + event.writeValue("sdk_version", sdk_version); + event.writeValue("device", device); + event.writeValue("manufacturer", manufacturer); + if (build != null) + event.writeValue("build", build); + event.writeValue("user_id", user_id); + event.writeValue("v", 2); + if (custom1 != null) + event.writeValue("custom_01", custom1); + if (custom2 != null) + event.writeValue("custom_02", custom2); + if (custom3 != null) + event.writeValue("custom_03", custom3); + + event.writeValue("session_id", sessionId); + event.writeValue("session_num", sessionNum); + + for (String key : keyValues.keySet()) { + event.writeValue(key, keyValues.get(key)); + } + } + + @Override + public void read(Json json, JsonValue jsonData) { + // not supported + } + + public void put(String name, String value) { + keyValues.put(name, value); + } + + public void putInt(String name, int value) { + keyValues.put(name, value); + } + + public void putFloat(String name, float value) { + keyValues.put(name, value); + } + } +} diff --git a/src/de/golfgl/gdxgameanalytics/GwtIncompatibleStuff.java b/src/de/golfgl/gdxgameanalytics/GwtIncompatibleStuff.java new file mode 100755 index 0000000..c6f286c --- /dev/null +++ b/src/de/golfgl/gdxgameanalytics/GwtIncompatibleStuff.java @@ -0,0 +1,104 @@ +package de.golfgl.gdxgameanalytics; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.utils.Base64Coder; +import com.badlogic.gdx.utils.SharedLibraryLoader; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.UUID; +import java.util.zip.GZIPOutputStream; + +/** + * Created by Benjamin Schulte on 05.05.2018. + */ + +public class GwtIncompatibleStuff { + + private static String generateHash(byte[] json, String secretKey) { + try { + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + byte[] encoded = secretKey.getBytes(); + SecretKeySpec secretKeySpec = new SecretKeySpec(encoded, "HmacSHA256"); + sha256_HMAC.init(secretKeySpec); + return new String(Base64Coder.encode(sha256_HMAC.doFinal(json))); + } catch (Exception ex) { + Gdx.app.error(GameAnalytics.TAG, "Error generating Hmac: " + ex.toString()); + return ""; + } + } + + /** + * @return UUID on Java, a nearly-UUID on GWT + */ + public static String generateUuid() { + UUID sid = UUID.randomUUID(); + return sid.toString(); + } + + protected static GameAnalytics.Platform getDefaultPlatform(Application.ApplicationType type) { + switch (type) { + case Android: + return GameAnalytics.Platform.Android; + case WebGL: + return GameAnalytics.Platform.WebGL; + default: + if (SharedLibraryLoader.isWindows) + return GameAnalytics.Platform.Windows; + else if (SharedLibraryLoader.isLinux) + return GameAnalytics.Platform.Linux; + else if (SharedLibraryLoader.isIos) + return GameAnalytics.Platform.iOS; + else if (SharedLibraryLoader.isMac) + return GameAnalytics.Platform.MacOS; + else + throw new IllegalStateException("You need to set a platform"); + } + } + + /** + * sets the http request content, zipped if possible. + * + * @return header for authentication + */ + protected static String setHttpRequestContent(Net.HttpRequest request, String content, String secretKey) { + byte[] compressedContent = null; + String hash; + + try { + compressedContent = compress(content); + } catch (Throwable t) { + // do nothing + } + + Gdx.app.debug(GameAnalytics.TAG, content); + + if (compressedContent != null) { + Gdx.app.debug(GameAnalytics.TAG, "(Compressed from " + content.length() + + " to " + compressedContent.length + " bytes)"); + + request.setContent(new ByteArrayInputStream(compressedContent), compressedContent.length); + hash = GwtIncompatibleStuff.generateHash(compressedContent, secretKey); + request.setHeader("Content-Encoding", "gzip"); + } else { + hash = GwtIncompatibleStuff.generateHash(content.getBytes(), secretKey); + request.setContent(content); + } + return hash; + } + + public static byte[] compress(String paramString) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(paramString.length()); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); + gzipOutputStream.write(paramString.getBytes()); + gzipOutputStream.close(); + byte[] bytes = byteArrayOutputStream.toByteArray(); + byteArrayOutputStream.close(); + return bytes; + } +} diff --git a/src/uk/me/fantastic/retro/AbstractGameFactory.kt b/src/uk/me/fantastic/retro/AbstractGameFactory.kt new file mode 100644 index 0000000..e491a5d --- /dev/null +++ b/src/uk/me/fantastic/retro/AbstractGameFactory.kt @@ -0,0 +1,47 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.graphics.Texture +import uk.me.fantastic.retro.menu.MultiChoiceMenuItem +import uk.me.fantastic.retro.screens.GameSession + +/** + * GameFactories provide RetroWar with info about a game and generate an + * instance of the game on demand, applying any configuration that has been + * stored in the factory. + * + * If you are making a stand-alone SimpleGame you don't need this, just create + * a SimpleGameFactory. But if you are making a plugin for RetroWar then you need to write + * your own subclass of AbstractGameFactory. + * + * @property name Game name + * @property levels a List of level names, only for games that have multiple levels. May be null. + */ +abstract class AbstractGameFactory(val name: String, val levels: List? = null) { + + /** Currently selected level number */ + var level = 0 + + /** Whether the game should be shown on the main menu or relegated to the 'mods' menu */ + var showOnGamesMenu = true + + /** Texture screenshot or logo to display on menu */ + abstract val image: Texture + + /** Description displayed on menu */ + abstract val description: String + + abstract fun create(session: GameSession): Game + + /** For mult-game tournaments ignore the settings in this factory and create a game with some + * defaults appropriate for a tournament */ + open fun createWithDefaultSettings(session: GameSession): Game { + return create(session) + } + + /** + * Any MenuItems in this List will be displayed by RetroWar on an option screen + * It's a way to configure the Factory via a GUI + * If there are none, just leave List empty + */ + open val options: List = ArrayList() +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/App.kt b/src/uk/me/fantastic/retro/App.kt new file mode 100644 index 0000000..5de93ab --- /dev/null +++ b/src/uk/me/fantastic/retro/App.kt @@ -0,0 +1,283 @@ +// +/* + Copyright 2018 Richard Smith. + + RetroWar 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. + + RetroWar is distributed in the hope that 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 RetroWar. If not, see . +*/ +// +package uk.me.fantastic.retro + +import com.badlogic.gdx.Application +import com.badlogic.gdx.Game +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import com.badlogic.gdx.InputAdapter +import com.badlogic.gdx.Screen +import com.badlogic.gdx.controllers.Controllers +import com.codedisaster.steamworks.SteamAPI +import de.golfgl.gdxgameanalytics.GameAnalytics +import uk.me.fantastic.retro.Prefs.BinPref +import uk.me.fantastic.retro.input.GamepadInput +import uk.me.fantastic.retro.input.KeyboardMouseInput +import uk.me.fantastic.retro.input.MappedController +import uk.me.fantastic.retro.input.SimpleTouchscreenInput +import uk.me.fantastic.retro.input.StatefulController +import uk.me.fantastic.retro.music.ibxm.IBXMPlayer +import uk.me.fantastic.retro.network.Client +import uk.me.fantastic.retro.network.Server +import uk.me.fantastic.retro.screens.GameSession +import uk.me.fantastic.retro.utils.RetroShader +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.net.URL +import kotlin.concurrent.thread + +/** + * Main libgdx common application class. Created by platform specific launchers. + * Delegates actual rendering loop to Screens + * + * For most games you can use SimpleApp rather than making your own subclass of App. + * + * @param callback For setting maximum FPS, platform specific + * @param logger If debug is on then logs are sent here, which may send them to screen, file or + * cloud. + * @param manualGC GDX on iOS has very poor garbage collection. Supply one of these to disable + * it and do your own GC. Otherwise null. + */ +abstract class App(val callback: Callback, val logger: Logger, val manualGC: ManualGC? = null) : + Game() { + + /** Title screen */ + var title: Screen? = null + + /** If you are using GameAnalytics service set this, otherwise null */ + var gameAnalytics: GameAnalytics? = null + + companion object { + /** A static reference to the singleton Application */ + @JvmStatic + lateinit var app: App + + val LOG_FILE_PATH: String = System.getProperty("user.home") + File.separator + "retrowar-log.txt" + val PREF_DIR: String = System.getProperty("user.home") + File.separator + ".prefs" + } + + init { + app = this + findIPaddress() + } + + protected fun findIPaddress() { + thread { + try { + val whatismyip = URL("http://checkip.amazonaws.com") + val i = BufferedReader(InputStreamReader(whatismyip.openStream())) + ip = i.readLine() + } catch (e: Exception) { + } + } + } + + /** Uses the Callback to set max FPS, if the platform supports it */ + fun setFPS(f: Int) { + callback.setForegroundFPS(f) + callback.setBackgroundFPS(f) + } + + /** Setup network stuff, not currently working */ + protected fun initialiseNetwork() { + server = Server() + server?.initialise() + client = Client() + client?.initialise() + } + + internal val mappedControllers = ArrayList() + + internal val statefulControllers = ArrayList() + + /** Current IP address, if known. Else "unknown" */ + var ip: String = "unknown" + + /** May be null if no Server */ + var server: Server? = null + + /** May be null if no Client */ + var client: Client? = null + + lateinit var controllerTest: GameFactory + lateinit var screenTest1: GameFactory + lateinit var screenTest2: GameFactory + + val ibxmPlayer = IBXMPlayer() + + internal var mouseClicked = false + + lateinit var shader: RetroShader + + fun anyKeyHit(): Boolean { + return statefulControllers.any { it.isButtonAJustPressed } || + app.mouseJustClicked || + Gdx.input.isKeyJustPressed(Input.Keys.SPACE) || + Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE) + } + + fun testSandbox(): String { + return (App::class.java.name + ": I shouldnt be able to do this: " + System.getProperty("os" + + ".name")) + } + + val mouseJustClicked: Boolean + get() { + val t = mouseClicked + mouseClicked = false + return t + } + + val versionString = (App::class.java.`package`.implementationVersion ?: "devel") + + fun swapScreenAndDispose(screen: Screen) { + log("swapscreen") + val s = app.screen + app.setScreen(screen) + s.dispose() + } + + fun showTitleScreen() { + val title = title + if (title != null) { + swapScreenAndDispose(title) + } else { + log("There is no titlescreen so exiting") + quit() + } + } + + abstract fun quit() + + fun clearEvents() { + statefulControllers.forEach { it.clearEvents() } + mouseJustClicked // eat the event if there is a click already waiting + } + + protected fun initialisePrefs() { + BinPref.values().forEach(BinPref::apply) + Prefs.MultiChoicePref.LIMIT_FPS.apply() + } + + protected fun initialiseControllers() { + println("Detected ${Controllers.getControllers().size} controllers") + + Controllers.getControllers().mapTo(mappedControllers, ::MappedController) + + mappedControllers.mapTo(statefulControllers, ::StatefulController) + } + + protected fun initializeInput() { + Gdx.input.inputProcessor = object : InputAdapter() { + override fun touchUp(x: Int, y: Int, pointer: Int, button: Int): Boolean { + log("app touchdown") + mouseClicked = true + return true + } + } + } + + protected fun initialiseDesktop() { + } + + protected fun initialiseAndroid() { + if (Gdx.app.type == Application.ApplicationType.Android) { + Gdx.input.isCatchBackKey = true + } + } + + fun initialiseShader() { + shader = RetroShader("shaders/" + Prefs.MultiChoicePref.SHADER.getString()) + } + + protected fun initialiseSteam() { + System.out.println("Initialise Steam client API ...") + + if (!SteamAPI.init()) { + log("steam error") + SteamAPI.printDebugInfo(System.err) + } + + SteamAPI.printDebugInfo(System.out) + } + + fun setScreenMode() { + if (BinPref.FULLSCREEN.isEnabled()) { + Gdx.graphics.setFullscreenMode(Gdx.graphics.displayMode) + } else { + Gdx.graphics.setWindowedMode(832, 512) + } + Gdx.graphics.setVSync(BinPref.VSYNC.isEnabled()) + } + + open fun submitAnalytics(s: String) { + gameAnalytics?.submitDesignEvent(s) + } + + fun configureSessionWithPreSelectedInputDevice(session: GameSession){ + if (Gdx.app.type == Application.ApplicationType.Desktop) { + val controller1 = App.app.mappedControllers.firstOrNull() + if (controller1 != null) { + session.preSelectedInputDevice = GamepadInput(controller1) + } else { + session.preSelectedInputDevice = KeyboardMouseInput(session) + session.KBinUse = true + } + } else if (isMobile) { + session.preSelectedInputDevice = SimpleTouchscreenInput() + } + } +} + +// class SingleGameAppFromClass(callback: Callback, val name: String, val gameClazz: Class, val screenClazz: Class, val t: Screen? = null) : App(callback) { +// +// val factory: AbstractGameFactory = +// GameFactory(name = name, +// createGame = +// { session: GameSession -> (gameClazz.getConstructor(GameSession::class.java).newInstance(session)) } +// ) +// +// +// override fun create() { +// log("SingleGameAppFromClass create") +// +// app = this +// games = listOf(factory) +// +// initialiseAndroid() +// initialiseDesktop() +// setPrefsToDefaultsForSingleGames() +// initialisePrefs() +// initializeInput() +// initialiseControllers() +// +// +// val game = GameSession(factory) +// +// if (screenClazz != null) { +// title = screenClazz.newInstance() +// setScreen(title) +// } else { +// setScreen(game) +// } +// +// } +// } diff --git a/src/uk/me/fantastic/retro/ControllerTester.kt b/src/uk/me/fantastic/retro/ControllerTester.kt new file mode 100644 index 0000000..21aa85f --- /dev/null +++ b/src/uk/me/fantastic/retro/ControllerTester.kt @@ -0,0 +1,125 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.controllers.Controllers +import com.badlogic.gdx.controllers.PovDirection +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.g2d.Batch +import com.badlogic.gdx.utils.Align +import com.badlogic.gdx.utils.GdxRuntimeException +import uk.me.fantastic.retro.App.Companion.app +import uk.me.fantastic.retro.screens.GameSession + +/** + * A simple RetroWar game + */ +class ControllerTester(session: GameSession) // Constructor (required) +// width and height of screen in pixels + : SimpleGame(session, 640f, 480f) { + + override fun doLogic(deltaTime: Float) { // Called automatically every frame + } + + override fun doDrawing(batch: Batch) { // called automatically every frame + + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) // clear the screen + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + font.draw(batch, "Controllers connected: ${Controllers.getControllers().size}", 0f, 20f) + // batch.begin() + + var x = 0f + for (i in 0..app.mappedControllers.lastIndex) { + var y = 472f + val m = app.mappedControllers[i] + val c = m.controller + font.draw(batch, c.name, x, y, 256f, Align.left, false) + y -= 8 + font.draw(batch, m.mapping, x, y, 256f, Align.left, false) + for (j in 0..31) { + if (c.getButton(j)) { + y -= 8f + val mapping: String = when (j) { + m.A -> "A" + m.B -> "B" + m.X -> "X" + m.Y -> "Y" + m.L_BUMPER -> "L_BUMPER" + m.R_BUMPER -> "R_BUMPER" + m.GUIDE -> "GUIDE" + m.BACK -> "BACK" + m.START -> "START" + m.DPAD_DOWN -> "DPAD_DOWN" + m.DPAD_UP -> "DPAD_UP" + m.DPAD_LEFT -> "DPAD_LEFT" + m.DPAD_RIGHT -> "DPAD_RIGHT" + m.L_TRIGGER -> "L_TRIGGER" + m.R_TRIGGER -> "R_TRIGGER" + m.R_STICK_PUSH -> "R_STICK_PUSH" + m.L_STICK_PUSH -> "L_STICK_PUSH" + else -> "UNMAPPED BUTTON $j" + } + font.draw(batch, mapping, x, y, 256f, Align.left, false) + } + } + + for (j in 0..31) { + if (c.getAxis(j) != 0f) { + y -= 8f + val mapping: String = when (j) { + m.L_STICK_HORIZONTAL_AXIS -> "L_STICK_HORIZONTAL_AXIS" + m.R_STICK_HORIZONTAL_AXIS -> "R_STICK_HORIZONTAL_AXIS" + m.L_STICK_VERTICAL_AXIS -> "L_STICK_VERTICAL_AXIS" + m.R_STICK_VERTICAL_AXIS -> "R_STICK_VERTICAL_AXIS" + m.L_TRIGGER_AXIS -> "L_TRIGGER_AXIS" + m.R_TRIGGER_AXIS -> "R_TRIGGER_AXIS" + else -> "UNMAPPED AXIS $j" + } + font.draw(batch, "$mapping: ${c.getAxis(j)} ", x, y, 256f, Align.left, false) + } + } + for (j in 0..31) { + if (c.getPov(j) != PovDirection.center) { + y -= 8f + val mapping: String = when (j) { + m.DPAD -> "DPAD" + else -> "UNKNOWN DPAD $j" + } + font.draw(batch, "$mapping ${c.getPov(j)}", x, y, 256f, Align.left, false) + } + } + try { + + for (j in 0..31) { + y -= 8f + font.draw(batch, "Accel$j: ${c.getAccelerometer(j)}", x, y, 256f, Align.left, false) + } + } catch (e: GdxRuntimeException) { + } + + for (j in 0..31) { + if (c.getSliderX(j)) { + y -= 8f + font.draw(batch, "XSlider$j: ${c.getSliderX(j)}", x, y, 256f, Align.left, false) + } + } + for (j in 0..31) { + if (c.getSliderY(j)) { + y -= 8f + font.draw(batch, "YSlider$j: ${c.getSliderY(j)}", x, y, 256f, Align.left, false) + } + } + x += 256f + } + + // batch.end() + } + + // These methods must be implemented but don't have to do anything + override fun show() { + } + + override fun hide() {} + + override fun dispose() {} +} diff --git a/src/uk/me/fantastic/retro/FBORenderer.kt b/src/uk/me/fantastic/retro/FBORenderer.kt new file mode 100644 index 0000000..187237d --- /dev/null +++ b/src/uk/me/fantastic/retro/FBORenderer.kt @@ -0,0 +1,203 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Camera +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.g2d.Batch +import com.badlogic.gdx.graphics.g2d.GlyphLayout +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.glutils.ShapeRenderer +import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType.Filled +import com.badlogic.gdx.math.MathUtils +import com.badlogic.gdx.math.Vector3 +import uk.me.fantastic.retro.Prefs.BinPref.FPS + +/** + * Renders sprites to a FrameBufferObject and thence to the screen + * This is the version where I attempt to add new features and probably fuck up how it works + * Does not support bilinear filtering when smooth motion is enabled + * Creates new objects every frame, not sure how heavy they are or if they could better be pooled and reused + */ +class FBORenderer(val WIDTH: Float, val HEIGHT: Float, val fadeInEffect: Boolean) { + + var cam: OrthographicCamera = setupCam(Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat()) + internal var shape = ShapeRenderer(5000, createDefaultShapeShader()) + internal var batch = SpriteBatch(1000, createDefaultShader()) + + internal var scaleFactor = 1f + + var timer = 0.0f + + internal var fboBatch = SpriteBatch(100, createDefaultShader()) + internal var glyphLayout = GlyphLayout() + + private val mFBO = ManagedFBO() + + fun renderFBOtoScreen() { + endFBO() + + cam.update() + fboBatch.projectionMatrix = cam.combined + + Gdx.gl.glClearColor(0f, 0f, 0.2f, 1f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + Prefs.BinPref.BILINEAR.filter(mFBO.texture) + + fboBatch.begin() + + mFBO.updateShader(fboBatch) + + fboBatch.draw( + mFBO.texture, + 0f, 0f, + 0f, 0f, + // mFBO.width, mFBO.height, + WIDTH, HEIGHT, + 1f, 1f, + 0f, + 0, 0, + mFBO.width.toInt(), mFBO.height.toInt(), + // WIDTH.toInt(), HEIGHT.toInt(), + false, true + ) + + fboBatch.end() + + drawScanlines(shape, cam) + } + + fun beginFBO(): Batch { + timer += Gdx.graphics.deltaTime + + if (fadeInEffect && timer < 3f) { + scaleFactor = 4f * (-MathUtils.log2(timer) + 1.5f) + if (scaleFactor < 1f) scaleFactor = 1f + } else { + scaleFactor = 1f + } + + if (Prefs.BinPref.SMOOTH.isEnabled()) { + mFBO.resizeToScreenSize(WIDTH, HEIGHT) + } else { + mFBO.resize(WIDTH, HEIGHT, scaleFactor) + } + mFBO.begin() + + batch.projectionMatrix = mFBO.projectionMatrix + + return batch + } + + fun getShape(): ShapeRenderer { + shape.projectionMatrix = mFBO.projectionMatrix + return shape + } + + private fun endFBO() { + batch.begin() + if (FPS.isEnabled()) { + Resources.FONT_ENGLISH.setColor(Color.WHITE) + glyphLayout.setText(Resources.FONT_ENGLISH, "FPS ${Gdx.graphics.framesPerSecond}") + Resources.FONT_ENGLISH.draw(batch, glyphLayout, WIDTH / 2 - glyphLayout.width / 2, HEIGHT + 1) + } + + batch.end() + + mFBO.end() + } + + fun resize(width: Int, height: Int) { + log("FBOrenderer resize") + // mFBO.resizeToScreenSize(WIDTH, HEIGHT, scaleFactor, m) + + cam = setupCam(width.toFloat(), height.toFloat()) + } + + // fixme messes up batch somehow + fun darkenScreen(c: Color) { + // batch.end() + + shape.projectionMatrix = mFBO.projectionMatrix + + Gdx.gl.glEnable(GL20.GL_BLEND) + Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA) + + shape.begin(Filled) + shape.color = c + + shape.rect(0f, 0f, WIDTH, HEIGHT) + + shape.end() + + Gdx.gl.glDisable(GL20.GL_BLEND) + + // batch.begin() + } + + var scaledWidth = 0f + var scaledHeight = 0f + var m = 0f + + fun setupCam(x: Float, h: Float): OrthographicCamera { + val w = x + // val m: Float + m = findAppropriateScaleFactor(w, h) + scaledWidth = WIDTH * m + scaledHeight = HEIGHT * m + log("setupcam $scaledWidth $scaledHeight $m") + val cam = OrthographicCamera((w) / m, h / m) + cam.translate((WIDTH / 2), (HEIGHT / 2)) + cam.update() + return cam + } + + fun findAppropriateScaleFactor(w: Float, h: Float): Float = + if (Prefs.BinPref.STRETCH.isEnabled()) findHighestScaleFactor(w, h) + else findHighestIntegerScaleFactor(w, h) + + fun findHighestIntegerScaleFactor(width: Float, height: Float): Float { + val w = width / WIDTH + val h = height / HEIGHT + return if (w < h) w.roundDown() else h.roundDown() + } + + fun findHighestScaleFactor(width: Float, height: Float): Float { + val w = width / WIDTH + val h = height / HEIGHT + return if (w < h) w else h + } + + fun drawScanlines(shape: ShapeRenderer, cam: Camera) { + if (Prefs.BinPref.SCANLINES.isEnabled()) { + shape.projectionMatrix = cam.combined + + Gdx.gl.glEnable(GL20.GL_BLEND) + Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA) + + shape.begin(ShapeRenderer.ShapeType.Line) + shape.setColor(0.0f, 0.0f, 0.0f, 0.5f) + + for (i in 0..HEIGHT.toInt()) { + val y = i.toFloat() + shape.line(0f, y, WIDTH, y) + } + shape.end() + + Gdx.gl.glDisable(GL20.GL_BLEND) + } + } + + fun convertGameCoordsToScreenCoords(x: Float, y: Float): Vector3 { + return cam.project(Vector3(x, y, 0f)) + } + + fun convertScreenToGameCoords(x: Int, y: Int): Vector3 { + val g = cam.unproject(Vector3(x.toFloat(), y.toFloat(), 0f)) + // log("convertcoords","${g.x} ${g.y}") + // g.y = Renderer.HEIGHT-g.y + return g + } +} diff --git a/src/uk/me/fantastic/retro/Game.kt b/src/uk/me/fantastic/retro/Game.kt new file mode 100644 index 0000000..8432e5c --- /dev/null +++ b/src/uk/me/fantastic/retro/Game.kt @@ -0,0 +1,41 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.math.MathUtils +import uk.me.fantastic.retro.screens.GameSession +import uk.me.fantastic.retro.utils.Vec + +/** + * top the hierarchy, most abstract kind of Game we support. if you implement this you will do most everything youself. + * most games probably want to implement SimpleGame subclass instead to get menus. + */ +abstract class Game(val session: GameSession) { + + interface UsesMouseAsInputDevice { + fun getMouse(): Vec + } + + open val MAX_FPS = 1000f + open val MIN_FPS = 10f + + val players: ArrayList + get() = session.players + + open fun gameover() { + session.quit() + } + + abstract fun show() + abstract fun hide() + + abstract fun resize(width: Int, height: Int) + abstract fun render(deltaTime: Float) + abstract fun postMessage(s: String) + + abstract val renderer: FBORenderer + abstract fun dispose() + fun renderAndClampFramerate() { + val delta = MathUtils.clamp(Gdx.graphics.rawDeltaTime, 1f / MAX_FPS, 1f / MIN_FPS) + render(delta) + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/GameFactory.kt b/src/uk/me/fantastic/retro/GameFactory.kt new file mode 100644 index 0000000..3c0269c --- /dev/null +++ b/src/uk/me/fantastic/retro/GameFactory.kt @@ -0,0 +1,29 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Texture +import uk.me.fantastic.retro.screens.GameSession + +/** + * produces Games. you may want to subclass this for your own Game, but you may be able + * use it as-is by passing in your own constructor method + */ +open class GameFactory(name: String, val createGame: (GameSession) -> Game, val i: Texture? = null) : AbstractGameFactory(name) { + + val default = Texture(Gdx.files.internal("badlogic.jpg")) + + override val description: String = name + + override val image: Texture + get() { + if (i != null) { + return i + } else { + return default + } + } + + override fun create(session: GameSession): Game { + return createGame(session) + } +} diff --git a/src/uk/me/fantastic/retro/ManagedFBO.kt b/src/uk/me/fantastic/retro/ManagedFBO.kt new file mode 100644 index 0000000..e24460e --- /dev/null +++ b/src/uk/me/fantastic/retro/ManagedFBO.kt @@ -0,0 +1,64 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.Pixmap +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.glutils.FrameBuffer +import com.badlogic.gdx.math.Vector2 +import uk.me.fantastic.retro.App.Companion.app + +class ManagedFBO { + val MAX_WIDTH = Gdx.graphics.displayMode.width + val MAX_HEIGHT = Gdx.graphics.displayMode.height + private var fbo: FrameBuffer = FrameBuffer(Pixmap.Format.RGB888, MAX_WIDTH, MAX_HEIGHT, false) + internal var fboCam: OrthographicCamera = OrthographicCamera() + + val texture + get() = fbo.colorBufferTexture + + var width: Float = 0f + var height: Float = 0f + + val projectionMatrix + get() = fboCam.combined + + fun resize(w: Float, h: Float, scale: Float) { + width = Math.max(w / scale, 1f) + height = Math.max(h / scale, 1f) + + val camWidth = MAX_WIDTH * scale // renderer.WIDTH + val camHeight = MAX_HEIGHT * scale // renderer.HEIGHT + fboCam.setToOrtho(false, camWidth, camHeight) + + fboCam.position.set(camWidth / 2f, camHeight / 2f, 0f) + fboCam.update() + } + + fun resizeToScreenSize(w: Float, h: Float) { + width = Gdx.graphics.width.toFloat() + height = Gdx.graphics.height.toFloat() + + val camWidth = MAX_WIDTH.toFloat() / (width / w) + val camHeight = MAX_HEIGHT.toFloat() / (height / h) + + fboCam.setToOrtho(false, camWidth, camHeight) + // fboCam.position.set((camWidth / 2f).roundToInt().toFloat(), camHeight / 2f, 0f) + fboCam.update() + } + + fun begin() { + fbo.begin() + } + + fun end() { + fbo.end() + } + + fun updateShader(fboBatch: SpriteBatch) { + val outVec = Vector2(Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat()) + val inVec = Vector2(width, height) + val textureSize = Vector2(Gdx.graphics.displayMode.width.toFloat(), Gdx.graphics.displayMode.height.toFloat()) + app.shader.process(fboBatch, textureSize, inVec, outVec) + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/Player.kt b/src/uk/me/fantastic/retro/Player.kt new file mode 100644 index 0000000..4d87d89 --- /dev/null +++ b/src/uk/me/fantastic/retro/Player.kt @@ -0,0 +1,63 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.math.MathUtils +import uk.me.fantastic.retro.input.InputDevice + +/** + * + */ +open class Player( + @Transient val input: InputDevice, + val name: String, + val color: Color, + val color2: + Color +) : + Comparable { + + override fun compareTo(other: Player): Int { + return score.compareTo(other.score) + } + + var entityId: Int = -1 // might not be used by most games but convenient for those that use it to have it here + + var score: Int = 0 + var deaths: Int = 0 + var healthLost: Float = 0f + + var metaScore: Int = 0 + private set(value) {field = value} + + fun incMetaScore(i: Int=1){ + metaScore+=i + } + + var startingHealth: Float = 0f + + fun healthDisplayString(startingHealth: Float): String { + val healthLeft = MathUtils.clamp((startingHealth - healthLost).toInt(), 0, 10) + val health = "#".repeat(healthLeft) + return health + } + + fun livesDisplayString(startingLives: Int): String { + val livesLeft = MathUtils.clamp(startingLives - deaths, 0, 10) + + val lives = "*".repeat(livesLeft) + + return lives + } + + fun isOutOfLives(lives: Int): Boolean { + return deaths >= lives + } + + fun reset() { + score = 0 + deaths = 0 + healthLost = 0f + } + + // constructor() : this(null, "", -1, Color.WHITE) +} diff --git a/src/uk/me/fantastic/retro/Prefs.kt b/src/uk/me/fantastic/retro/Prefs.kt new file mode 100644 index 0000000..3878fa0 --- /dev/null +++ b/src/uk/me/fantastic/retro/Prefs.kt @@ -0,0 +1,357 @@ +// +/* + Copyright 2018 Richard Smith. + + RetroWar 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. + + RetroWar is distributed in the hope that 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 RetroWar. If not, see . +*/ +// +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Preferences +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.Texture.TextureFilter.Linear +import com.badlogic.gdx.graphics.Texture.TextureFilter.Nearest +import com.badlogic.gdx.graphics.g2d.TextureRegion +import com.badlogic.gdx.math.MathUtils +import uk.me.fantastic.retro.App.Companion.app + +import uk.me.fantastic.retro.Resources.Companion.TEXT + +/** + * Stores stuff in GDX preference files, but also provides singleton/enums for use in creating option Menus + */ +object Prefs { + + private var name = System.getProperty("sun.java.command")?.substringBefore(' ') + private var fileName = name ?: "uk.me.fantastic.retro" + + var prefs: Preferences = Gdx.app.getPreferences(fileName) + + val shaders = makeShaderList() + + val colors = listOf( + Color(0, 0, 0), // 0: black + Color(157, 157, 157), // 1: grey + Color(255, 255, 255), // 2: white + Color(190, 38, 51), // 3: red + Color(224, 111, 139), // 4: pink + Color(73, 60, 43), // 5: dbrown + Color(164, 100, 34), // 6: lbrown + Color(235, 137, 49), // 7: orange + Color(247, 226, 107), // 8: yellow + Color(47, 72, 78), // 9: unknown + Color(68, 137, 26), // 10: dgreen + Color(163, 206, 39), // 11: lgreen + Color(27, 38, 50), // 12: ddblue + Color(0, 87, 132), // 13: dblue + Color(49, 162, 242), // 14: blue + Color(178, 220, 239) // 15: lblue + ).map { it.toString() } + + private fun makeShaderList(): List { + val shaderFiles = Gdx.files.internal("shaders").list("glsl").map { it.name() } + return listOf("NONE") + shaderFiles + } + + /** @suppress */ + enum class MultiChoicePref(val pref: String, vararg val choices: String, val default: Int = 0) { + GRAPHICS("graphics", "retro", "modern", "CRT") { + override fun apply() { + when (getNum()) { + 0 -> { + BinPref.SMOOTH.disable() + BinPref.BILINEAR.enable() + BinPref.SCANLINES.enable() + SHADER.set(0) + app.initialiseShader() + } + 1 -> { + BinPref.SMOOTH.enable() + BinPref.BILINEAR.disable() + BinPref.SCANLINES.disable() + SHADER.set(0) + app.initialiseShader() + } + else -> { + BinPref.SMOOTH.disable() + BinPref.BILINEAR.disable() + BinPref.SCANLINES.disable() + SHADER.set(1) + app.initialiseShader() + } + } + } + }, + + SHADER("shader", *shaders.toTypedArray()) { + override fun apply() { + app.initialiseShader() + BinPref.BILINEAR.disable() + BinPref.SMOOTH.disable() + BinPref.SCANLINES.disable() + BinPref.STRETCH.enable() + } + }, + PLAYER1_COLOR("player1_color", *(colors.map { it.toString() }.toTypedArray()), default = 9), + PLAYER1_COLOR2("player1_color2", *(colors.map { it.toString() }.toTypedArray()), default = 15), + PLAYER2_COLOR("player2_color", *(colors.map { it.toString() }.toTypedArray()), default = 3), + PLAYER2_COLOR2("player2_color2", *(colors.map { it.toString() }.toTypedArray()), default = 4), + PLAYER3_COLOR("player3_color", *(colors.map { it.toString() }.toTypedArray()), default = 13), + PLAYER3_COLOR2("player3_color2", *(colors.map { it.toString() }.toTypedArray()), default = 14), + PLAYER4_COLOR("player4_color", *(colors.map { it.toString() }.toTypedArray()), default = 10), + PLAYER4_COLOR2("player4_color2", *(colors.map { it.toString() }.toTypedArray()), default = 11), + PLAYERGUEST_COLOR("playerguest_color", *(colors.map { it.toString() }.toTypedArray()), default = 5), + PLAYERGUEST_COLOR2("playerguest_color2", *(colors.map { it.toString() }.toTypedArray()), default = 7), + LIMIT_FPS("limitfps", "0", "30", "60") { + override fun apply() { + App.app.setFPS(getString().toInt()) + } + }; + + fun next() { + val n = getNum() + 1 + + if (n > choices.lastIndex) { + set(0) + } else { + set(n) + } + } + + fun prev() { + val n = getNum() - 1 + if (n < 0) { + set(choices.lastIndex) + } else { + set(n) + } + } + + fun set(n: Int) { + prefs.putInteger(pref, MathUtils.clamp(n, 0, choices.lastIndex)) + prefs.flush() + apply() + } + + fun displayText(): String { + return choices[prefs.getInteger(pref, default)] + } + + fun getString(): String { + return choices[prefs.getInteger(pref, default)] + } + + fun getNum(): Int { + return prefs.getInteger(pref, default) + } + + fun reset() { + set(default) + } + + open fun apply() {} + } + + /** @suppress */ + enum class BinPref( + val pref: String, + val text: String = pref, + val tText: String = TEXT["on"], + val fText: + String = TEXT["off"], + val default: Boolean = true + ) { + VSYNC("vsync") { + override fun apply() { + Gdx.graphics.setVSync(VSYNC.isEnabled()) + } + }, + STRETCH("stretch", tText = TEXT["stretched"], fText = TEXT["pixelPerfect"], default = true) { + override fun apply() { + App.app.resize(Gdx.graphics.width, Gdx.graphics.height) + } + }, + SCANLINES("scanlines", default = true), + SMOOTH("smooth", tText = "FAKE but SMOOTH", fText = "GENUINE", default = false), + SPLASH("splash", default = true), + // PROFANITY("profanity", default = false) { +// override fun apply() { +// var locale = Locale(Resources.defaultLocale.language, "", if (PROFANITY.isEnabled()) "profane" else "") +// Resources.TEXT = I18NBundle.createBundle(Resources.baseFileHandle, locale) +// } +// }, + BILINEAR("bilinear", tText = TEXT["on"], fText = TEXT["off"], default = true) { + override fun apply() {} + }, + DEBUG("debug", tText = TEXT["on"], fText = TEXT["off"], default = false) { + override fun apply() {} + }, + CRASH_REPORTS("crashreports", tText = TEXT["on"], fText = TEXT["off"], default = true) { + override fun apply() {} + }, + AUTOFIRE("autofire", tText = TEXT["on"], fText = TEXT["off"], default = false) { + override fun apply() {} + }, + MUSIC("music", tText = TEXT["on"], fText = TEXT["off"]) { + override fun apply() { + } + }, + FPS("fps", default = false) { + override fun apply() {} + }, + FULLSCREEN("fullscreen", tText = TEXT["fullscreen"], fText = TEXT["windowed"]) { + override fun apply() { + if (!isWindows) { + app.setScreenMode() + } + } + }; + + fun displayText(): String { + if (isEnabled()) return tText + else return fText + } + + fun enable() { + log("enabled " + this) + prefs.putBoolean(pref, true) + prefs.flush() + apply() + } + + fun disable() { + log("disabled " + this) + prefs.putBoolean(pref, false) + prefs.flush() + apply() + } + + fun toggle() { + log("toggled " + this) + prefs.putBoolean(pref, !isEnabled()) + prefs.flush() + apply() + } + + fun isEnabled(): Boolean { + return prefs.getBoolean(pref, default) + } + + open fun apply() {} + fun filter(img: TextureRegion) { + filter(img.texture) + } + + fun filter(tex: Texture) { + if (BILINEAR.isEnabled()) + tex.setFilter(Linear, Linear) + else tex.setFilter(Nearest, Nearest) + } + } + + /** @suppress */ + enum class NumPref(val pref: String, val text: String = pref, val min: Int = 0, val max: Int = 0, val default: Int = 50, val step: Int = 1) { + SCREEN_SHAKE("screenshake", min = 0, max = 100, default = 30, step = 10), + SHIP_SPEED("shipspeed", min = 100, max = 500, default = 180, step = 10), + SHIP_ACC("shipacc", min = 100, max = 1000, default = 300, step = 10), + BULLET_SPEED("bulletspeed", min = 100, max = 1000, default = 300, step = 10), + BULLET_RATE("bulletrate", min = 1, max = 100, default = 20), + SHIP_HEALTH("shiphealth", min = 1, max = 20, default = 10, step = 1), + SHIP_KNOCKBACK("shipgnockback", min = 0, max = 40, default = 10, step = 1), + FX_VOLUME("fxvolume", min = 0, max = 10, default = 10, step = 1), + MUSIC_VOLUME("musicvolume", min = 0, max = 10, default = 10, step = 1), + + BUFFER("buffer", min = 0, max = 20, default = 6, step = 1); + + fun displayText(): String { + return "${prefs.getInteger(pref, default)}" + } + + fun asVolume(): Float { + val a = getNum().toFloat() / 10f + return a * a * a + } + + fun asPercent(): Float { + return getNum().toFloat() / 100f + } + + fun getNum(): Int { + return prefs.getInteger(pref, default) + } + + fun increase() { + var i = prefs.getInteger(pref, default) + if (i < max) { + i += step + + prefs.putInteger(pref, i) + prefs.flush() + log("pref $name set to $i") + } + apply() + } + + fun decreass() { + var i = prefs.getInteger(pref, default) + if (i > min) { + i -= step + prefs.putInteger(pref, i) + prefs.flush() + log("pref $name set to $i") + } + apply() + } + + open fun apply() {} + } + + /** @suppress */ + enum class StringPref(val pref: String, val text: String = pref, val default: String = "") { + PLAYER1("player1", default = "PLAYER1"), + PLAYER2("player2", default = "PLAYER2"), + PLAYER3("player3", default = "PLAYER3"), + PLAYER4("player4", default = "PLAYER4"), + SERVER("server", default = "1.1.1.1"), + GAME("game", default = "RETDROID"), + PLAYER_GUEST("playerguest", default = "GUEST"); + + fun displayText(): String { + return prefs.getString(pref, default) + } + + fun getString(): String { + return prefs.getString(pref, default) + } + + fun setString(s: String) { + prefs.putString(pref, s) + prefs.flush() + } + + fun appendChar(c: Char) { + setString(getString() + c) + } + + fun deleteChar() { + setString(getString().dropLast(1)) + } + + fun reset() { + setString(default) + } + } +} diff --git a/src/uk/me/fantastic/retro/Resources.kt b/src/uk/me/fantastic/retro/Resources.kt new file mode 100644 index 0000000..8a05fad --- /dev/null +++ b/src/uk/me/fantastic/retro/Resources.kt @@ -0,0 +1,53 @@ +// +/* + Copyright 2018 Richard Smith. + + RetroWar 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. + + RetroWar is distributed in the hope that 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 RetroWar. If not, see . +*/ +// +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.BitmapFont +import com.badlogic.gdx.graphics.g2d.TextureRegion +import com.badlogic.gdx.utils.I18NBundle + +class Resources { + + companion object { + + var baseFileHandle = Gdx.files.internal("i18n/RetroWar") + var defaultLocale = java.util.Locale.getDefault() + + // var locale = Locale.Builder().setLocale(defaultLocale).setVariant("PROFANE").build() + + var TEXT = I18NBundle.createBundle(baseFileHandle) + + val MISSING_TEXTURE = Texture("badlogic.jpg") + val MISSING_TEXTUREREGION = TextureRegion(MISSING_TEXTURE) + + private fun TextureRegion(s: String): TextureRegion = TextureRegion(Texture(s)) + +// val FONT = BitmapFont(Gdx.files.internal("c64_low3_black.fnt")) +// val FONT_CLEAR = BitmapFont(Gdx.files.internal("c64_low3.fnt")) + + val FONT = BitmapFont(Gdx.files.internal(TEXT["fontBlack"])) + val FONT_CLEAR = BitmapFont(Gdx.files.internal(TEXT["font"])) + + val FONT_ENGLISH = BitmapFont(Gdx.files.internal("english.fnt")) + + val BLING = Gdx.audio.newSound(Gdx.files.internal("powerup.wav"))!! + } +} diff --git a/src/uk/me/fantastic/retro/SimpleApp.kt b/src/uk/me/fantastic/retro/SimpleApp.kt new file mode 100644 index 0000000..08f44c7 --- /dev/null +++ b/src/uk/me/fantastic/retro/SimpleApp.kt @@ -0,0 +1,47 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import uk.me.fantastic.retro.screens.SimpleTitleScreen + +class SimpleApp(callback: Callback, val name: String, val factory: AbstractGameFactory, logger: Logger, manualGC: +ManualGC? = null, val advertise: Boolean = false) : App +(callback, logger, manualGC) { + + override fun quit() { + log("App", "Quit") + Gdx.app.exit() + } + + override fun create() { + + log("SimpleApp from $factory create") + setScreenMode() + + initialiseAndroid() + initialiseDesktop() + setPrefsToDefaultsForSingleGames() + initialisePrefs() + initializeInput() + initialiseControllers() + initialiseShader() + + if(advertise){ + title = SimpleTitleScreen(title = name, factory = factory, quitText = "More RetroWar", quitURL = + "https://store.steampowered.com/app/664240/)") + }else { + title = SimpleTitleScreen(title = name, factory = factory) + } + setScreen(title) + } + + fun setPrefsToDefaultsForSingleGames() { + Prefs.BinPref.FULLSCREEN.enable() + Prefs.BinPref.VSYNC.enable() + Prefs.MultiChoicePref.LIMIT_FPS.set(0) +// BinPrefMenuItem("motion ", BinPref.SMOOTH), +// BinPrefMenuItem("pixels ", BinPref.BILINEAR), +// BinPrefMenuItem("scaling ", BinPref.STRETCH), +// BinPrefMenuItem("scanlines ", BinPref.SCANLINES), + Prefs.BinPref.FPS.disable() + } +} diff --git a/src/uk/me/fantastic/retro/SimpleGame.kt b/src/uk/me/fantastic/retro/SimpleGame.kt new file mode 100644 index 0000000..ab7b71f --- /dev/null +++ b/src/uk/me/fantastic/retro/SimpleGame.kt @@ -0,0 +1,78 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.g2d.Batch +import com.badlogic.gdx.graphics.g2d.BitmapFont +import uk.me.fantastic.retro.menu.MenuController +import uk.me.fantastic.retro.screens.GameSession + +/** + * Most games probably want to extend this. It does menus and rendering loop, but it's not a Unigame so you're still + * free to do any sort of game you want really. + */ +abstract class SimpleGame( + session: GameSession, + val width: Float, + val height: Float, + val fontClear: BitmapFont = + Resources.FONT_CLEAR, + val font: BitmapFont = Resources.FONT, + val fadeInEffect: Boolean = true +) : Game(session) { + + override val renderer = FBORenderer(WIDTH = width, HEIGHT = height, fadeInEffect = fadeInEffect) + + private val controller = MenuController(session.standardMenu(), width, height, x = 0f, y = height - + 4) + + init { + font.data.markupEnabled = true + fontClear.data.markupEnabled = true + } + + // render is called by libgdx once every frame (required) + override fun render(deltaTime: Float) { + + doLogic(deltaTime) + + val batch = renderer.beginFBO() + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + batch.begin() + doDrawing(batch) + + batch.end() + + if (session.state == GameSession.GameState.MENU) { + batch.begin() + drawMenu(batch) + batch.end() + } + + renderer.renderFBOtoScreen() + } + + fun simpleHighScoreTable(): String = players.sortedDescending().joinToString("") { + "\n\n${it.name} ${it.score}" + } + + abstract fun doDrawing(batch: Batch) + + abstract fun doLogic(deltaTime: Float) + + private fun drawMenu(batch: Batch) { + controller.doInput() + val mouse = renderer.convertScreenToGameCoords(Gdx.input.x, Gdx.input.y) + controller.doMouseInput(mouse.x, mouse.y) + controller.draw(batch) + } + + override fun resize(width: Int, height: Int) { + log("retrogame resize " + toString()) + renderer.resize(width, height) + } + + override fun postMessage(s: String) { + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/SimpleGameFactory.kt b/src/uk/me/fantastic/retro/SimpleGameFactory.kt new file mode 100644 index 0000000..f394168 --- /dev/null +++ b/src/uk/me/fantastic/retro/SimpleGameFactory.kt @@ -0,0 +1,14 @@ +package uk.me.fantastic.retro + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Texture +import uk.me.fantastic.retro.screens.GameSession + +class SimpleGameFactory(name: String, val gameClazz: Class) : AbstractGameFactory(name = name) { + + override val description = name + override val image: Texture by lazy { Texture(Gdx.files.internal("badlogic.jpg")) } + override fun create(session: GameSession): Game { + return gameClazz.getConstructor(GameSession::class.java).newInstance(session) + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/globals.kt b/src/uk/me/fantastic/retro/globals.kt new file mode 100644 index 0000000..6d399fa --- /dev/null +++ b/src/uk/me/fantastic/retro/globals.kt @@ -0,0 +1,349 @@ +// +/* + Copyright 2018 Richard Smith. + + RetroWar 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. + + RetroWar is distributed in the hope that 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 RetroWar. If not, see . +*/ +// +package uk.me.fantastic.retro + +import com.badlogic.gdx.Application +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.Pixmap +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.g2d.TextureRegion +import com.badlogic.gdx.graphics.glutils.FrameBuffer +import com.badlogic.gdx.graphics.glutils.ShaderProgram +import com.badlogic.gdx.graphics.glutils.ShapeRenderer +import com.badlogic.gdx.maps.tiled.TiledMap +import com.badlogic.gdx.maps.tiled.TiledMapTileLayer +import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer +import com.badlogic.gdx.math.MathUtils +import com.badlogic.gdx.math.Rectangle +import uk.me.fantastic.retro.input.InputDevice +import uk.me.fantastic.retro.input.NetworkInput +import uk.me.fantastic.retro.network.ClientPlayer +import uk.me.fantastic.retro.utils.sqrt +import java.util.ArrayList +import kotlin.math.roundToInt + +/* + * Global scope stuff that IntelliJ doesn't like being in the actual files where it is used + */ + +interface Logger { + fun log(message: String) + fun log(caller: String, message: String) + fun error(message: String) + fun initialize() +} + +val osName = System.getProperty("os.name") +val isOSX: Boolean = osName.contains("OS X") +val isMobile = Gdx.app.type == Application.ApplicationType.Android || Gdx.app.type == Application.ApplicationType.iOS +val isLinux: Boolean = osName.contains("Linux") && !isMobile +val isWindows: Boolean = !isLinux && !isOSX && !isMobile + +interface ManualGC { + fun enable() + fun disable() + fun doGC() +} + + +@Suppress("NOTHING_TO_INLINE") +inline fun log(log: String) { + if (Prefs.BinPref.DEBUG.isEnabled()) { + App.app.logger.log(log) + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun log(c: String, log: String) { + if (Prefs.BinPref.DEBUG.isEnabled()) { + App.app.logger.log(c, log) + } +} + +fun error(message: String) { + App.app.logger.error(message) +} + +fun drawBox( + MARGIN: Int, + SHADOW_OFFSET: Int, + shape: ShapeRenderer, + width: Float, + height: Float, + y: Float, + SCREEN_WIDTH: Float +) { + val box = Rectangle(0f, 0f, width + MARGIN, height + MARGIN) + shape.begin(ShapeRenderer.ShapeType.Filled) + shape.color = Color.BLACK + shape.rect(SCREEN_WIDTH / 2 - box.width / 2 + SHADOW_OFFSET, y - SHADOW_OFFSET - box.height + MARGIN / 2, box.width, box + .height) + shape.color = Color(0, 87, 132) + shape.rect(SCREEN_WIDTH / 2 - box.width / 2, y - box.height + MARGIN / 2, box.width, box.height) + shape.end() + shape.begin(ShapeRenderer.ShapeType.Line) + shape.color = Color.WHITE + shape.rect(SCREEN_WIDTH / 2 - box.width / 2, y - box.height + MARGIN / 2, box.width, box.height) + shape.end() +} + +fun listAllLevels() = Gdx.files.internal("levels").list().filter { it.extension() == "tmx" }.map { it.name().dropLast(4) } + +fun Float.roundDown(): Float { + return MathUtils.floor(this).toFloat() +} + +/** @suppress */ +enum class Size { + SMALL, MEDIUM, LARGE +} + +/** @suppress */ +class JoinRequest + +/** @suppress */ +class WorldUpdate(val buffer: ByteArray?, val id: Int) { + constructor() : this(null, 0) +} + +/** @suppress */ +class PlayersUpdate(val players: ArrayList?) { + constructor() : this(null) +} + +/** @suppress */ +class CreatePlayerRequest(val player: ClientPlayer?) { + constructor() : this(null) +} + +/** @suppress */ +class InputUpdate(input: InputDevice?, val playerId: Int) { + + constructor() : this(null, -1) + + var networkInput: NetworkInput? = null + + init { + if (input != null) { + networkInput = NetworkInput(input.leftStick, input.rightStick, input.leftTrigger, input.rightTrigger, + input.A) + } + } +} + +/** @suppress */ +class CreatePlayerResponse(val serverPlayerId: Int, val clientPlayerId: Int) { + constructor() : this(-1, -1) +} + +fun Pair.normVector(): Pair { + + val vMagnitude = (first * first + second * second).sqrt() + return Pair(first / vMagnitude, second / vMagnitude) +} + +interface Callback { + fun setForegroundFPS(foregroundFPS: Int) + fun setBackgroundFPS(backgroundFPS: Int) +} + +class EmptyCallback : Callback { + + override fun setForegroundFPS(foregroundFPS: Int) { + } + + override fun setBackgroundFPS(backgroundFPS: Int) { + } +} + +typealias Square = ArrayList + +fun Color(r: Int, g: Int, b: Int): Color { + return Color(r.toFloat() / 255f, g.toFloat() / 255f, b.toFloat() / 255f, 1.0f) +} + +/* +FIXME in Java this would be a performance improvement over ArrayList; in Kotlin I don't know how to avoid +autoboxing when I pull the ints out of the array so this might be what causes lots of Integers to be allocated! +It might be better just to use Array. There's also IntArray but that lacks a clear() +*/ +// class MyIntArray : com.badlogic.gdx.utils.IntArray(false, 256), Iterable { +// override fun iterator(): Iterator { +// return object : Iterator { +// var i = 0 +// override fun next(): Int { +// return items[i++] +// } +// +// override fun hasNext(): Boolean { +// return i < size +// } +// +// } +// } +// +// } + +inline fun matrix2d(height: Int, width: Int, init: (Int, Int) -> Array) = Array(height, { row -> init(row, width) }) + +fun Float.round(): Float = roundToInt().toFloat() + +fun createDefaultShader(): ShaderProgram { + val vertexShader = ("in vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + // + "in vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" + // + "in vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" + // + "uniform mat4 u_projTrans;\n" + // + "out vec4 v_color;\n" + // + "out vec2 v_texCoords;\n" + // + "\n" + // + "void main()\n" + // + "{\n" + // + " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" + // + " v_color.a = v_color.a * (255.0/254.0);\n" + // + " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" + // + " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + // + "}\n") + val fragmentShader = ("#ifdef GL_ES\n" + // + "#define LOWP lowp\n" + // + "precision mediump float;\n" + // + "#else\n" + // + "#define LOWP \n" + // + "#endif\n" + // + "in LOWP vec4 v_color;\n" + // + "in vec2 v_texCoords;\n" + // + "out vec4 fragColor;\n" + // + "uniform sampler2D u_texture;\n" + // + "void main()\n" + // + "{\n" + // + " fragColor = v_color * texture(u_texture, v_texCoords);\n" + // + "}") + + ShaderProgram.prependFragmentCode = "#version 330\n" + ShaderProgram.prependVertexCode = "#version 330\n" + val shader = ShaderProgram(vertexShader, fragmentShader) + if (shader.isCompiled == false) throw IllegalArgumentException("Error compiling shader: " + shader.log) + return shader +} + +private fun createVertexShader(hasNormals: Boolean, hasColors: Boolean, numTexCoords: Int): String { + var shader = ("in vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + + (if (hasNormals) "in vec3 " + ShaderProgram.NORMAL_ATTRIBUTE + ";\n" else "") + + if (hasColors) "in vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" else "") + + for (i in 0 until numTexCoords) { + shader += "in vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + i + ";\n" + } + + shader += "uniform mat4 u_projModelView;\n" + shader += if (hasColors) "out vec4 v_col;\n" else "" + + for (i in 0 until numTexCoords) { + shader += "out vec2 v_tex$i;\n" + } + + shader += ("void main() {\n" + " gl_Position = u_projModelView * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + + if (hasColors) " v_col = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" else "") + + for (i in 0 until numTexCoords) { + shader += " v_tex" + i + " = " + ShaderProgram.TEXCOORD_ATTRIBUTE + i + ";\n" + } + shader += " gl_PointSize = 1.0;\n" + shader += "}\n" + return shader +} + +private fun createFragmentShader(hasColors: Boolean, numTexCoords: Int): String { + var shader = "#ifdef GL_ES\n" + "precision mediump float;\n" + "#endif\n" + + if (hasColors) shader += "in vec4 v_col;\n" + for (i in 0 until numTexCoords) { + shader += "in vec2 v_tex$i;\n" + shader += "uniform sampler2D u_sampler$i;\n" + } + shader += "out vec4 fragColor;\n" + + shader += "void main() {\n" + " fragColor = " + if (hasColors) "v_col" else "vec4(1, 1, 1, 1)" + + if (numTexCoords > 0) shader += " * " + + for (i in 0 until numTexCoords) { + if (i == numTexCoords - 1) { + shader += " texture(u_sampler$i, v_tex$i)" + } else { + shader += " texture(u_sampler$i, v_tex$i) *" + } + } + + shader += ";\n}" + return shader +} + +/** Returns a new instance of the default shader used by SpriteBatch for GL2 when no shader is specified. */ +fun createDefaultShapeShader(hasNormals: Boolean = false, hasColors: Boolean = true, numTexCoords: Int = 0): + ShaderProgram { + val vertexShader = createVertexShader(hasNormals, hasColors, numTexCoords) + val fragmentShader = createFragmentShader(hasColors, numTexCoords) + ShaderProgram.prependFragmentCode = "#version 330\n" + ShaderProgram.prependVertexCode = "#version 330\n" + val shader = ShaderProgram(vertexShader, fragmentShader) + if (shader.isCompiled == false) throw IllegalArgumentException("Error compiling shader: " + shader.log) + return shader +} + +fun renderTileMapToTexture(map: TiledMap): TextureRegion { + val tiles = map.layers[0] as TiledMapTileLayer + val width = tiles.width * tiles.tileWidth + val height = tiles.height * tiles.tileHeight + val batch = SpriteBatch(1000, createDefaultShader()) + val mapRenderer = OrthogonalTiledMapRenderer(map, 1f, SpriteBatch(1000, createDefaultShader())) + + val fbo = FrameBuffer(Pixmap.Format.RGB888, width.toInt(), height.toInt(), false) + + val fboCam = OrthographicCamera(width, height) + val fboTexture = TextureRegion(fbo.colorBufferTexture) + + fboTexture.texture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest) + + fboTexture.flip(false, true) // for some reason y-axis is inverted in framebuffer + + fboCam.position.set(width / 2f, height / 2f, 0f) + fboCam.update() + + fbo.begin() + + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + batch.projectionMatrix = fboCam.combined + + batch.begin() + + mapRenderer.setView(fboCam) + mapRenderer.render() + + batch.end() + + fbo.end() + + return fboTexture +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/input/GamepadInput.kt b/src/uk/me/fantastic/retro/input/GamepadInput.kt new file mode 100644 index 0000000..10593c8 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/GamepadInput.kt @@ -0,0 +1,66 @@ +package uk.me.fantastic.retro.input + +import uk.me.fantastic.retro.utils.Vec + +/** + * Created by Richard on 13/08/2016. + * Maps a controller to an input + */ +internal class GamepadInput(val controller: MappedController) : InputDevice() { + + override val leftStick: Vec + get() { + val x = controller.LStickHorizontalAxis() + val y = controller.LStickVerticalAxis() + val (a, b) = filterDeadzone(0.05f, x, y) + return Vec(a, b) + } + + override val rightStick: Vec + get() { + val x = controller.RStickHorizontalAxis() + val y = controller.RStickVerticalAxis() + val (a, b) = filterDeadzone(0.6f, x, y) + return Vec(a, b) + } + + override val leftTrigger: Float + get() { + // log("GamepadInput ${controller.leftTrigger()}") + return controller.leftTrigger() + } + override val rightTrigger: Float + get() { + return controller.rightTrigger() + } + + override val A: Boolean + get() { + return controller.a() + } + + override val B: Boolean + get() { + return controller.b() + } + + override val X: Boolean + get() { + return controller.x() + } + + override val Y: Boolean + get() { + return controller.y() + } + + override val leftBumper: Boolean + get() { + return controller.lBumper() + } + + override val rightBumper: Boolean + get() { + return controller.rBumper() + } +} diff --git a/src/uk/me/fantastic/retro/input/InputDevice.kt b/src/uk/me/fantastic/retro/input/InputDevice.kt new file mode 100644 index 0000000..def135a --- /dev/null +++ b/src/uk/me/fantastic/retro/input/InputDevice.kt @@ -0,0 +1,36 @@ +package uk.me.fantastic.retro.input + +import uk.me.fantastic.retro.utils.Vec + +/** + * All input devices are abstracted to look something like the ubiquitious xbox controller + */ +abstract class InputDevice { + + abstract val leftStick: Vec + abstract val rightStick: Vec + abstract val leftTrigger: Float + abstract val rightTrigger: Float + + abstract val A: Boolean + abstract val B: Boolean + abstract val X: Boolean + abstract val Y: Boolean + + abstract val leftBumper: Boolean + abstract val rightBumper: Boolean + + val fire: Boolean + get() { + return (A || B || X || Y) || rightBumper + } + + var entity: Int = -1 + + internal fun filterDeadzone(deadzone: Float, axisX: Float, axisY: Float): Pair { + if (axisX < deadzone && axisX > -deadzone && axisY < deadzone && axisY > -deadzone) { + return Pair(0f, 0f) + } + return Pair(axisX, axisY) + } +} diff --git a/src/uk/me/fantastic/retro/input/KeyboardMouseInput.kt b/src/uk/me/fantastic/retro/input/KeyboardMouseInput.kt new file mode 100644 index 0000000..94688fc --- /dev/null +++ b/src/uk/me/fantastic/retro/input/KeyboardMouseInput.kt @@ -0,0 +1,115 @@ +package uk.me.fantastic.retro.input + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import uk.me.fantastic.retro.Game +import uk.me.fantastic.retro.screens.GameSession + +import uk.me.fantastic.retro.utils.Vec + +/** + * Maps Keyboard and mouse to input + */ +internal class KeyboardMouseInput(val session: GameSession) : InputDevice() { + + override val leftTrigger: Float + get() = 0f + override val rightTrigger: Float + get() = 0f + + override val leftStick: Vec + get() { + var x = 0f + var y = 0f + if (Gdx.input.isKeyPressed(Input.Keys.A) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_4)) { + x = -1f + } + if (Gdx.input.isKeyPressed(Input.Keys.D) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_6)) { + x = 1f + } + if (Gdx.input.isKeyPressed(Input.Keys.W) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_8)) { + y = -1f + } + if (Gdx.input.isKeyPressed(Input.Keys.S) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_2)) { + y = 1f + } + return Vec(x, y) + } + + override val A: Boolean + get() { + return Gdx.input.isKeyPressed(Input.Keys.SPACE) || Gdx.input.isButtonPressed(0) + } + override val B: Boolean + get() { + return Gdx.input.isKeyPressed(Input.Keys.ENTER) + } + override val X: Boolean + get() { + return Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) + } + override val Y: Boolean + get() { + return Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT) + } + + override val leftBumper: Boolean + get() { + return Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) + } + override val rightBumper: Boolean + get() { + return Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT) + } + // val pointers = Aspect.all(IsPointer::class.java) + + override val rightStick: Vec + get() { + if (pressed(Input.Keys.UP) || pressed(Input.Keys.DOWN) || pressed(Input.Keys.LEFT) || pressed(Input.Keys.RIGHT)) { + return keyboardAsRightStick() + } + + // if (!Gdx.input.isButtonPressed(0)) return Vec(0f, 0f) + // if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) { + // val target = GameMappers.positionMapper.get(pointer) + + // val playerV = (playerVelocity.x*playerVelocity.x+playerVelocity.y*playerVelocity.y).sqrt() + + if (!Gdx.input.isButtonPressed(Input.Buttons.LEFT)) { + return Vec(0f, 0f) + } + + val game = session.game + if (game != null && game is Game.UsesMouseAsInputDevice) { + val m = game.getMouse() + return m + } + return Vec(0f, 0f) + } + + private fun keyboardAsRightStick(): Vec { + var x = 0f + var y = 0f + if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { + x = -1f + } + if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { + x = 1f + } + if (Gdx.input.isKeyPressed(Input.Keys.UP)) { + y = -1f + } + if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { + y = 1f + } + return Vec(x, y) + } + + fun pressed(x: Int): Boolean = Gdx.input.isKeyPressed(x) + + val Pair.x: A + get() = first + + val Pair.y: B + get() = second +} diff --git a/src/uk/me/fantastic/retro/input/MappedController.kt b/src/uk/me/fantastic/retro/input/MappedController.kt new file mode 100644 index 0000000..db96c83 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/MappedController.kt @@ -0,0 +1,397 @@ +package uk.me.fantastic.retro.input + +import com.badlogic.gdx.controllers.Controller +import com.badlogic.gdx.controllers.ControllerAdapter +import com.badlogic.gdx.controllers.PovDirection + +import uk.me.fantastic.retro.isLinux +import uk.me.fantastic.retro.isOSX + +/** + * Wraps a GDX controller, provides mapping for buttons/axises because GDX seems to lack this + */ +internal class MappedController(val controller: Controller) { + var A: Int = 1 + var B: Int = 2 + var X: Int = 0 + var Y: Int = 3 + var GUIDE: Int = 12 + var L_BUMPER: Int = 4 + var R_BUMPER: Int = 5 + var BACK: Int = 8 + var START: Int = 9 + var DPAD_UP: Int = -1 + var DPAD_DOWN: Int = -1 + var DPAD_LEFT: Int = -1 + var DPAD_RIGHT: Int = -1 + var L_STICK_PUSH: Int = 10 + var R_STICK_PUSH: Int = 11 + + var DPAD = 0 + var combinedDpad = true + + // Axes + + var L_TRIGGER: Int = 6 + var R_TRIGGER: Int = 7 + var L_TRIGGER_AXIS: Int = -1 + var R_TRIGGER_AXIS: Int = -1 + + /** left stick vertical axis, -1 if up, 1 if down */ + var L_STICK_VERTICAL_AXIS: Int = 2 + /** left stick horizontal axis, -1 if left, 1 if right */ + var L_STICK_HORIZONTAL_AXIS: Int = 3 + /** right stick vertical axis, -1 if up, 1 if down */ + var R_STICK_VERTICAL_AXIS: Int = 0 + /** right stick horizontal axis, -1 if left, 1 if right */ + var R_STICK_HORIZONTAL_AXIS: Int = 1 + + var mapping = "Unknown" + + var crippledTrigger = false + + fun setToMacDefault() { + mapping = "MAC Default" + A = 1 + B = 2 + X = 0 + Y = 3 + GUIDE = 12 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 8 + START = 9 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 10 + R_STICK_PUSH = 11 + + DPAD = -1 + + L_TRIGGER_AXIS = 4 + R_TRIGGER_AXIS = 5 + + R_TRIGGER = 7 + L_TRIGGER = 6 + + L_STICK_VERTICAL_AXIS = 1 + + L_STICK_HORIZONTAL_AXIS = 0 + + R_STICK_VERTICAL_AXIS = 3 + + R_STICK_HORIZONTAL_AXIS = 2 + } + + fun setToLinuxDefault() { + mapping = "Linux Default" + A = 0 + B = 1 + X = 3 + Y = 2 + GUIDE = 10 // was 12 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 8 + START = 9 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 11 // was 10 + R_STICK_PUSH = 12 // was 11 + DPAD = 0 + L_TRIGGER_AXIS = 2 + R_TRIGGER_AXIS = 5 + R_TRIGGER = 7 + L_TRIGGER = 6 + L_STICK_VERTICAL_AXIS = 1 + L_STICK_HORIZONTAL_AXIS = 0 + R_STICK_VERTICAL_AXIS = 4 + R_STICK_HORIZONTAL_AXIS = 3 + } + + fun setToLinuxXbox() { + mapping = "Linux Xbox" + A = 0 + B = 1 + X = 2 + Y = 3 + GUIDE = 8 // was 12 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 6 + START = 7 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 9 // was 10 + R_STICK_PUSH = 10 // 12 + DPAD = 0 + L_TRIGGER_AXIS = 2 + R_TRIGGER_AXIS = 5 + R_TRIGGER = 11 + L_TRIGGER = 12 + L_STICK_VERTICAL_AXIS = 1 + L_STICK_HORIZONTAL_AXIS = 0 + R_STICK_VERTICAL_AXIS = 4 + R_STICK_HORIZONTAL_AXIS = 3 + } + + fun setToDS4() { + mapping = "DS4" + A = 1 + B = 2 + X = 0 + Y = 3 + GUIDE = 12 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 8 + START = 9 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 10 + R_STICK_PUSH = 11 + + DPAD = 0 + + L_TRIGGER_AXIS = 5 + R_TRIGGER_AXIS = 4 + + R_TRIGGER = 7 + L_TRIGGER = 6 + + L_STICK_VERTICAL_AXIS = 2 + + L_STICK_HORIZONTAL_AXIS = 3 + + R_STICK_VERTICAL_AXIS = 0 + + R_STICK_HORIZONTAL_AXIS = 1 + } + + fun setToX360() { + mapping = "Xbox 360" + A = 0 + B = 1 + X = 2 + Y = 3 + GUIDE = 12 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 6 + START = 7 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 8 // was 10, changed for wii u pad + R_STICK_PUSH = 9 // was 11 + + DPAD = 0 + + L_TRIGGER_AXIS = 4 + R_TRIGGER_AXIS = 5 // doesnt work on 360 pad, both are same axis + crippledTrigger = true + + R_TRIGGER = 7 // doesnt work on 360 pad + L_TRIGGER = 6 // doesnt work on 360 pad + + L_STICK_VERTICAL_AXIS = 0 + + L_STICK_HORIZONTAL_AXIS = 1 + + R_STICK_VERTICAL_AXIS = 2 + + R_STICK_HORIZONTAL_AXIS = 3 + } + + fun setToF310() { + mapping = "Logitech F310" + A = 0 + B = 1 + X = 2 + Y = 3 + GUIDE = -1 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 6 + START = 7 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 8 + R_STICK_PUSH = 9 + + DPAD = 0 + + // TRIGGER_AXIS = 4 + + R_TRIGGER = -1 + L_TRIGGER = -1 + + L_STICK_VERTICAL_AXIS = 0 + + L_STICK_HORIZONTAL_AXIS = 1 + + R_STICK_VERTICAL_AXIS = 2 + + R_STICK_HORIZONTAL_AXIS = 3 + } + + fun setToDirectX() { + mapping = "DirectX controller" + A = 1 + B = 2 + X = 0 + Y = 3 + GUIDE = -1 + L_BUMPER = 4 + R_BUMPER = 5 + BACK = 8 + START = 9 + DPAD_UP = -1 + DPAD_DOWN = -1 + DPAD_LEFT = -1 + DPAD_RIGHT = -1 + L_STICK_PUSH = 10 // f310, was 8 + R_STICK_PUSH = 11 // f310, was9 + + DPAD = 0 + + // TRIGGER_AXIS = 4 + + R_TRIGGER = 7 + L_TRIGGER = 6 + + L_STICK_VERTICAL_AXIS = 2 + + L_STICK_HORIZONTAL_AXIS = 3 + + R_STICK_VERTICAL_AXIS = 0 + + R_STICK_HORIZONTAL_AXIS = 1 + } + + init { + if (isOSX) { + setToMacDefault() + } else if (isLinux) { + if (controller.name.contains("Sony")) { + setToLinuxDefault() + } else if (controller.name.contains("X-Box")) { + setToLinuxXbox() + } else { + setToLinuxDefault() + } + } else { // isWindows + if (controller.name.contains("F310")) { + setToF310() + } else if (controller.name.contains("Logitech Dual Action")) { + setToDirectX() + } else if (controller.name.contains("Wireless Controller")) { + setToDS4() + } else { + setToX360() + } + } + } + + fun b(): Boolean { + return controller.getButton(B) + } + + fun a(): Boolean { + return controller.getButton(A) + } + + fun x(): Boolean { + return controller.getButton(X) + } + + fun y(): Boolean { + return controller.getButton(Y) + } + + fun lBumper(): Boolean { + return controller.getButton(L_BUMPER) + } + + fun rBumper(): Boolean { + return controller.getButton(R_BUMPER) + } + + fun lTrigger(): Boolean { + return controller.getButton(L_TRIGGER) + } + + fun rTrigger(): Boolean { + return controller.getButton(R_TRIGGER) + } + + fun LStickHorizontalAxis(): Float { + if (combinedDpad && DPAD != -1) { + val d = controller.getPov(DPAD) + if (d == PovDirection.east || d == PovDirection.northEast || d == PovDirection.southEast) { + return 1f + } else if (d == PovDirection.west || d == PovDirection.northWest || d == PovDirection.southWest) { + return -1f + } else if (d == PovDirection.north || d == PovDirection.south) { + return 0f + } + } + return controller.getAxis(L_STICK_HORIZONTAL_AXIS) + } + + fun LStickVerticalAxis(): Float { + if (combinedDpad && DPAD != -1) { + val d = controller.getPov(DPAD) + if (d == PovDirection.north || d == PovDirection.northEast || d == PovDirection.northWest) { + return -1f + } else if (d == PovDirection.south || d == PovDirection.southEast || d == PovDirection.southWest) { + return 1f + } else if (d == PovDirection.east || d == PovDirection.west) { + return 0f + } + } + return controller.getAxis(L_STICK_VERTICAL_AXIS) + } + + fun RStickHorizontalAxis(): Float { + return controller.getAxis(R_STICK_HORIZONTAL_AXIS) + } + + fun RStickVerticalAxis(): Float { + return controller.getAxis(R_STICK_VERTICAL_AXIS) + } + + fun leftTrigger(): Float { + // log("mappedcontroller","$L_TRIGGER_AXIS $L_TRIGGER ${controller.getAxis(L_TRIGGER_AXIS)}") + if (crippledTrigger) { + return (controller.getAxis(L_TRIGGER_AXIS) * 2) - 1 + } else { + return controller.getAxis(L_TRIGGER_AXIS) + } + } + + fun rightTrigger(): Float { + if (crippledTrigger) { + return (controller.getAxis(L_TRIGGER_AXIS) * -2) - 1 + } else { + return controller.getAxis((R_TRIGGER_AXIS)) + } + } + + fun start(): Boolean { + return controller.getButton(START) + } + + var listener: ControllerAdapter? = null +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/input/NetworkInput.kt b/src/uk/me/fantastic/retro/input/NetworkInput.kt new file mode 100644 index 0000000..e281098 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/NetworkInput.kt @@ -0,0 +1,40 @@ +package uk.me.fantastic.retro.input + +import uk.me.fantastic.retro.utils.Vec + +/** + * stores state of a device (FIXME REMOVE CLIENTID?) + * to be sent over network + */ +open class NetworkInput( + override var leftStick: Vec = Vec(0f, 0f), + override var rightStick: Vec = Vec(0f, 0f), + override var leftTrigger: Float = 0f, + override var rightTrigger: Float = 0f, + override var A: Boolean = false, + val clientId: Int = -1 +) : + InputDevice() { + override val B: Boolean + get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates. + override val X: Boolean + get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates. + override val Y: Boolean + get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates. + override val leftBumper: Boolean + get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates. + override val rightBumper: Boolean + get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates. + + fun copyTo(other: NetworkInput) { + other.leftStick = leftStick + other.rightStick = rightStick + other.A = A + } + + fun copyFrom(other: InputDevice) { + leftStick = other.leftStick + rightStick = other.rightStick + A = other.A + } +} diff --git a/src/uk/me/fantastic/retro/input/RobotInput.kt b/src/uk/me/fantastic/retro/input/RobotInput.kt new file mode 100644 index 0000000..689e0b8 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/RobotInput.kt @@ -0,0 +1,66 @@ +package uk.me.fantastic.retro.input + +/** + * AI controlled, not very smart, more like drunk input! + */ +// class RobotInput(val game: GameMappers) : InputDevice() { +// override val B: Boolean +// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. +// override val X: Boolean +// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. +// override val Y: Boolean +// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. +// override val leftBumper: Boolean +// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. +// override val rightBumper: Boolean +// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. +// override val leftTrigger: Float +// get() = 0f +// override val rightTrigger: Float +// get() = 0f +// +// +// // FIXME array is not the best for a circular list I know +// val waypoints = arrayOf(Pair(350f, 200f), Pair(100f, 200f), Pair(100f, 100f), Pair(350f, 100f)) +// +// var waypoint = 0 +// +// override val leftStick: Vec +// get() { +// +// val destination = waypoints[waypoint] +// val (x, y) = destination +// +// val position = game.positionMapper[entity] +// +// if (position!!.isCloseTo(destination)) { +// waypoint++ +// if (waypoint >= waypoints.size) { +// waypoint = 0 +// } +// } +// +// val dx = x - position.x +// val dy = y - position.y +// +// val nx = clamp(dx, -1f, 1f) +// val ny = clamp(dy, -1f, 1f) +// +// return Vec(nx, -ny) +// } +// +// override val rightStick: Vec +// get() { +// +// return Vec() +// } +// +// override val A: Boolean +// get() { +// return false +// } +// +// fun clamp(x: Float, min: Float, max: Float): Float { +// return Math.max(min, Math.min(max, x)) +// } +// } diff --git a/src/uk/me/fantastic/retro/input/SimpleTouchscreenInput.kt b/src/uk/me/fantastic/retro/input/SimpleTouchscreenInput.kt new file mode 100644 index 0000000..06710e6 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/SimpleTouchscreenInput.kt @@ -0,0 +1,84 @@ +package uk.me.fantastic.retro.input + +import com.badlogic.gdx.Gdx +import uk.me.fantastic.retro.utils.Vec + +/** + * has one joystick on the left and one button on the right of the screen + */ +internal class SimpleTouchscreenInput : InputDevice() { + + var joyStickOrigin = Vec(0f, 0f) + var joyStickPosition = Vec(0f, 0f) + var joyStickFinger = -1 + + override val leftTrigger: Float + get() = 0f + override val rightTrigger: Float + get() = 0f + + override val A: Boolean + get() { +// if (TouchscreenJoystick.touchReleased) { +// TouchscreenJoystick.touchReleased = false +// return true +// } + for (i in 0..10) { + if (Gdx.input.isTouched(i)) { + val x = Gdx.input.getX(i).toFloat() + // val y = Gdx.input.getY(i).toFloat() + if (x > Gdx.graphics.displayMode.width / 2) { + return true + } + } + } + return false + } + + override val leftStick: Vec + get() { + for (i in 0..10) { + if (Gdx.input.isTouched(i)) { + val x = Gdx.input.getX(i).toFloat() + val y = Gdx.input.getY(i).toFloat() + if (x < Gdx.graphics.displayMode.width / 2) { + if (joyStickFinger == -1) { + joyStickOrigin = Vec(x, y) + } + joyStickFinger = i + joyStickPosition = Vec(x, y) + val (a, b) = filterDeadzone(0f, (joyStickPosition.x - joyStickOrigin.x), (joyStickPosition.y - joyStickOrigin.y)) + return Vec(a, b).normVector() + } + } + } + joyStickFinger = -1 + return Vec(0f, 0f) + } + + override val rightStick: Vec + get() { + + return Vec(0f, 0f) + } + override val B: Boolean + get() { + return false + } + override val X: Boolean + get() { + return false + } + override val Y: Boolean + get() { + return false + } + override val leftBumper: Boolean + get() { + return false + } + override val rightBumper: Boolean + get() { + return false + } +} diff --git a/src/uk/me/fantastic/retro/input/StatefulController.kt b/src/uk/me/fantastic/retro/input/StatefulController.kt new file mode 100644 index 0000000..5c8c493 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/StatefulController.kt @@ -0,0 +1,186 @@ +package uk.me.fantastic.retro.input + +import com.badlogic.gdx.controllers.Controller +import com.badlogic.gdx.controllers.ControllerAdapter +import com.badlogic.gdx.controllers.PovDirection + +/** + * Gets state of wrapped controller using events, then provides this state through an + * api that can be polled + * Useful for menus (and simple games where you don't want to miss an input ?) + */ +internal class StatefulController(val c: MappedController) : ControllerAdapter() { + + var startPressed = false + var upPressed = false + var downPressed = false + var leftPressed = false + var rightPressed = false + var APressed = false + var BPressed = false + + var horCentered = true + var vertCentered = true + + val THRESHOLD = 0.3 + val THRESHOLD_H = 0.6 + + init { + c.controller.addListener(this) + } + + fun clearEvents() { + startPressed = false + upPressed = false + downPressed = false + leftPressed = false + rightPressed = false + APressed = false + BPressed = false + horCentered = true + vertCentered = true + } + + val isUpButtonJustPressed: Boolean + get() { + val t = upPressed + upPressed = false + return t + } + + val isDownButtonJustPressed: Boolean + get() { + val t = downPressed + downPressed = false + return t + } + + val isLeftButtonJustPressed: Boolean + get() { + val t = leftPressed + leftPressed = false + return t + } + + val isRightButtonJustPressed: Boolean + get() { + val t = rightPressed + rightPressed = false + return t + } + + val isButtonAJustPressed: Boolean + get() { + // log("stateful $this pressed button") + val t = APressed + APressed = false + return t + } + val isButtonBJustPressed: Boolean + get() { + val t = BPressed + BPressed = false + return t + } + + val isStartButtonJustPressed: Boolean + get() { + val t = startPressed + startPressed = false + return t + } + + override fun buttonDown(controller: Controller?, buttonIndex: Int): Boolean { + + when (buttonIndex) { + c.START -> startPressed = true + c.A -> APressed = true + c.B -> BPressed = true + c.DPAD_UP -> upPressed = true + c.DPAD_DOWN -> downPressed = true + c.DPAD_LEFT -> leftPressed = true + c.DPAD_RIGHT -> rightPressed = true + } + + return true + } + + override fun povMoved(controller: Controller?, povIndex: Int, value: PovDirection?): Boolean { + + when (value) { + PovDirection.north -> upPressed = true + PovDirection.south -> downPressed = true + PovDirection.west -> leftPressed = true + PovDirection.east -> rightPressed = true + PovDirection.center -> { + } + PovDirection.northEast -> { + } + PovDirection.northWest -> { + } + PovDirection.southEast -> { + } + PovDirection.southWest -> { + } + } + + return true + } + + override fun axisMoved(controller: Controller?, axisIndex: Int, value: Float): Boolean { + if (axisIndex == c.L_STICK_HORIZONTAL_AXIS) { + if (value > THRESHOLD_H && horCentered) { + rightPressed = true + horCentered = false + } else if (value < -THRESHOLD_H && horCentered) { + leftPressed = true + horCentered = false + } else if (value < 0.1 && value > -0.1) { + horCentered = true + } + } else if (axisIndex == c.L_STICK_VERTICAL_AXIS) { + if (value > THRESHOLD && vertCentered) { + downPressed = true + vertCentered = false + } else if (value < -THRESHOLD && vertCentered) { + upPressed = true + vertCentered = false + } else if (value < 0.1 && value > -0.1) { + vertCentered = true + } + } + + return true + } +} + +/* + * move this to MAppedController. possibly make a named controlleradapter class, or use mappedcontroller to implement + it, whatever. DONE + * make isJustPressed behave like the keyboard one, only store the data for one frame TODO NOT SURE WONT MISS STUFF + so anyone who wants to use these events will have to call a poll method first anyway + so we could do it all by polling, the only reason for events is that polling the mouse was found to miss clicks + no??? we can assume each bool is read once per frame, then no need for poll method??? but then we can't know + the state on the previous frame to know if its a press or a hold + also if it doesnt get read by a screen but does get set then event stayed until another screen reads it + + for controllers, using the gdx event api still doesnt get us press/depress events on sticks + the old way of having a delay in the menu after an input received treats keyboard and controller the same + when actually we may want key-repeat behaviour to be different. could scale repeats with how far stick is + pushed for instance. + + * can we do poll in app render so its always done? i think so. TODO NOT SURE + + polling vs events + if you want controller stuff to be entirely + poll based we could do it here too, but since you seem to be able to stack as many listeners as you want + on a controller i dont see a downside of using them. (with key/mouse listener you only get one, unless you + deliberately using a multiplex one, but then if you forgot on any other screen and overwrite it you lose your listeners) + listeners are kind of like 'comefrom' though. if you forget you created them you have code executing that you cant discover + just from reading the main loop. if they are created when app starts and they dont affect code in other files that + seems ok. but game session adds and removes listeners during app lifetime. + + * maybe we could get rid of listeners in gamesession though and just query an isAnyButtonJustPressed for each controller? + + * remember also to test splashscreens DONE + */ diff --git a/src/uk/me/fantastic/retro/input/TouchscreenInput.kt b/src/uk/me/fantastic/retro/input/TouchscreenInput.kt new file mode 100644 index 0000000..6bab954 --- /dev/null +++ b/src/uk/me/fantastic/retro/input/TouchscreenInput.kt @@ -0,0 +1,59 @@ +package uk.me.fantastic.retro.input + +/** + * Created by Richard on 13/08/2016. + * Maps a the touchscreen controller system controller to an inputdevice so it can be read + */ +// class TouchscreenInput : InputDevice() { +// +// override val leftTrigger: Float +// get() = 0f +// override val rightTrigger: Float +// get() = 0f +// +// override val A: Boolean +// get() { +// // if (TouchscreenJoystick.touchReleased) { +// // TouchscreenJoystick.touchReleased = false +// // return true +// // } +// return (Gdx.input.isTouched(1)) +// // return false +// } +// +// override val leftStick: Vec +// get() { +// val x = TouchscreenJoystick.LStickHorizontalAxis +// val y = TouchscreenJoystick.LStickVerticalAxis +// val (a, b) = filterDeadzone(0.15f, x, y) +// return Vec(a, b) +// } +// +// override val rightStick: Vec +// get() { +// val x = TouchscreenJoystick.RStickHorizontalAxis +// val y = TouchscreenJoystick.RStickVerticalAxis +// val (a, b) = filterDeadzone(0.6f, x, y) +// return Vec(a, b) +// } +// override val B: Boolean +// get() { +// return false +// } +// override val X: Boolean +// get() { +// return false +// } +// override val Y: Boolean +// get() { +// return false +// } +// override val leftBumper: Boolean +// get() { +// return false +// } +// override val rightBumper: Boolean +// get() { +// return false +// } +// } diff --git a/src/uk/me/fantastic/retro/menu/Menu.kt b/src/uk/me/fantastic/retro/menu/Menu.kt new file mode 100644 index 0000000..500da72 --- /dev/null +++ b/src/uk/me/fantastic/retro/menu/Menu.kt @@ -0,0 +1,74 @@ +package uk.me.fantastic.retro.menu + +import uk.me.fantastic.retro.Resources +import uk.me.fantastic.retro.log +import java.util.ArrayList + +/** + * Created by richard on 13/07/2016. + * Essentially an ArrayList of MenuItems that tracks which of them is selected + */ + +class Menu( + val title: String, + val bottomText: () -> String = { "" }, + val quitAction: () -> Unit = {}, + + val doubleSpaced: Boolean = Resources.FONT.data.down > -10 +) : ArrayList() { + + var selectedItem = 0 + + var editing: MenuItem? = null + + fun getSelected(): MenuItem { + return this[selectedItem] + } + + fun getText(highlight: String?): String { + var text = "$title\n\n" + + this.forEachIndexed { i, menuItem -> + if (doubleSpaced) text += "\n" + if (i == selectedItem && highlight != null) text = "$text[$highlight]" + if (!menuItem.isHidden) { + text += menuItem.text + text += menuItem.displayText() + } + + if (menuItem == editing) text += "*" + text += "\n" + // if (doubleSpaced) text += "\n" + if (i == selectedItem && highlight != null) text += "[]" + } + + return "$text${bottomText.invoke()}" + } + + fun up() { + selectedItem-- + + while (selectedItem > 0 && get(selectedItem).isHidden) { + selectedItem-- + } + + if (selectedItem < 0) + selectedItem = size - 1 + + Resources.BLING.play() + } + + fun down() { + selectedItem++ + + while (selectedItem < size - 1 && get(selectedItem).isHidden) { + selectedItem++ + } + + if (selectedItem > size - 1) + selectedItem = 0 + + Resources.BLING.play() + log("selecteditem $selectedItem") + } +} diff --git a/src/uk/me/fantastic/retro/menu/MenuController.kt b/src/uk/me/fantastic/retro/menu/MenuController.kt new file mode 100644 index 0000000..8633985 --- /dev/null +++ b/src/uk/me/fantastic/retro/menu/MenuController.kt @@ -0,0 +1,257 @@ +package uk.me.fantastic.retro.menu + +import com.badlogic.gdx.Gdx.input +import com.badlogic.gdx.Input +import com.badlogic.gdx.Input.Keys.BACK +import com.badlogic.gdx.Input.Keys.DOWN +import com.badlogic.gdx.Input.Keys.ENTER +import com.badlogic.gdx.Input.Keys.ESCAPE +import com.badlogic.gdx.Input.Keys.LEFT +import com.badlogic.gdx.Input.Keys.RIGHT +import com.badlogic.gdx.Input.Keys.SPACE +import com.badlogic.gdx.Input.Keys.UP +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.g2d.Batch +import com.badlogic.gdx.graphics.g2d.BitmapFont +import com.badlogic.gdx.graphics.g2d.GlyphLayout +import com.badlogic.gdx.graphics.glutils.ShapeRenderer +import com.badlogic.gdx.utils.Align +import uk.me.fantastic.retro.App.Companion.app +import uk.me.fantastic.retro.Resources +import uk.me.fantastic.retro.createDefaultShapeShader +import uk.me.fantastic.retro.drawBox +import uk.me.fantastic.retro.log +import java.util.Stack + +/** + * Wraps a Menu and changes menu selections based on input + */ + +class MenuController( + val rootMenu: Menu, + val WIDTH: Float, + val HEIGHT: Float, + val font: BitmapFont = Resources.FONT_CLEAR, + val x: Float = 0f, + val y: Float +) { + + val menus = Stack

() + + internal var glyphLayout = GlyphLayout() + + var count = 0 + + // val sequence = arrayOf("RED", "PURPLE", "BLUE", "CYAN", "GREEN", "YELLOW") + val sequence = arrayOf("RED", "RED", "PURPLE", "BLUE", "BLUE", "CYAN", "GREEN", "GREEN", "YELLOW") + + var flash = "" + + internal var shape = ShapeRenderer(5000, createDefaultShapeShader()) + + init { + font.data.markupEnabled = true + menus.push(rootMenu) + } + + fun draw(batch: Batch) { + + batch.end() + + // updateaGlyph("RED") + + shape.projectionMatrix = batch.projectionMatrix + + val MARGIN = 6 + val SHADOW_OFFSET = 5 + + drawBox(MARGIN, SHADOW_OFFSET, shape, glyphLayout.width, glyphLayout.height, y, WIDTH) + + batch.begin() + // fontClear.draw(batch, glyphLayout, 0f, y) + drawFlashing(batch) + // batch.end() + } + + fun drawFlashing(batch: Batch) { +// batch.begin() + flash = sequence[count++ % sequence.size] + updateaGlyph(flash) + font.draw(batch, glyphLayout, x, y) + // batch.end() + } + + fun updateaGlyph(color: String) { + glyphLayout.setText(font, menus.peek().getText(color), Color.WHITE, WIDTH, Align.center, true) + } + + fun select() { + log("select") + val s = menus.peek().getSelected() + when (s) { + is StringPrefMenuItem -> { + s.stringPref.setString("") + menus.peek().editing = s + } + is SubMenuItem -> menus.push(s.subMenu) + is BackMenuItem -> doBack() + else -> s.doAction() // fixme theres logic in some of these doActions that perhaps should be here instead + } + } + + fun pushRight() { + log("pushRight") + val s = menus.peek().getSelected() + when (s) { + + is MultiPrefMenuItem, is NumPrefMenuItem, is BinPrefMenuItem, is MultiChoiceMenuItem -> { + s.doAction() + Resources.BLING.play() + } + else -> { + log("pushRight nothing") + } + } + } + + fun pushLeft() { + log("pushLeft") + val s = menus.peek().getSelected() + s.doAction2() + } + + fun doInput() { + + val menu = menus.peek() + + if (menu.editing is StringPrefMenuItem) { + doTextInput(menu.editing as StringPrefMenuItem) + return + } + + when { + inputUp() -> { + menu.up() + } + inputDown() -> { + menu.down() + } + inputSelect() -> { + select() + } + inputLeft() -> { + pushLeft() + } + inputRight() -> { + pushRight() + } + inputBack() -> { + log("menucontroller back") + + doBack() + } + } + } + + private fun doBack() { + menus.pop() + + if (menus.isEmpty()) { + menus.push(rootMenu) + rootMenu.quitAction() + } + } + + val chars = " 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + private fun doTextInput(item: StringPrefMenuItem) { + + val editing = item.stringPref + for (i in Input.Keys.NUM_0..Input.Keys.Z) { + if (input.isKeyJustPressed(i)) { + editing.appendChar(chars[i]) + } + } + if (input.isKeyJustPressed(Input.Keys.PERIOD)) { + editing.appendChar('.') + } + if (input.isKeyJustPressed(Input.Keys.BACKSPACE)) { + editing.deleteChar() + } + if (input.isKeyJustPressed(Input.Keys.ENTER)) { + menus.peek().editing = null + } + } + + fun doMouseInput(x: Float, y: Float) { + if (mouseMoved(x, y)) { + mouseSelect(menus.peek(), x, y) + } + } + + val lineHeightPixels: Float = -Resources.FONT.data.down + + @Suppress("UNUSED_PARAMETER") + private fun mouseSelect(menu: Menu, mx: Float, my: Float) { + // log("mouseselect $y") + var counter = 0f + for (i in menu.indices) { + + val d = if (menu.doubleSpaced) 2 else 2 + val top = y - d * lineHeightPixels - counter - lineHeightPixels / 2 + // val bottom = top - lineHeightPixels - lineHeightPixels / 2 + counter += (if (menu.doubleSpaced) 2 else 1) * lineHeightPixels + + // log(" item $i top $top bottom $bottom") + if (my < top && !menu.get(i).isHidden) { + menu.selectedItem = i + } + } + } + + var oldY = 0f + var oldX = 0f + + private fun mouseMoved(x: Float, y: Float): Boolean { + val ty = oldY + val tx = oldX + oldY = y + oldX = x + if (tx == 0f && ty == 0f) { + return false + } + if (tx == x && ty == y) { + return false + } + return true + } + + private fun inputBack() = + input.isKeyJustPressed(ESCAPE) || + input.isKeyJustPressed(BACK) || + app.statefulControllers.any { it.isStartButtonJustPressed } || + app.statefulControllers.any { it.isButtonBJustPressed } + + private fun inputSelect() = + // input.isKeyJustPressed(RIGHT) || + input.isKeyJustPressed(SPACE) || + input.isKeyJustPressed(ENTER) || + app.mouseJustClicked || + app.statefulControllers.any { it.isButtonAJustPressed } + + private fun inputRight() = + input.isKeyJustPressed(RIGHT) || + app.statefulControllers.any { it.isRightButtonJustPressed } + + private fun inputLeft() = + input.isKeyJustPressed(LEFT) || + app.statefulControllers.any { it.isLeftButtonJustPressed } + + private fun inputDown() = + input.isKeyJustPressed(DOWN) || + app.statefulControllers.any { it.isDownButtonJustPressed } + + private fun inputUp() = + input.isKeyJustPressed(UP) || + app.statefulControllers.any { it.isUpButtonJustPressed } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/menu/MenuItem.kt b/src/uk/me/fantastic/retro/menu/MenuItem.kt new file mode 100644 index 0000000..dabb906 --- /dev/null +++ b/src/uk/me/fantastic/retro/menu/MenuItem.kt @@ -0,0 +1,146 @@ +package uk.me.fantastic.retro.menu + +import uk.me.fantastic.retro.Prefs + +/** + * Menus are lists of MenuItems. There are several distinct types.... + */ + +abstract class MenuItem(open val text: String) { + abstract fun displayText(): String + abstract fun doAction() + open fun doAction2() { + // return doAction() + } + + var isHidden: Boolean = false +} + +class GraphicalMenuItem + +class BackMenuItem(text: String = "<<<<") : MenuItem(text) { + override fun displayText() = "" + override fun doAction() { + } +} + +class MultiPrefMenuItem(text: String, val mPref: Prefs.MultiChoicePref, val action: () -> Unit = {}) : MenuItem(text) { + override fun doAction() { + mPref.next() + action.invoke() + } + + override fun doAction2() { + mPref.prev() + action.invoke() + } + + override fun displayText(): String { + return mPref.displayText() + } +} + +class ColorPrefMenuItem(text: String, val mPref: Prefs.MultiChoicePref, val action: () -> Unit = {}) : MenuItem(text) { + override fun doAction() { + mPref.next() + action.invoke() + } + + override fun doAction2() { + mPref.prev() + action.invoke() + } + + override fun displayText(): String { + return "" + } + + override val text: String = text + get() = "* [#${mPref.displayText()}]$field[]" +} + +// fixme this really shouldnt be mutable. there should just be some mutable wrapper around it to swap them when needed. +class MultiChoiceMenuItem( + text: String, + val onUpdate: (String, Int) -> Unit = { _, _ -> run {} }, + var choices: List, + val + intValues: List +) : MenuItem(text) { + var selected = 0 + + override fun doAction() { + selected++ + + if (selected > choices.lastIndex) { + selected = 0 + } + onUpdate(choices[selected], intValues[selected]) + } + + override fun doAction2() { + selected-- + + if (selected < 0) { + selected = choices.lastIndex + } + onUpdate(choices[selected], intValues[selected]) + } + + override fun displayText(): String { + return if (isHidden) "hidden!" else choices[selected] + } + + fun getSelectedInt(): Int { + val i = intValues.get(selected) + return i + } +} + +class NumPrefMenuItem(text: String, val numPref: Prefs.NumPref) : MenuItem(text) { + override fun displayText() = numPref.displayText() + override fun doAction() { + numPref.increase() + numPref.apply() + } + + override fun doAction2() { + numPref.decreass() + } +} + +class StringPrefMenuItem(text: String, val stringPref: Prefs.StringPref) : MenuItem(text) { + override fun displayText() = stringPref.displayText() + override fun doAction() { + } +} + +class BinPrefMenuItem(text: String, val binPref: Prefs.BinPref) : MenuItem(text) { + override fun displayText() = binPref.displayText() + override fun doAction() { + binPref.toggle() + } +} + +class ActionMenuItem(text: String, val action: () -> Unit) : MenuItem(text) { + override fun displayText() = "" + override fun doAction() { + action.invoke() + } +} + +class SubMenuItem(text: String, val subMenu: Menu) : MenuItem(text) { + override fun displayText() = "" + override fun doAction() { + } +} + +class EmptyMenuItem : MenuItem("") { + init { + isHidden = true + } + + override fun displayText() = "" + override fun doAction() { + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/menu/MenuRenderer.kt b/src/uk/me/fantastic/retro/menu/MenuRenderer.kt new file mode 100644 index 0000000..c8efdb8 --- /dev/null +++ b/src/uk/me/fantastic/retro/menu/MenuRenderer.kt @@ -0,0 +1,26 @@ +package uk.me.fantastic.retro.menu + +import com.badlogic.gdx.graphics.g2d.GlyphLayout + +/** + * Wraps a MenuController and draws the contained menu. + * Depreciated! + */ +class MenuRenderer(val menu: Menu) { + + internal var glyphLayout = GlyphLayout() + + var count = 0 + + val sequence = arrayOf("RED", "PURPLE", "BLUE", "CYAN", "GREEN", "YELLOW") + + var flash = "" + +// fun draw(batch: SpriteBatch) { +// batch.begin() +// flash = sequence[count++ % sequence.size] +// glyphLayout.setText(Resources.FONT, menu.getText("RED"), Color.WHITE, Renderer.WIDTH, Align.center, true) +// Resources.FONT.draw(batch, glyphLayout, 0f, Renderer.HEIGHT) +// batch.end() +// } +} diff --git a/src/uk/me/fantastic/retro/music/gme/BlipBuffer.java b/src/uk/me/fantastic/retro/music/gme/BlipBuffer.java new file mode 100755 index 0000000..18d663b --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/BlipBuffer.java @@ -0,0 +1,350 @@ +package uk.me.fantastic.retro.music.gme;// Band-limited sound synthesis buffer +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +final class BlipBuffer { + static final boolean muchFaster = false; // speeds synthesis at a cost of quality + + BlipBuffer() { + setVolume(1.0); + } + + // Sets sample rate of output and changes buffer length to msec + public void setSampleRate(int rate, int msec) { + sampleRate = rate; + buf = new int[(int) ((long) msec * rate / 1000) + 1024]; + } + + // Sets input clock rate. Must be set after sample rate. + public void setClockRate(int rate) { + clockRate_ = rate; + factor = (int) (sampleRate / (float) clockRate_ * (1 << timeBits) + 0.5); + } + + // Current clock rate + public int clockRate() { + return clockRate_; + } + + // Removes all samples from buffer + public void clear() { + offset = 0; + accum = 0; + java.util.Arrays.fill(buf, 0, buf.length, 0); + } + + // Sets overall volume, where 1.0 is normal + public void setVolume(double v) { + final int shift = 15; + final int round = 1 << (shift - 1); + + volume = (int) ((1 << shift) * v + 0.5) & ~1; + + if (!muchFaster) { + // build new set of kernels + int[][] nk = new int[phaseCount + 1][]; + for (int i = nk.length; --i >= 0; ) + nk[i] = new int[halfWidth]; + + + // must be even since center kernel uses same half twice + final int mul = volume; + + final int pc = phaseCount; + for (int p = 17; --p >= 0; ) { + int remain = mul; + for (int i = 8; --i >= 0; ) { + remain -= (nk[p][i] = (baseKernel[p * halfWidth + i] * mul + round) >> shift); + remain -= (nk[pc - p][i] = (baseKernel[(pc - p) * halfWidth + i] * mul + round) >> shift); + } + nk[p][7] += remain; // each pair of kernel halves must total mul + } + + // replace kernel atomically + kernel = nk; + } + } + + // Adds delta at given time + public void addDelta(int time, int delta) { + final int[] buf = this.buf; + final int phase = (time = time * factor + offset) >> + (timeBits - phaseBits) & (phaseCount - 1); + + if (muchFaster) { + final int right = ((delta *= volume) >> phaseBits) * phase; + buf[time >>= timeBits] += delta - right; + buf[time + 1] += right; + } else { + // TODO: use smaller kernel + + // left half + int[] k = kernel[phase]; + buf[time >>= timeBits] += k[0] * delta; + buf[time + 1] += k[1] * delta; + buf[time + 2] += k[2] * delta; + buf[time + 3] += k[3] * delta; + buf[time + 4] += k[4] * delta; + buf[time + 5] += k[5] * delta; + buf[time + 6] += k[6] * delta; + buf[time + 7] += k[7] * delta; + + // right half (mirrored version of a left half) + k = kernel[phaseCount - phase]; + time += 8; + buf[time] += k[7] * delta; + buf[time + 1] += k[6] * delta; + buf[time + 2] += k[5] * delta; + buf[time + 3] += k[4] * delta; + buf[time + 4] += k[3] * delta; + buf[time + 5] += k[2] * delta; + buf[time + 6] += k[1] * delta; + buf[time + 7] += k[0] * delta; + } + } + + // Number of samples that would be available at time + public int countSamples(int time) { + int last_sample = (time * factor + offset) >> timeBits; + int first_sample = offset >> timeBits; + return last_sample - first_sample; + } + + // Ends current time frame and makes samples available for reading + public void endFrame(int time) { + offset += time * factor; + assert samplesAvail() < buf.length; + } + + // Number of samples available to be read + public int samplesAvail() { + return offset >> timeBits; + } + + // Reads at most count samples into out at offset pos*2 (2 bytes per sample) + // and returns number of samples actually read. + public int readSamples(byte[] out, int pos, int count) { + final int avail = samplesAvail(); + if (count > avail) + count = avail; + + if (count > 0) { + // Integrate + final int[] buf = this.buf; + int accum = this.accum; + pos <<= 1; + int i = 0; + do { + int s = (accum += buf[i] - (accum >> 9)) >> 15; + + // clamp to 16 bits + if ((short) s != s) + s = (s >> 24) ^ 0x7FFF; + + // write as little-endian + out[pos] = (byte) (s >> 8); + out[pos + 1] = (byte) s; + pos += 2; + } + while (++i < count); + this.accum = accum; + + removeSamples(count); + } + return count; + } + +// internal + + static final int timeBits = 16; + static final int phaseBits = (muchFaster ? 8 : 5); + static final int phaseCount = 1 << phaseBits; + static final int halfWidth = 8; + static final int stepWidth = halfWidth * 2; + + int factor; + int offset; + int[][] kernel; + int accum; + int[] buf; + int sampleRate; + int clockRate_; + int volume; + + void removeSilence(int count) { + offset -= count << timeBits; + assert samplesAvail() >= 0; + } + + void removeSamples(int count) { + int remain = samplesAvail() - count + stepWidth; + System.arraycopy(buf, count, buf, 0, remain); + java.util.Arrays.fill(buf, remain, remain + count, 0); + removeSilence(count); + } + + // TODO: compute at run-time + static final int[] baseKernel = + { + 10, -61, 284, -615, 1359, -1753, 5911, 22498, + 14, -71, 295, -616, 1314, -1615, 5259, 22472, + 17, -80, 304, -611, 1260, -1468, 4626, 22402, + 21, -88, 309, -603, 1200, -1313, 4015, 22285, + 23, -94, 313, -589, 1134, -1151, 3426, 22122, + 26, -100, 313, -572, 1063, -986, 2861, 21915, + 28, -104, 312, -550, 986, -818, 2322, 21663, + 30, -108, 308, -525, 906, -648, 1810, 21369, + 31, -110, 302, -497, 823, -478, 1326, 21034, + 33, -112, 295, -466, 737, -309, 871, 20657, + 34, -112, 285, -433, 649, -143, 446, 20242, + 34, -111, 274, -397, 561, 19, 51, 19790, + 34, -110, 261, -359, 472, 176, -313, 19302, + 35, -108, 247, -320, 383, 327, -646, 18781, + 34, -105, 232, -280, 296, 472, -948, 18230, + 34, -101, 216, -240, 210, 608, -1219, 17651, + 33, -97, 199, -199, 126, 736, -1459, 17045, + 32, -92, 182, -158, 45, 855, -1668, 16413, + 31, -86, 164, -117, -33, 964, -1847, 15761, + 30, -80, 145, -77, -107, 1063, -1996, 15091, + 28, -74, 127, -38, -177, 1151, -2117, 14405, + 26, -67, 108, 0, -243, 1228, -2211, 13706, + 24, -60, 90, 37, -304, 1294, -2277, 12996, + 22, -53, 72, 72, -360, 1349, -2318, 12278, + 20, -46, 54, 105, -410, 1392, -2334, 11556, + 18, -39, 37, 136, -455, 1425, -2327, 10831, + 15, -31, 21, 164, -495, 1446, -2298, 10107, + 13, -24, 5, 191, -529, 1456, -2249, 9385, + 10, -17, -10, 215, -557, 1456, -2182, 8669, + 8, -10, -24, 236, -580, 1446, -2096, 7962, + 5, -3, -37, 255, -597, 1426, -1996, 7265, + 3, 4, -50, 271, -608, 1397, -1881, 6580, + 0, 10, -61, 284, -615, 1359, -1753, 5911, + }; +} + +// Stereo sound buffer with center channel + +final class StereoBuffer { + private BlipBuffer[] bufs = new BlipBuffer[3]; + + // Same behavior as in BlipBuffer unless noted + + public StereoBuffer() { + for (int i = bufs.length; --i >= 0; ) + bufs[i] = new BlipBuffer(); + } + + public void setSampleRate(int rate, int msec) { + for (int i = bufs.length; --i >= 0; ) + bufs[i].setSampleRate(rate, msec); + } + + public void setClockRate(int rate) { + for (int i = bufs.length; --i >= 0; ) + bufs[i].setClockRate(rate); + } + + public int clockRate() { + return bufs[0].clockRate(); + } + + public int countSamples(int time) { + return bufs[0].countSamples(time); + } + + public void clear() { + for (int i = bufs.length; --i >= 0; ) + bufs[i].clear(); + } + + public void setVolume(double v) { + for (int i = bufs.length; --i >= 0; ) + bufs[i].setVolume(v); + } + + // The three channels that are mixed together + // left output = left + center + // right output = right + center + public BlipBuffer center() { + return bufs[2]; + } + + public BlipBuffer left() { + return bufs[0]; + } + + public BlipBuffer right() { + return bufs[1]; + } + + public void endFrame(int time) { + for (int i = bufs.length; --i >= 0; ) + bufs[i].endFrame(time); + } + + public int samplesAvail() { + return bufs[2].samplesAvail() << 1; + } + + // Output is in stereo, so count must always be a multiple of 2 + public int readSamples(byte[] out, int start, int count) { + assert (count & 1) == 0; + + final int avail = samplesAvail(); + if (count > avail) + count = avail; + + if ((count >>= 1) > 0) { + // TODO: optimize for mono case + + // calculate center in place + final int[] mono = bufs[2].buf; + { + int accum = bufs[2].accum; + int i = 0; + do { + mono[i] = (accum += mono[i] - (accum >> 9)); + } + while (++i < count); + bufs[2].accum = accum; + } + + // calculate left and right + for (int ch = 2; --ch >= 0; ) { + // add right and output + final int[] buf = bufs[ch].buf; + int accum = bufs[ch].accum; + int pos = (start + ch) << 1; + int i = 0; + do { + int s = ((accum += buf[i] - (accum >> 9)) + mono[i]) >> 15; + + // clamp to 16 bits + if ((short) s != s) + s = (s >> 24) ^ 0x7FFF; + + // write as big endian + out[pos] = (byte) (s >> 8); + out[pos + 1] = (byte) s; + pos += 4; + } + while (++i < count); + bufs[ch].accum = accum; + } + + for (int i = bufs.length; --i >= 0; ) + bufs[i].removeSamples(count); + } + return count << 1; + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/ClassicEmu.java b/src/uk/me/fantastic/retro/music/gme/ClassicEmu.java new file mode 100755 index 0000000..6155e6b --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/ClassicEmu.java @@ -0,0 +1,76 @@ +package uk.me.fantastic.retro.music.gme;// Common aspects of emulators which use BlipBuffer for sound output +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +class ClassicEmu extends MusicEmu { + protected int setSampleRate_(int rate) { + buf.setSampleRate(rate, 1000 / bufLength); + return rate; + } + + public void startTrack(int track) { + super.startTrack(track); + buf.clear(); + } + + protected int play_(byte[] out, int count) { + int pos = 0; + while (true) { + int n = buf.readSamples(out, pos, count); + mixSamples(out, pos, n); + + pos += n; + count -= n; + if (count <= 0) + break; + + if (trackEnded_) { + java.util.Arrays.fill(out, pos, pos + count, (byte) 0); + break; + } + + int clocks = runMsec(bufLength); + buf.endFrame(clocks); + } + return pos; + } + + protected final int countSamples(int time) { + return buf.countSamples(time); + } + + protected void mixSamples(byte[] out, int offset, int count) { + // derived class can override and mix its own samples here + } + +// internal + + static final int bufLength = 32; + protected StereoBuffer buf = new StereoBuffer(); + + protected void setClockRate(int rate) { + buf.setClockRate(rate); + } + + // Subclass should run here for at most clockCount and return actual + // number of clocks emulated (can be less) + protected int runClocks(int clockCount) { + return 0; + } + + // Subclass can also get number of msec to run, and return number of clocks emulated + protected int runMsec(int msec) { + assert bufLength == 32; + return runClocks(buf.clockRate() >> 5); + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/DataReader.java b/src/uk/me/fantastic/retro/music/gme/DataReader.java new file mode 100755 index 0000000..17d376e --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/DataReader.java @@ -0,0 +1,69 @@ +package uk.me.fantastic.retro.music.gme;// Helpers for loading/decompressing data from various sources +// http://www.slack.net/~ant/ + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +class DataReader { + // Opens InputStream to file stored in various ways + static InputStream openHttp(String path) throws Exception { + return new URL(path).openConnection().getInputStream(); + } + + static InputStream openFile(String path) throws Exception { + return new FileInputStream(new File(path)); + } + + static InputStream openGZIP(InputStream in) throws Exception { + return new GZIPInputStream(in); + } + + // "Resizes" array to new size and preserves elements from in + static byte[] resize(byte[] in, int size) { + byte[] out = new byte[size]; + if (size > in.length) + size = in.length; + System.arraycopy(in, 0, out, 0, size); + return out; + } + + // Loads entire stream into byte array, then closes stream + static byte[] loadData(InputStream in) throws Exception { + byte[] data = new byte[256 * 1024]; + int size = 0; + int count; + while ((count = in.read(data, size, data.length - size)) != -1) { + size += count; + if (size >= data.length) + data = resize(data, data.length * 2); + } + in.close(); + + if (data.length - size > data.length / 4) + data = resize(data, size); + + return data; + } + + // Loads stream into ByteArrayInputStream + static ByteArrayInputStream cacheStream(InputStream in) throws Exception { + return new ByteArrayInputStream(loadData(in)); + } + + // Finds file named 'path' inside zip file, or returns null if not found. + // You should use a BufferedInputStream or cacheStream() for input. + static InputStream openZip(InputStream in, String path) throws Exception { + ZipInputStream zis = new ZipInputStream(in); + for (ZipEntry entry; (entry = zis.getNextEntry()) != null; ) { + if (path.equals(entry.getName())) + return zis; + } + return null; + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/GbApu.java b/src/uk/me/fantastic/retro/music/gme/GbApu.java new file mode 100755 index 0000000..812044b --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/GbApu.java @@ -0,0 +1,764 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo Game Boy sound emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +class GbOsc { + static final boolean gbc_02 = false; // TODO: allow to be set? + static final int trigger_mask = 0x80; + static final int length_enabled = 0x40; + static final int dac_bias = 7; + + BlipBuffer output; + int output_select; + final int[] regs = new int[5]; + + int vol_unit; + int delay; + int last_amp; + int length; + int enabled; + + void reset() { + output = null; + output_select = 0; + delay = 0; + last_amp = 0; + length = 64; + enabled = 0; + + for (int i = 5; --i >= 0; ) + regs[i] = 0; + } + + void clock_length() { + if ((regs[4] & length_enabled) != 0 && length != 0) { + if (--length <= 0) + enabled = 0; + } + } + + int frequency() { + return (regs[4] & 7) * 0x100 + regs[3]; + } + + boolean write_register(int frame_phase, int reg, int old_data, int data) { + return false; + } + + int write_trig(int frame_phase, int max_len, int old_data) { + int data = regs[4]; + + if (gbc_02 && (frame_phase & 1) != 0 && (old_data & length_enabled) == 0 && length != 0) + length--; + + if ((data & trigger_mask) != 0) { + enabled = 1; + if (length == 0) { + length = max_len; + if (gbc_02 && (frame_phase & 1) != 0 && (data & length_enabled) != 0) + length--; + } + } + + if (gbc_02 && length == 0) + enabled = 0; + + return data & trigger_mask; + } +} + +class GbEnv extends GbOsc { + int env_delay; + int volume; + + int dac_enabled() { + return regs[2] & 0xF8; + } + + void reset() { + env_delay = 0; + volume = 0; + super.reset(); + } + + int reload_env_timer() { + int raw = regs[2] & 7; + env_delay = (raw != 0 ? raw : 8); + return raw; + } + + void clock_envelope() { + if (--env_delay <= 0 && reload_env_timer() != 0) { + int v = volume + ((regs[2] & 0x08) != 0 ? +1 : -1); + if (0 <= v && v <= 15) + volume = v; + } + } + + boolean write_register(int frame_phase, int reg, int old_data, int data) { + final int max_len = 64; + + switch (reg) { + case 1: + length = max_len - (data & (max_len - 1)); + break; + + case 2: + if (dac_enabled() == 0) + enabled = 0; + + // TODO: once zombie mode used, envelope not clocked? + if (((old_data ^ data) & 8) != 0) { + int step = 0; + if ((old_data & 7) != 0) + step = +1; + else if ((data & 7) != 0) + step = -1; + + if ((data & 8) != 0) + step = -step; + + volume = (15 + step - volume) & 15; + } else { + int step = ((old_data & 7) != 0 ? 2 : 0) | ((data & 7) != 0 ? 0 : 1); + volume = (volume + step) & 15; + } + break; + + case 4: + if (write_trig(frame_phase, max_len, old_data) != 0) { + volume = regs[2] >> 4; + reload_env_timer(); + if (frame_phase == 7) + env_delay++; + if (dac_enabled() == 0) + enabled = 0; + return true; + } + } + return false; + } +} + +class GbSquare extends GbEnv { + int phase; + + final int period() { + return (2048 - frequency()) * 4; + } + + void reset() { + phase = 0; + super.reset(); + delay = 0x40000000; // TODO: less hacky (never clocked until first trigger) + } + + boolean write_register(int frame_phase, int reg, int old_data, int data) { + boolean result = super.write_register(frame_phase, reg, old_data, data); + if (result) + delay = period(); + return result; + } + + static final byte[] duty_offsets = {1, 1, 3, 7}; + static final byte[] duties = {1, 2, 4, 6}; + + void run(int time, int end_time) { + final int duty_code = regs[1] >> 6; + final int duty_offset = duty_offsets[duty_code]; + final int duty = duties[duty_code]; + int playing = 0; + int amp = 0; + int phase = (this.phase + duty_offset) & 7; + + if (output != null) { + if (volume != 0) { + playing = -enabled; + + if (phase < duty) + amp = volume & playing; + + // Treat > 16 kHz as DC + if (frequency() > 2041 && delay < 32) { + amp = (volume * duty) >> 3 & playing; + playing = 0; + } + } + + if (dac_enabled() == 0) { + playing = 0; + amp = 0; + } else { + amp -= dac_bias; + } + + int delta = amp - last_amp; + if (delta != 0) { + last_amp = amp; + output.addDelta(time, delta * vol_unit); + } + } + + time += delay; + if (time < end_time) { + final int period = this.period(); + if (playing == 0) { + // maintain phase + int count = (end_time - time + period - 1) / period; + phase = (phase + count) & 7; + time += count * period; + } else { + final BlipBuffer output = this.output; + // TODO: eliminate ugly +dac_bias -dac_bias adjustments + int delta = ((amp + dac_bias) * 2 - volume) * vol_unit; + do { + if ((phase = (phase + 1) & 7) == 0 || phase == duty) + output.addDelta(time, delta = -delta); + } + while ((time += period) < end_time); + + last_amp = (delta < 0 ? 0 : volume) - dac_bias; + } + this.phase = (phase - duty_offset) & 7; + } + delay = time - end_time; + } +} + +final class GbSweepSquare extends GbSquare { + static final int period_mask = 0x70; + static final int shift_mask = 0x07; + + int sweep_freq; + int sweep_delay; + int sweep_enabled; + int sweep_neg; + + void reset() { + sweep_freq = 0; + sweep_delay = 0; + sweep_enabled = 0; + sweep_neg = 0; + super.reset(); + } + + void reload_sweep_timer() { + sweep_delay = (regs[0] & period_mask) >> 4; + if (sweep_delay == 0) + sweep_delay = 8; + } + + void calc_sweep(boolean update) { + int freq = sweep_freq; + int shift = regs[0] & shift_mask; + int delta = freq >> shift; + sweep_neg = regs[0] & 0x08; + if (sweep_neg != 0) + delta = -delta; + freq += delta; + + if (freq > 0x7FF) { + enabled = 0; + } else if (shift != 0 && update) { + sweep_freq = freq; + regs[3] = freq & 0xFF; + regs[4] = (regs[4] & ~0x07) | (freq >> 8 & 0x07); + } + } + + void clock_sweep() { + if (--sweep_delay <= 0) { + reload_sweep_timer(); + if (sweep_enabled != 0 && (regs[0] & period_mask) != 0) { + calc_sweep(true); + calc_sweep(false); + } + } + } + + boolean write_register(int frame_phase, int reg, int old_data, int data) { + if (reg == 0 && (sweep_neg & 0x08 & ~data) != 0) + enabled = 0; + + if (super.write_register(frame_phase, reg, old_data, data)) { + sweep_freq = frequency(); + reload_sweep_timer(); + sweep_enabled = regs[0] & (period_mask | shift_mask); + if ((regs[0] & shift_mask) != 0) + calc_sweep(false); + } + + return false; + } +} + +final class GbNoise extends GbEnv { + int bits; + + boolean write_register(int frame_phase, int reg, int old_data, int data) { + if (reg == 3) { + int p = period(); + if (p != 0) + delay %= p; // TODO: not entirely correct + } + + if (super.write_register(frame_phase, reg, old_data, data)) + bits = 0x7FFF; + + return false; + } + + static final byte[] noise_periods = {8, 16, 32, 48, 64, 80, 96, 112}; + + int period() { + int shift = regs[3] >> 4; + int p = noise_periods[regs[3] & 7] << shift; + if (shift >= 0x0E) + p = 0; + return p; + } + + void run(int time, int end_time) { + int feedback = (1 << 14) >> (regs[3] & 8); + int playing = 0; + int amp = 0; + + if (output != null) { + if (volume != 0) { + playing = -enabled; + + if ((bits & 1) == 0) + amp = volume & playing; + } + + if (dac_enabled() != 0) { + amp -= dac_bias; + } else { + amp = 0; + playing = 0; + } + + int delta = amp - last_amp; + if (delta != 0) { + last_amp = amp; + output.addDelta(time, delta * vol_unit); + } + } + + time += delay; + if (time < end_time) { + final int period = this.period(); + if (period == 0) { + time = end_time; + } else { + int bits = this.bits; + if (playing == 0) { + // maintain phase + int count = (end_time - time + period - 1) / period; + time += count * period; + + // TODO: be sure this doesn't drag performance too much + bits ^= (feedback << 1) & -(bits & 1); + feedback *= 3; + do { + bits = (bits >> 1) ^ (feedback & -(bits & 2)); + } + while (--count > 0); + bits &= ~(feedback << 1); + } else { + final BlipBuffer output = this.output; + // TODO: eliminate ugly +dac_bias -dac_bias adjustments + int delta = ((amp + dac_bias) * 2 - volume) * vol_unit; + + do { + int changed = bits + 1; + bits >>= 1; + if ((changed & 2) != 0) { + bits |= feedback; + output.addDelta(time, delta = -delta); + } + } + while ((time += period) < end_time); + + last_amp = (delta < 0 ? 0 : volume) - dac_bias; + } + this.bits = bits; + } + } + delay = time - end_time; + } +} + +final class GbWave extends GbOsc { + int wave_pos; + int sample_buf_high; + int sample_buf; + static final int wave_size = 32; + int[] wave = new int[wave_size]; + + int period() { + return (2048 - frequency()) * 2; + } + + int dac_enabled() { + return regs[0] & 0x80; + } + + int access(int addr) { + if (enabled != 0) + addr = 0xFF30 + (wave_pos >> 1); + return addr; + } + + void reset() { + wave_pos = 0; + sample_buf_high = 0; + sample_buf = 0; + length = 256; + super.reset(); + } + + boolean write_register(int frame_phase, int reg, int old_data, int data) { + final int max_len = 256; + + switch (reg) { + case 1: + length = max_len - data; + break; + + case 4: + if (write_trig(frame_phase, max_len, old_data) != 0) { + wave_pos = 0; + delay = period() + 6; + sample_buf = sample_buf_high; + } + // fall through + case 0: + if (dac_enabled() == 0) + enabled = 0; + } + + return false; + } + + void run(int time, int end_time) { + int volume_shift = regs[2] >> 5 & 3; + int playing = 0; + + if (output != null) { + playing = -enabled; + if (--volume_shift < 0) { + volume_shift = 7; + playing = 0; + } + + int amp = sample_buf & playing; + + if (frequency() > 0x7FB && delay < 16) { + // 16 kHz and above, act as DC at mid-level + // (really depends on average level of entire wave, + // but this is good enough) + amp = 8; + playing = 0; + } + + amp >>= volume_shift; + + if (dac_enabled() == 0) { + playing = 0; + amp = 0; + } else { + amp -= dac_bias; + } + + int delta = amp - last_amp; + if (delta != 0) { + last_amp = amp; + output.addDelta(time, delta * vol_unit); + } + } + + time += delay; + if (time < end_time) { + int wave_pos = (this.wave_pos + 1) & (wave_size - 1); + final int period = this.period(); + if (playing == 0) { + // maintain phase + int count = (end_time - time + period - 1) / period; + wave_pos += count; // will be masked below + time += count * period; + } else { + final BlipBuffer output = this.output; + int last_amp = this.last_amp + dac_bias; + do { + int amp = wave[wave_pos] >> volume_shift; + wave_pos = (wave_pos + 1) & (wave_size - 1); + int delta; + if ((delta = amp - last_amp) != 0) { + last_amp = amp; + output.addDelta(time, delta * vol_unit); + } + } + while ((time += period) < end_time); + this.last_amp = last_amp - dac_bias; + } + wave_pos = (wave_pos - 1) & (wave_size - 1); + this.wave_pos = wave_pos; + if (enabled != 0) { + sample_buf_high = wave[wave_pos & ~1]; + sample_buf = wave[wave_pos]; + } + } + delay = time - end_time; + } +} + +final public class GbApu { + public GbApu() { + oscs[0] = square1; + oscs[1] = square2; + oscs[2] = wave; + oscs[3] = noise; + + reset(); + } + + // Resets oscillators and internal state + public void setOutput(BlipBuffer center, BlipBuffer left, BlipBuffer right) { + outputs[1] = right; + outputs[2] = left; + outputs[3] = center; + + for (int i = osc_count; --i >= 0; ) + oscs[i].output = outputs[oscs[i].output_select]; + } + + private void update_volume() { + final int unit = (int) (1.0 / osc_count / 15 / 8 * 65536); + + // TODO: doesn't handle left != right volume (not worth the complexity) + int data = regs[vol_reg - startAddr]; + int left = data >> 4 & 7; + int right = data & 7; + int vol_unit = (left > right ? left : right) * unit; + for (int i = osc_count; --i >= 0; ) + oscs[i].vol_unit = vol_unit; + } + + private void reset_regs() { + for (int i = 0x20; --i >= 0; ) + regs[i] = 0; + + for (int i = osc_count; --i >= 0; ) + oscs[i].reset(); + + update_volume(); + } + + static final int initial_wave[] = { + 0x84, 0x40, 0x43, 0xAA, 0x2D, 0x78, 0x92, 0x3C, + 0x60, 0x59, 0x59, 0xB0, 0x34, 0xB8, 0x2E, 0xDA + }; + + public void reset() { + frame_time = 0; + last_time = 0; + frame_phase = 0; + + reset_regs(); + + for (int i = 16; --i >= 0; ) + write(0, i + wave_ram, initial_wave[i]); + } + + private void run_until(int end_time) { + assert end_time >= last_time; // end_time must not be before previous time + if (end_time == last_time) + return; + + while (true) { + // run oscillators + int time = end_time; + if (time > frame_time) + time = frame_time; + + square1.run(last_time, time); + square2.run(last_time, time); + wave.run(last_time, time); + noise.run(last_time, time); + last_time = time; + + if (time == end_time) + break; + + // run frame sequencer + frame_time += frame_period; + switch (frame_phase++) { + case 2: + case 6: + // 128 Hz + square1.clock_sweep(); + case 0: + case 4: + // 256 Hz + square1.clock_length(); + square2.clock_length(); + wave.clock_length(); + noise.clock_length(); + break; + + case 7: + // 64 Hz + frame_phase = 0; + square1.clock_envelope(); + square2.clock_envelope(); + noise.clock_envelope(); + } + } + } + + // Runs all oscillators up to specified time, ends current time frame, then + // starts a new frame at time 0 + public void endFrame(int end_time) { + if (end_time > last_time) + run_until(end_time); + + assert frame_time >= end_time; + frame_time -= end_time; + + assert last_time >= end_time; + last_time -= end_time; + } + + static void silence_osc(int time, GbOsc osc) { + int amp = osc.last_amp; + if (amp != 0) { + osc.last_amp = 0; + if (osc.output != null) + osc.output.addDelta(time, -amp * osc.vol_unit); + } + } + + // Reads and writes at addr must satisfy start_addr <= addr <= end_addr + public static final int startAddr = 0xFF10; + public static final int endAddr = 0xFF3F; + + public void write(int time, int addr, int data) { + assert startAddr <= addr && addr <= endAddr; + assert 0 <= data && data < 0x100; + + if (addr < status_reg && (regs[status_reg - startAddr] & power_mask) == 0) + return; + + run_until(time); + int reg = addr - startAddr; + if (addr < wave_ram) { + int old_data = regs[reg]; + regs[reg] = data; + + if (addr < vol_reg) { + int index = reg / 5; + GbOsc osc = oscs[index]; + int r = reg - index * 5; + osc.regs[r] = data; + osc.write_register(frame_phase, r, old_data, data); + } else if (addr == vol_reg && data != old_data) { + for (int i = osc_count; --i >= 0; ) + silence_osc(time, oscs[i]); + + update_volume(); + } else if (addr == stereo_reg) { + for (int i = osc_count; --i >= 0; ) { + GbOsc osc = oscs[i]; + int bits = data >> i; + osc.output_select = (bits >> 3 & 2) | (bits & 1); + BlipBuffer output = outputs[osc.output_select]; + if (osc.output != output) { + silence_osc(time, osc); + osc.output = output; + } + } + } else if (addr == status_reg && ((data ^ old_data) & power_mask) != 0) { + frame_phase = 0; + if ((data & power_mask) == 0) { + for (int i = osc_count; --i >= 0; ) + silence_osc(time, oscs[i]); + + reset_regs(); + } + } + } else // wave data + { + addr = wave.access(addr); + regs[addr - startAddr] = data; + int index = (addr & 0x0F) * 2; + wave.wave[index] = data >> 4; + wave.wave[index + 1] = data & 0x0F; + } + } + + static final int masks[] = { + 0x80, 0x3F, 0x00, 0xFF, 0xBF, + 0xFF, 0x3F, 0x00, 0xFF, 0xBF, + 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, + 0xFF, 0xFF, 0x00, 0x00, 0xBF, + 0x00, 0x00, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + + // Reads from address at specified time + public int read(int time, int addr) { + assert startAddr <= addr && addr <= endAddr; + + run_until(time); + + if (addr >= wave_ram) + addr = wave.access(addr); + + int index = addr - startAddr; + int data = regs[index]; + if (index < masks.length) + data |= masks[index]; + + if (addr == status_reg) { + data &= 0xF0; + if (square1.enabled != 0) data |= 1; + if (square2.enabled != 0) data |= 2; + if (wave.enabled != 0) data |= 4; + if (noise.enabled != 0) data |= 8; + } + + return data; + } + + static final int vol_reg = 0xFF24; + static final int stereo_reg = 0xFF25; + static final int status_reg = 0xFF26; + static final int wave_ram = 0xFF30; + static final int frame_period = 4194304 / 512; // 512 Hz + + static final int power_mask = 0x80; + + static final int osc_count = 4; + final GbOsc[] oscs = new GbOsc[osc_count]; + int frame_time; + int last_time; + int frame_phase; + final BlipBuffer[] outputs = new BlipBuffer[4]; + + final GbSweepSquare square1 = new GbSweepSquare(); + final GbSquare square2 = new GbSquare(); + final GbWave wave = new GbWave(); + final GbNoise noise = new GbNoise(); + final int[] regs = new int[endAddr - startAddr + 1]; +} diff --git a/src/uk/me/fantastic/retro/music/gme/GbCpu.java b/src/uk/me/fantastic/retro/music/gme/GbCpu.java new file mode 100755 index 0000000..4154477 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/GbCpu.java @@ -0,0 +1,1154 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo Game Boy GB-Z80 CPU emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +public class GbCpu extends ClassicEmu { + public GbCpu() { + rstBase = 0; + } + + // Resets registers, uses supplied physical memory, and + // maps all memory pages to unmapped + public final void reset(byte[] mem, int unmapped) { + this.mem = mem; + a = 0; + bc = 0; + de = 0; + hl = 0; + pc = 0; + sp = 0xFFFF; + ph = 0x100; + cz = 1; + + time = 0; + + for (int i = 0; i < pageCount + 1; i++) + mapPage(i, unmapped); + } + + static final int pageShift = 13; + static final int pageCount = 0x10000 >> pageShift; + static final int pageSize = 1 << pageShift; + + // Maps address range to offset in physical memory + public final void mapMemory(int addr, int size, int offset) { + assert addr % pageSize == 0; + assert size % pageSize == 0; + int firstPage = addr / pageSize; + for (int i = size / pageSize; i-- > 0; ) + mapPage(firstPage + i, offset + i * pageSize); + } + + // Maps address to memory + public final int mapAddr(int addr) { + return pages[addr >> pageShift] + addr; + } + + // Emulation + + // Registers. NOT kept updated during runCpu() + public int a, bc, de, hl, sp, pc; + + // Base address for RST vectors (normally 0) + public int rstBase; + + // Current time + public int time; + + // Memory read and write handlers + protected int cpuRead(int addr) { + return 0; + } + + protected void cpuWrite(int addr, int data) { + } + + int pages[] = new int[pageCount + 1]; + int cz, ph; + byte[] mem; + + final void mapPage(int page, int offset) { + if (debug) assert 0 <= page && page < pageCount + 1; + pages[page] = offset - page * pageSize; + } + + // Runs until time >= 0 + public final void runCpu() { + // locals are faster, and first three are more efficient to access + final byte[] mem = this.mem; + int pc = this.pc; + int data = 0; + + int time = this.time; + int a = this.a; + int bc = this.bc; + int de = this.de; + int hl = this.hl; + int sp = this.sp; + int cz = this.cz; + int ph = this.ph; + final int pages[] = this.pages; + final int instrTimes[] = this.instrTimes; + + loop: + while (time < 0) { + if (debug) { + assert 0 <= a && a < 0x00100; + assert 0 <= bc && bc < 0x10000; + assert 0 <= de && de < 0x10000; + assert 0 <= hl && hl < 0x10000; + assert 0 <= pc && pc < 0x10000; + assert 0 <= pc && pc < 0x10000; + } + + int instr; + int opcode; + if ((opcode = mem[instr = pages[pc >> pageShift] + pc] & 0xFF) == 0xCB) { + // CB + + // Source + this.time = (time += cbTimes[opcode = mem[instr + 1] & 0xFF]); + pc += 2; + int operand; + switch ((operand = opcode & 7)) { + case 0: + data = bc >> 8; + break; + case 1: + data = bc & 0xFF; + break; + case 2: + data = de >> 8; + break; + case 3: + data = de & 0xFF; + break; + case 4: + data = hl >> 8; + break; + case 5: + data = hl & 0xFF; + break; + case 6: + data = cpuRead(hl); + break; + default: + data = a; + break; + } + + // Operation + int operation; + switch ((operation = opcode >> 3)) { + case 0x08: // BIT 0,r + case 0x09: // BIT 1,r + case 0x0A: // BIT 2,r + case 0x0B: // BIT 3,r + case 0x0C: // BIT 4,r + case 0x0D: // BIT 5,r + case 0x0E: // BIT 6,r + case 0x0F: // BIT 7,r + cz = (cz & 0x100) | (data >> (operation - 0x08) & 1); + ph = (cz | 0x100) ^ 0x10; // N=0 H=1 + continue; + + case 0x00: // RLC r + cz = (data << 1 & 0x100) | data; // Z=* C=* + ph = data | 0x100; // N=0 H=0 + data = (data << 1 & 0xFF) | (data >> 7); + break; + + case 0x01: // RRC r + cz = (data << 8) | data; // Z=* C=* + ph = data | 0x100; // N=0 H=0 + data = ((data & 1) << 7) | (data >> 1); + break; + + case 0x04: // SLA r + cz = 0; + case 0x02: // RL r + cz = (data << 1) | (cz >> 8 & 1); // Z=* C=* + data = cz & 0xFF; + ph = cz | 0x100; // N=0 H=0 + break; + + case 0x05: // SRA r + cz = data << 1; + case 0x03: // RR r + data |= cz & 0x100; + case 0x07: // SRL r + cz = data << 8; // Z=* C=* + data >>= 1; + cz |= data; + ph = data | 0x100; // N=0 H=0 + break; + + case 0x06: // SWAP + data = (data >> 4) | (data << 4 & 0xFF); + cz = data; + ph = cz | 0x100; + break; + + case 0x10: // RES 0,r + case 0x11: // RES 1,r + case 0x12: // RES 2,r + case 0x13: // RES 3,r + case 0x14: // RES 4,r + case 0x15: // RES 5,r + case 0x16: // RES 6,r + case 0x17: // RES 7,r + data &= ~(1 << (operation - 0x10)); + break; + + default: + /* + assert false; + case 0x18: // SET 0,r + case 0x19: // SET 1,r + case 0x1A: // SET 2,r + case 0x1B: // SET 3,r + case 0x1C: // SET 4,r + case 0x1D: // SET 5,r + case 0x1E: // SET 6,r + case 0x1F: // SET 7,r + */ + data |= 1 << (operation - 0x18); + break; + } + + // Dest + switch (operand) { + case 0: + bc = data << 8 | (bc & 0xFF); + continue; + case 1: + bc = (bc & 0xFF00) | data; + continue; + case 2: + de = data << 8 | (de & 0xFF); + continue; + case 3: + de = (de & 0xFF00) | data; + continue; + case 4: + hl = data << 8 | (hl & 0xFF); + continue; + case 5: + hl = (hl & 0xFF00) | data; + continue; + case 6: + cpuWrite(hl, data); + continue; + default: + a = data; + continue; + } + } + + // Normal instruction + pc++; + this.time = (time += instrTimes[opcode]); + + // Source + switch (opcode) { + case 0xF3: // DI + // TODO: implement + continue; + + case 0xFB: // EI + // TODO: implement + continue; + + case 0x76: // HALT + case 0x10: // STOP + case 0xD3: + case 0xDB: + case 0xDD: // Illegal + case 0xE3: + case 0xE4: + case 0xEB: + case 0xEC: + case 0xED: + case 0xF4: + case 0xFC: + case 0xFD: + pc--; + break loop; + + case 0xE9: // LD PC,HL + pc = hl; + continue; + + case 0xF9: // LD SP,HL + sp = hl; + continue; + + case 0x37: // SCF + ph = cz | 0x100; // N=0 H=0 + cz |= 0x100; // C=1 Z=- + continue; + + case 0x3F: // CCF + ph = cz | 0x100; // N=0 H=0 + cz ^= 0x100; // C=* Z=- + continue; + + case 0x2F: // CPL + a ^= 0xFF; + ph = ~cz & 0xFF; // N=1 H=1 + continue; + + case 0x27: {// DAA + int h = ph ^ cz; + if ((ph & 0x100) != 0) { + if ((h & 0x10) != 0 || (a & 0x0F) > 9) + a += 6; + + if ((cz & 0x100) != 0 || a > 0x9F) + a += 0x60; + } else { + if ((h & 0x10) != 0) { + a -= 6; + if ((cz & 0x100) == 0) + a &= 0xFF; + } + + if ((cz & 0x100) != 0) + a -= 0x60; + } + + cz = (cz & 0x100) | a; + a &= 0xFF; + ph = (ph & 0x100) | a; + continue; + } + + case 0xC0: // RET NZ + if (((byte) cz) != 0) + break; + continue; + + case 0xC8: // RET Z + if (((byte) cz) == 0) + break; + continue; + + case 0xD0: // RET NC + if ((cz & 0x100) == 0) + break; + continue; + + case 0xD8: // RET C + if ((cz & 0x100) != 0) + break; + continue; + + case 0xF5: // PUSH AF + data = (cz >> 4 & 0x10) | (a << 8); + data |= ~ph >> 2 & 0x40; + data |= (ph ^ cz) << 1 & 0x20; + if (((byte) cz) == 0) + data |= 0x80; + break; + + case 0x22: // LD (HL+),A + case 0x2A: // LD A,(HL+) + data = hl; + hl = (hl + 1) & 0xFFFF; + break; + + case 0x32: // LD (HL-),A + case 0x3A: // LD A,(HL-) + data = hl; + hl = (hl - 1) & 0xFFFF; + break; + + case 0x7E: // LD A,(HL) + case 0x23: // INC HL + case 0x29: // ADD HL,HL + case 0x2B: // DEC HL + case 0xE5: // PUSH HL + data = hl; + break; + + case 0x02: // LD (BC),A + case 0x0A: // LD A,(BC) + case 0x03: // INC BC + case 0x09: // ADD HL,BC + case 0x0B: // DEC BC + case 0xC5: // PUSH BC + data = bc; + break; + + case 0x12: // LD (DE),A + case 0x1A: // LD A,(DE) + case 0x13: // INC DE + case 0x19: // ADD HL,DE + case 0x1B: // DEC DE + case 0xD5: // PUSH DE + data = de; + break; + + case 0x33: // INC SP + case 0x39: // ADD HL,SP + case 0x3B: // DEC SP + data = sp; + break; + + case 0xF0: // LDH A,(n) + case 0xE0: // LDH (n),A + case 0x06: // LD B,n + case 0x0E: // LD C,n + case 0x16: // LD D,n + case 0x1E: // LD E,n + case 0x26: // LD H,n + case 0x2E: // LD L,n + case 0x36: // LD (HL),n + case 0x3E: // LD A,n + case 0xC6: // ADD n + case 0xCE: // ADC n + case 0xD6: // SUB n + case 0xDE: // SBC n + case 0xE6: // AND n + case 0xEE: // XOR n + case 0xF6: // OR n + case 0xFE: // CP n + case 0x18: // JR r + case 0x20: // JR NZ,r + case 0x28: // JR Z,r + case 0x30: // JR NC,r + case 0x38: // JR C,r + case 0xF8: // LD HL,SPs + case 0xE8: // ADD SP,s + data = mem[instr + 1] & 0xFF; + pc++; + break; + + case 0x01: // LD BC,nn + case 0x11: // LD DE,nn + case 0x21: // LD HL,nn + case 0x31: // LD SP,nn + case 0xC2: // JP NZ,nn + case 0xC3: // JP nn + case 0xC4: // CALL NZ,nn + case 0xCA: // JP Z,nn + case 0xCC: // CALL Z,nn + case 0xCD: // CALL nn + case 0xD2: // JP NC,nn + case 0xD4: // CALL NC,nn + case 0xDA: // JP C,nn + case 0xDC: // CALL C,nn + case 0xEA: // LD (nn),A + case 0x08: // LD (nn),SP + case 0xFA: // LD A,(nn) + data = (mem[instr + 2] & 0xFF) << 8 | (mem[instr + 1] & 0xFF); + pc += 2; + break; + + case 0x34: // INC (HL) + case 0x35: // DEC (HL) + case 0x46: // LD B,(HL) + case 0x4E: // LD C,(HL) + case 0x56: // LD D,(HL) + case 0x5E: // LD E,(HL) + case 0x66: // LD H,(HL) + case 0x6E: // LD L,(HL) + case 0x86: // ADD (HL) + case 0x8E: // ADC (HL) + case 0x96: // SUB (HL) + case 0x9E: // SBC (HL) + case 0xA6: // AND (HL) + case 0xAE: // XOR (HL) + case 0xB6: // OR (HL) + case 0xBE: // CP (HL) + data = cpuRead(hl); + break; + + case 0x3C: // INC A + case 0x3D: // DEC A + case 0x47: // LD B,A + case 0x4F: // LD C,A + case 0x57: // LD D,A + case 0x5F: // LD E,A + case 0x67: // LD H,A + case 0x6F: // LD L,A + case 0x77: // LD (HL),A + case 0x87: // ADD A + case 0x8F: // ADC A + case 0x97: // SUB A + case 0x9F: // SBC A + case 0xA7: // AND A + case 0xAF: // XOR A + case 0xB7: // OR A + case 0xBF: // CP A + data = a; + break; + + case 0x04: // INC B + case 0x05: // DEC B + case 0x48: // LD C,B + case 0x50: // LD D,B + case 0x58: // LD E,B + case 0x60: // LD H,B + case 0x68: // LD L,B + case 0x70: // LD (HL),B + case 0x78: // LD A,B + case 0x80: // ADD B + case 0x88: // ADC B + case 0x90: // SUB B + case 0x98: // SBC B + case 0xA0: // AND B + case 0xA8: // XOR B + case 0xB0: // OR B + case 0xB8: // CP B + data = bc >> 8; + break; + + case 0xF2: // LDH A,(C) + case 0xE2: // LDH (C),A + case 0x0C: // INC C + case 0x0D: // DEC C + case 0x41: // LD B,C + case 0x51: // LD D,C + case 0x59: // LD E,C + case 0x61: // LD H,C + case 0x69: // LD L,C + case 0x71: // LD (HL),C + case 0x79: // LD A,C + case 0x81: // ADD C + case 0x89: // ADC C + case 0x91: // SUB C + case 0x99: // SBC C + case 0xA1: // AND C + case 0xA9: // XOR C + case 0xB1: // OR C + case 0xB9: // CP C + data = bc & 0xFF; + break; + + case 0x14: // INC D + case 0x15: // DEC D + case 0x42: // LD B,D + case 0x4A: // LD C,D + case 0x5A: // LD E,D + case 0x62: // LD H,D + case 0x6A: // LD L,D + case 0x72: // LD (HL),D + case 0x7A: // LD A,D + case 0x82: // ADD D + case 0x8A: // ADC D + case 0x92: // SUB D + case 0x9A: // SBC D + case 0xA2: // AND D + case 0xAA: // XOR D + case 0xB2: // OR D + case 0xBA: // CP D + data = de >> 8; + break; + + case 0x1C: // INC E + case 0x1D: // DEC E + case 0x43: // LD B,E + case 0x4B: // LD C,E + case 0x53: // LD D,E + case 0x63: // LD H,E + case 0x6B: // LD L,E + case 0x73: // LD (HL),E + case 0x7B: // LD A,E + case 0x83: // ADD E + case 0x8B: // ADC E + case 0x93: // SUB E + case 0x9B: // SBC E + case 0xA3: // AND E + case 0xAB: // XOR E + case 0xB3: // OR E + case 0xBB: // CP E + data = de & 0xFF; + break; + + case 0x24: // INC H + case 0x25: // DEC H + case 0x44: // LD B,H + case 0x4C: // LD C,H + case 0x54: // LD D,H + case 0x5C: // LD E,H + case 0x6C: // LD L,H + case 0x74: // LD (HL),H + case 0x7C: // LD A,H + case 0x84: // ADD H + case 0x8C: // ADC H + case 0x94: // SUB H + case 0x9C: // SBC H + case 0xA4: // AND H + case 0xAC: // XOR H + case 0xB4: // OR H + case 0xBC: // CP H + data = hl >> 8; + break; + + case 0x2C: // INC L + case 0x2D: // DEC L + case 0x45: // LD B,L + case 0x4D: // LD C,L + case 0x55: // LD D,L + case 0x5D: // LD E,L + case 0x65: // LD H,L + case 0x75: // LD (HL),L + case 0x7D: // LD A,L + case 0x85: // ADD L + case 0x8D: // ADC L + case 0x95: // SUB L + case 0x9D: // SBC L + case 0xA5: // AND L + case 0xAD: // XOR L + case 0xB5: // OR L + case 0xBD: // CP L + data = hl & 0xFF; + break; + } + + // Operation + switch (opcode) { + case 0x09: // ADD HL,BC + case 0x19: // ADD HL,DE + case 0x29: // ADD HL,HL + case 0x39: // ADD HL,SP + ph = hl ^ data; + data += hl; + hl = data & 0xFFFF; + ph ^= data; + cz = (cz & 0xFF) | (data >> 8 & 0x100); // C=* Z=- + ph = ((ph >> 8) ^ cz) | 0x100; // N=0 H=* + continue; + + case 0x88: // ADC B + case 0x89: // ADC C + case 0x8A: // ADC D + case 0x8B: // ADC E + case 0x8C: // ADC H + case 0x8D: // ADC L + case 0x8E: // ADC (HL) + case 0x8F: // ADC A + case 0xCE: // ADC n + ph = 0x100 | (a ^ data); // N=0 H=* + cz = a + data + (cz >> 8 & 1); // C=* Z=* + a = cz & 0xFF; + continue; + + case 0x80: // ADD B + case 0x81: // ADD C + case 0x82: // ADD D + case 0x83: // ADD E + case 0x84: // ADD H + case 0x85: // ADD L + case 0x86: // ADD (HL) + case 0x87: // ADD A + case 0xC6: // ADD n + ph = 0x100 | (a ^ data); // N=0 H=* + cz = a + data; // C=* Z=* + a = cz & 0xFF; + continue; + + case 0xB8: // CP B + case 0xB9: // CP C + case 0xBA: // CP D + case 0xBB: // CP E + case 0xBC: // CP H + case 0xBD: // CP L + case 0xBE: // CP (HL) + case 0xBF: // CP A + case 0xFE: // CP n + ph = a ^ data; // N=1 H=* + cz = a - data; // C=* Z=* + continue; + + case 0x90: // SUB B + case 0x91: // SUB C + case 0x92: // SUB D + case 0x93: // SUB E + case 0x94: // SUB H + case 0x95: // SUB L + case 0x96: // SUB (HL) + case 0x97: // SUB A + case 0xD6: // SUB n + ph = a ^ data; // N=1 H=* + cz = a - data; // C=* Z=* + a = cz & 0xFF; + continue; + + case 0x98: // SBC B + case 0x99: // SBC C + case 0x9A: // SBC D + case 0x9B: // SBC E + case 0x9C: // SBC H + case 0x9D: // SBC L + case 0x9E: // SBC (HL) + case 0x9F: // SBC A + case 0xDE: // SBC n + ph = a ^ data; // N=1 H=* + cz = a - data - (cz >> 8 & 1); // C=* Z=* + a = cz & 0xFF; + continue; + + case 0xA0: // AND B + case 0xA1: // AND C + case 0xA2: // AND D + case 0xA3: // AND E + case 0xA4: // AND H + case 0xA5: // AND L + case 0xA6: // AND (HL) + case 0xA7: // AND A + case 0xE6: // AND n + a &= data; + cz = a; // C=0 Z=* + ph = ~a; // N=0 H=1 + continue; + + case 0xB0: // OR B + case 0xB1: // OR C + case 0xB2: // OR D + case 0xB3: // OR E + case 0xB4: // OR H + case 0xB5: // OR L + case 0xB6: // OR (HL) + case 0xB7: // OR A + case 0xF6: // OR n + a |= data; + cz = a; // C=0 Z=* + ph = a | 0x100; // N=0 H=0 + continue; + + case 0xA8: // XOR B + case 0xA9: // XOR C + case 0xAA: // XOR D + case 0xAB: // XOR E + case 0xAC: // XOR H + case 0xAD: // XOR L + case 0xAE: // XOR (HL) + case 0xAF: // XOR A + case 0xEE: // XOR n + a ^= data; + cz = a; // C=0 Z=* + ph = a | 0x100; // N=0 H=0 + continue; + + case 0x17: // RLA + cz = (a << 1) | (cz >> 8 & 1); + ph = cz | 0x100; + a = cz & 0xFF; + cz |= 1; + continue; + + case 0x07: // RLCA + cz = a << 1; + a = (cz & 0xFF) | (a >> 7); + ph = a | 0x100; + cz |= 1; + continue; + + case 0x1F: // RRA + a |= cz & 0x100; + cz = a << 8 | 1; // Z=0 C=* + a >>= 1; + ph = 0x100; // N=0 H=0 + continue; + + case 0x0F: // RRCA + cz = a << 8 | 1; // Z=0 C=* + a = ((a & 1) << 7) | (a >> 1); + ph = 0x100; // N=0 H=0 + continue; + + case 0xE8: // ADD SP,s + case 0xF8: {// LD HL,SPs + int t = (sp + (byte) data) & 0xFFFF; + cz = (((sp & 0xFF) + data) & 0x100) | 1; // Z=0 C=* + ph = (sp ^ data ^ t) | 0x100; // N=0 H=* + data = t; + break; + } + + case 0x0B: // DEC BC + case 0x1B: // DEC DE + case 0x2B: // DEC HL + case 0x3B: // DEC SP + data = (data - 1) & 0xFFFF; + break; + + case 0x05: // DEC B + case 0x0D: // DEC C + case 0x15: // DEC D + case 0x1D: // DEC E + case 0x25: // DEC H + case 0x2D: // DEC L + case 0x35: // DEC (HL) + case 0x3D: // DEC A + ph = data; // N=1 H=* + data = (data - 1) & 0xFF; + cz = (cz & 0x100) | data; // C=- Z=* + break; + + case 0x03: // INC BC + case 0x13: // INC DE + case 0x23: // INC HL + case 0x33: // INC SP + data = (data + 1) & 0xFFFF; + break; + + case 0x04: // INC B + case 0x0C: // INC C + case 0x14: // INC D + case 0x1C: // INC E + case 0x24: // INC H + case 0x2C: // INC L + case 0x34: // INC (HL) + case 0x3C: // INC A + ph = data | 0x100; // N=0 H=* + data = (data + 1) & 0xFF; + cz = (cz & 0x100) | data; // C=- Z=* + break; + + case 0xD9: // RETI + // TODO: EI + case 0xC0: // RET NZ + case 0xC8: // RET Z + case 0xD0: // RET NC + case 0xD8: // RET C + time += 12; + case 0xC9: {// RET + data = pages[sp >> pageShift] + sp; + pc = (mem[data + 1] & 0xFF) << 8 | (mem[data] & 0xFF); + sp = (sp + 2) & 0xFFFF; + continue; + } + + case 0xC1: // POP BC + case 0xD1: // POP DE + case 0xE1: // POP HL + case 0xF1: {// POP AF + data = pages[sp >> pageShift] + sp; + data = (mem[data + 1] & 0xFF) << 8 | (mem[data] & 0xFF); + sp = (sp + 2) & 0xFFFF; + break; + } + + case 0xC4: // CALL NZ,nn + case 0x20: // JR NZ,r + case 0xC2: // JP NZ,nn + if (((byte) cz) != 0) + break; + continue; + + case 0xCC: // CALL Z,nn + case 0x28: // JR Z,r + case 0xCA: // JP Z,nn + if (((byte) cz) == 0) + break; + continue; + + case 0xD4: // CALL NC,nn + case 0x30: // JR NC,r + case 0xD2: // JP NC,nn + if ((cz & 0x100) == 0) + break; + continue; + + case 0xDC: // CALL C,nn + case 0x38: // JR C,r + case 0xDA: // JP C,nn + if ((cz & 0x100) != 0) + break; + continue; + + case 0xFF: // RST $38 + case 0xC7: // RST $00 + case 0xCF: // RST $08 + case 0xD7: // RST $10 + case 0xDF: // RST $18 + case 0xE7: // RST $20 + case 0xEF: // RST $28 + case 0xF7: // RST $30 + data = (opcode & 0x38) + rstBase; + break; + + } + + // Destination + switch (opcode) { + case 0xC2: // JP NZ,nn + case 0xCA: // JP Z,nn + case 0xD2: // JP NC,nn + case 0xDA: // JP C,nn + time += 4; + case 0xC3: // JP nn + pc = data; + continue; + + case 0x20: // JR NZ,r + case 0x28: // JR Z,r + case 0x30: // JR NC,r + case 0x38: // JR C,r + time += 4; + case 0x18: // JR r + pc = (pc + (byte) data) & 0xFFFF; + continue; + + case 0xC4: // CALL NZ,nn + case 0xCC: // CALL Z,nn + case 0xD4: // CALL NC,nn + case 0xDC: // CALL C,nn + time += 12; + case 0xC7: // RST $00 + case 0xCF: // RST $08 + case 0xD7: // RST $10 + case 0xDF: // RST $18 + case 0xE7: // RST $20 + case 0xEF: // RST $28 + case 0xF7: // RST $30 + case 0xFF: // RST $38 + case 0xCD: {// CALL nn + int t = pc; + pc = data; + data = t; + } + case 0xC5: // PUSH BC + case 0xD5: // PUSH DE + case 0xE5: // PUSH HL + case 0xF5: {// PUSH AF + sp = (sp - 2) & 0xFFFF; + int offset = pages[sp >> pageShift] + sp; + mem[offset + 1] = (byte) (data >> 8); + mem[offset] = (byte) data; + continue; + } + + case 0xF1: {// POP AF + cz = (data << 4 & 0x100) | ((data >> 7 & 1) ^ 1); + ph = (~data << 2 & 0x100) | (data >> 1 & 0x10); + a = data >> 8; + continue; + } + + case 0xF0: // LDH A,(n) + case 0xF2: // LDH A,(C) + data += 0xFF00; + case 0xFA: // LD A,(nn) + case 0x0A: // LD A,(BC) + case 0x1A: // LD A,(DE) + case 0x7E: // LD A,(HL) + case 0x2A: // LD A,(HL+) + case 0x3A: // LD A,(HL-) + a = cpuRead(data); + continue; + + case 0xE0: // LDH (n),A + case 0xE2: // LDH (C),A + data += 0xFF00; + case 0xEA: // LD (nn),A + case 0x02: // LD (BC),A + case 0x12: // LD (DE),A + case 0x22: // LD (HL+),A + case 0x32: // LD (HL-),A + cpuWrite(data, a); + continue; + + case 0x08: // LD (nn),SP + cpuWrite(data, sp & 0xFF); + cpuWrite((data + 1) & 0xFFFF, sp >> 8); + continue; + + case 0x34: // INC (HL) + case 0x35: // DEC (HL) + case 0x36: // LD (HL),n + case 0x70: // LD (HL),B + case 0x71: // LD (HL),C + case 0x72: // LD (HL),D + case 0x73: // LD (HL),E + case 0x74: // LD (HL),H + case 0x75: // LD (HL),L + case 0x77: // LD (HL),A + cpuWrite(hl, data); + continue; + + case 0x01: // LD BC,nn + case 0x03: // INC BC + case 0x0B: // DEC BC + case 0xC1: // POP BC + bc = data; + continue; + + case 0x11: // LD DE,nn + case 0x13: // INC DE + case 0x1B: // DEC DE + case 0xD1: // POP DE + de = data; + continue; + + case 0xF8: // LD HL,SPs + case 0x21: // LD HL,nn + case 0x23: // INC HL + case 0x2B: // DEC HL + case 0xE1: // POP HL + hl = data; + continue; + + case 0xE8: // ADD SP,s + case 0x31: // LD SP,nn + case 0x33: // INC SP + case 0x3B: // DEC SP + sp = data; + continue; + + case 0x3C: // INC A + case 0x3D: // DEC A + case 0x3E: // LD A,n + case 0x78: // LD A,B + case 0x79: // LD A,C + case 0x7A: // LD A,D + case 0x7B: // LD A,E + case 0x7C: // LD A,H + case 0x7D: // LD A,L + a = data; + continue; + + case 0x04: // INC B + case 0x05: // DEC B + case 0x06: // LD B,n + case 0x41: // LD B,C + case 0x42: // LD B,D + case 0x43: // LD B,E + case 0x44: // LD B,H + case 0x45: // LD B,L + case 0x46: // LD B,(HL) + case 0x47: // LD B,A + bc = (data << 8) | (bc & 0xFF); + continue; + + case 0x14: // INC D + case 0x15: // DEC D + case 0x16: // LD D,n + case 0x50: // LD D,B + case 0x51: // LD D,C + case 0x53: // LD D,E + case 0x54: // LD D,H + case 0x55: // LD D,L + case 0x56: // LD D,(HL) + case 0x57: // LD D,A + de = (data << 8) | (de & 0xFF); + continue; + + case 0x24: // INC H + case 0x25: // DEC H + case 0x26: // LD H,n + case 0x60: // LD H,B + case 0x61: // LD H,C + case 0x62: // LD H,D + case 0x63: // LD H,E + case 0x65: // LD H,L + case 0x66: // LD H,(HL) + case 0x67: // LD H,A + hl = (data << 8) | (hl & 0xFF); + continue; + + case 0x0C: // INC C + case 0x0D: // DEC C + case 0x0E: // LD C,n + case 0x48: // LD C,B + case 0x4A: // LD C,D + case 0x4B: // LD C,E + case 0x4C: // LD C,H + case 0x4D: // LD C,L + case 0x4E: // LD C,(HL) + case 0x4F: // LD C,A + bc = (bc & 0xFF00) | data; + continue; + + case 0x1C: // INC E + case 0x1D: // DEC E + case 0x1E: // LD E,n + case 0x58: // LD E,B + case 0x59: // LD E,C + case 0x5A: // LD E,D + case 0x5C: // LD E,H + case 0x5D: // LD E,L + case 0x5E: // LD E,(HL) + case 0x5F: // LD E,A + de = (de & 0xFF00) | data; + continue; + + case 0x2C: // INC L + case 0x2D: // DEC L + case 0x2E: // LD L,n + case 0x68: // LD L,B + case 0x69: // LD L,C + case 0x6A: // LD L,D + case 0x6B: // LD L,E + case 0x6C: // LD L,H + case 0x6E: // LD L,(HL) + case 0x6F: // LD L,A + hl = (hl & 0xFF00) | data; + continue; + } + } + + this.a = a; + this.bc = bc; + this.de = de; + this.hl = hl; + this.sp = sp; + this.pc = pc; + this.ph = ph; + this.cz = cz; + this.time = time; + } + + static final int[] instrTimes = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 4, 12, 8, 8, 4, 4, 8, 4, 20, 8, 8, 8, 4, 4, 8, 4,// 0 + 4, 12, 8, 8, 4, 4, 8, 4, 12, 8, 8, 8, 4, 4, 8, 4,// 1 + 8, 12, 8, 8, 4, 4, 8, 4, 8, 8, 8, 8, 4, 4, 8, 4,// 2 + 8, 12, 8, 8, 12, 12, 12, 4, 8, 8, 8, 8, 4, 4, 8, 4,// 3 + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// 4 + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// 5 + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// 6 + 8, 8, 8, 8, 8, 8, 0, 8, 4, 4, 4, 4, 4, 4, 8, 4,// 7 + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// 8 + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// 9 + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// A + 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4,// B + 8, 12, 12, 16, 12, 16, 8, 16, 8, 16, 12, 0, 12, 24, 8, 16,// C + 8, 12, 12, 0, 12, 16, 8, 16, 8, 4, 12, 0, 12, 0, 8, 16,// D + 12, 12, 8, 0, 0, 16, 8, 16, 16, 4, 16, 0, 0, 0, 8, 16,// E + 12, 12, 8, 4, 0, 16, 8, 16, 12, 8, 16, 4, 0, 0, 8, 16,// F + }; + + static final int[] cbTimes = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// 0 + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// 1 + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// 2 + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// 3 + 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,// 4 + 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,// 5 + 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,// 6 + 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,// 7 + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// 8 + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// 9 + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// A + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// B + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// C + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// D + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// E + 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8,// F + }; +} diff --git a/src/uk/me/fantastic/retro/music/gme/GbsEmu.java b/src/uk/me/fantastic/retro/music/gme/GbsEmu.java new file mode 100755 index 0000000..41c1920 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/GbsEmu.java @@ -0,0 +1,192 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo Game Boy GBS music file emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +final class GbsEmu extends GbCpu { + // header offsets + static final int trackCountOff = 0x04; + static final int loadAddrOff = 0x06; + static final int initAddrOff = 0x08; + static final int playAddrOff = 0x0A; + static final int stackPtrOff = 0x0C; + static final int timerModuloOff = 0x0E; + static final int timerModeOff = 0x0F; + + // memory addresses + static final int idleAddr = 0xF00D; + static final int ramAddr = 0xA000; + static final int hiPage = 0xFF00 - ramAddr; + + static final int ramSize = 0x4000 + 0x2000; + static final int bankSize = 0x4000; + + final MemPager rom = new MemPager(bankSize, ramSize); + final byte[] header = new byte[0x70]; + byte[] ram; + + int endTime; + int playPeriod; + int nextPlay; + + GbApu apu = new GbApu(); + + protected int loadFile_(byte in[]) { + if (!isHeader(in, "GBS\u0001")) + error("Not a GBS file"); + + rstBase = getLE16(in, loadAddrOff); + ram = rom.load(in, header, rstBase, 0xFF); + + setClockRate(4194304); + apu.setOutput(buf.center(), buf.left(), buf.right()); + + return header[trackCountOff] & 0xFF; + } + + final void setBank(int n) { + int addr = rom.maskAddr(n * bankSize); + if (addr == 0 && rom.size() > bankSize) + n = 1; + mapMemory(bankSize, bankSize, rom.mapAddr(addr)); + } + + static final byte[] rates = {10, 4, 6, 8}; + + void updateTimer() { + playPeriod = 70224; // 59.73 Hz + if ((header[timerModeOff] & 0x04) != 0) { + int shift = rates[ram[hiPage + 7] & 3] - (header[timerModeOff] >> 7 & 1); + playPeriod = (256 - (ram[hiPage + 6] & 0xFF)) << shift; + } + } + + static final int[] sound_data = { + 0x80, 0xBF, 0x00, 0x00, 0xBF, // square 1 + 0x00, 0x3F, 0x00, 0x00, 0xBF, // square 2 + 0x7F, 0xFF, 0x9F, 0x00, 0xBF, // wave + 0x00, 0xFF, 0x00, 0x00, 0xBF, // noise + 0x77, 0xFF, 0x80, // vin/volume, status, power mode + 0, 0, 0, 0, 0, 0, 0, 0, 0, // unused + 0xAC, 0xDD, 0xDA, 0x48, 0x36, 0x02, 0xCF, 0x16, // waveform data + 0x2C, 0x04, 0xE5, 0x2C, 0xAC, 0xDD, 0xDA, 0x48 + }; + + void cpuCall(int addr) { + assert sp == getLE16(header, stackPtrOff); + pc = addr; + cpuWrite(--sp, idleAddr >> 8); + cpuWrite(--sp, idleAddr & 0xFF); + } + + public void startTrack(int track) { + super.startTrack(track); + + apu.reset(); + apu.write(0, 0xFF26, 0x80); // power on + for (int i = 0; i < sound_data.length; i++) + apu.write(0, i + apu.startAddr, sound_data[i]); + + reset(ram, rom.unmapped()); + mapMemory(ramAddr, 0x10000 - ramAddr, 0); + mapMemory(0, bankSize, rom.mapAddr(0)); + setBank(1); + + java.util.Arrays.fill(ram, 0, 0x4000, (byte) 0); + java.util.Arrays.fill(ram, 0x4000, 0x5F80, (byte) 0xFF); + java.util.Arrays.fill(ram, 0x5F80, ramSize, (byte) 0); + + ram[hiPage] = 0; // joypad reads back as 0 + ram[mapAddr(idleAddr)] = (byte) 0xED; // illegal instruction + + ram[hiPage + 6] = header[timerModuloOff]; + ram[hiPage + 7] = header[timerModeOff]; + updateTimer(); + nextPlay = playPeriod; + + a = track; + pc = idleAddr; + sp = getLE16(header, stackPtrOff); + cpuCall(getLE16(header, initAddrOff)); + } + + protected int runClocks(int clockCount) { + endTime = clockCount; + time = -endTime; + + while (true) { + runCpu(); + if (time >= 0) + break; + + if (pc != idleAddr) { + // TODO: PC overflow handling + pc = (pc + 1) & 0xFFFF; + logError(); + return endTime; + } + + // Next play call + int next = nextPlay - endTime; + if (time < next) { + time = 0; + if (next > 0) + break; + time = next; + } + + nextPlay += playPeriod; + cpuCall(getLE16(header, playAddrOff)); + } + + // End time frame + endTime += time; + nextPlay -= endTime; + if (nextPlay < 0) // could go negative if routine is taking too long to return + nextPlay = 0; + apu.endFrame(endTime); + + return endTime; + } + + protected int cpuRead(int addr) { + if (debug) assert 0 <= addr && addr < 0x10000; + + if (apu.startAddr <= addr && addr <= apu.endAddr) + return apu.read(time + endTime, addr); + + return ram[mapAddr(addr)] & 0xFF; + } + + protected void cpuWrite(int addr, int data) { + if (debug) assert 0 <= data && data < 0x100; + if (debug) assert 0 <= addr && addr < 0x10000; + + int offset = addr - ramAddr; + if (offset >= 0) { + ram[offset] = (byte) data; + if (addr < 0xFF80 && addr >= 0xFF00) { + if (apu.startAddr <= addr && addr <= apu.endAddr) { + apu.write(time + endTime, addr, data); + } else if ((addr ^ 0xFF06) < 2) { + updateTimer(); + } else if (addr == 0xFF00) { + ram[offset] = 0; // keep joypad return value 0 + } else { + ram[offset] = (byte) 0xFF; + } + } + } else if ((addr ^ 0x2000) <= 0x2000 - 1) { + setBank(data); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/Main.java b/src/uk/me/fantastic/retro/music/gme/Main.java new file mode 100644 index 0000000..2bd9192 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/Main.java @@ -0,0 +1,71 @@ +package uk.me.fantastic.retro.music.gme; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +/** + * Created by rich on 13/04/2017. + */ +public class Main { + + static PlayerWithUpdate player; + + public static void main(String[] args) { + try { + + player = new PlayerWithUpdate(); + + + // Optionally start playing file immediately + String url = "http://blargg.8bitalley.com/parodius/gme_java/vgm/sonic.vgz"; + // url = "http://iterations.org/files/music/chiptunes/archives/m/mega-man-2-nes-%5BNSF-ID2018%5D.nsf"; + // url = "http://blargg.8bitalley.com/parodius/gme_java/vgm/Phantasy_Star.zip"; + String path = ""; + //"Phantasy_Star/Intro.vgz"; + // player.add(url, path, 1, "", -1, true); + + player.add(url); + // player.playIndex(0); + + // byte[] data = player.readFile(url, path); + //InputStream in = DataReader.openHttp(url); + File file = new File("/home/richard/IdeaProjects/retrogame/android/assets/test.nsf"); + // FileHandle fileHandle = Gdx.files.internal("sonic.vgz"); + InputStream in = new FileInputStream(file); + + //in = DataReader.openGZIP(in); + byte[] data = DataReader.loadData(in); + + String name = url.toUpperCase(); + + + MusicEmu emu = player.createEmu(file.getName().toUpperCase()); + if (emu == null) + return; // TODO: throw exception? + int actualSampleRate = emu.setSampleRate(player.sampleRate); + emu.loadFile(data); + + // now that new emulator is ready, replace old one + player.setEmu(emu, actualSampleRate); + player.loadedUrl = url; + player.loadedPath = path; + + player.pause(); + if (player.line != null) + player.line.flush(); + emu.startTrack(0); + // emu.setFade( time, 6 ); + player.play(); +// +// VGMPlayer player = new VGMPlayer(); +// player.loadFile(url, path); + // player.startTrack(0,-1 ); +// player.play(); + + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/MemPager.java b/src/uk/me/fantastic/retro/music/gme/MemPager.java new file mode 100755 index 0000000..56c583f --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/MemPager.java @@ -0,0 +1,64 @@ +package uk.me.fantastic.retro.music.gme;// Manages memory paging used by CPU emulators +// http://www.slack.net/~ant/ + +final class MemPager { + public MemPager(int pageSize, int ramSize) { + this.pageSize = pageSize; + this.romOffset = ramSize + pageSize; + } + + // Loads data and returns memory array + public byte[] load(byte in[], byte[] header, int addr, int fill) { + // allocate + int romLength = in.length - header.length; + int romSize = (romLength + addr + pageSize - 1) / pageSize * pageSize; + data = new byte[romOffset + romSize + padding]; + + // copy data + java.util.Arrays.fill(data, 0, romOffset + addr, (byte) fill); + java.util.Arrays.fill(data, data.length - pageSize - padding, data.length, (byte) fill); + System.arraycopy(in, header.length, data, romOffset + addr, romLength); + + // addrMask + int shift = 0; + int max_addr = romSize - 1; + while ((max_addr >> shift) != 0) + shift++; + addrMask = (1 << shift) - 1; + + // copy header + System.arraycopy(in, 0, header, 0, header.length); + return data; + } + + // Size of ROM data, a multiple of pageSize + public int size() { + return data.length - padding - romOffset; + } + + // Page of unmapped fill value + public int unmapped() { + return romOffset - pageSize; + } + + // Masks address to nearest power of two greater than size() + public int maskAddr(int addr) { + return addr & addrMask; + } + + // Page starting at addr. Returns unmapped() if outside data. + public int mapAddr(int addr) { + int offset = maskAddr(addr); + if (offset < 0 || size() - pageSize < offset) + offset = -pageSize; + return offset + romOffset; + } + +// private + + static final int padding = 8; // extra at end for CPU emulators that read past end + byte[] data; + int pageSize; + int romOffset; + int addrMask; +} diff --git a/src/uk/me/fantastic/retro/music/gme/MusicEmu.java b/src/uk/me/fantastic/retro/music/gme/MusicEmu.java new file mode 100755 index 0000000..da51962 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/MusicEmu.java @@ -0,0 +1,201 @@ +package uk.me.fantastic.retro.music.gme;// Music emulator interface +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +class MusicEmu { + // enables performance-intensive assertions + protected static final boolean debug = false; + + public MusicEmu() { + trackCount_ = 0; + trackEnded_ = true; + currentTrack_ = 0; + } + + // Requests change of sample rate and returns sample rate used, which might be different + public final int setSampleRate(int rate) { + return sampleRate_ = setSampleRate_(rate); + } + + public final int sampleRate() { + return sampleRate_; + } + + // Loads music file into emulator. Might keep reference to data. + public void loadFile(byte[] data) { + trackEnded_ = true; + currentTrack_ = 0; + currentTime_ = 0; + trackCount_ = loadFile_(data); + } + + // Number of tracks + public final int trackCount() { + return trackCount_; + } + + // Starts track, where 0 is first track + public void startTrack(int track) { + if (track < 0 || track > trackCount_) + error("Invalid track"); + + trackEnded_ = false; + currentTrack_ = track; + currentTime_ = 0; + fadeStart = 0x40000000; // far into the future + fadeStep = 1; + } + + // Currently started track + public final int currentTrack() { + return currentTrack_; + } + + // Generates at most count samples into out and returns + // number of samples written. If track has ended, fills + // buffer with silence. + public final int play(byte[] out, int count) { + if (!trackEnded_) { + count = play_(out, count); + if ((currentTime_ += count >> 1) > fadeStart) + applyFade(out, count); + } else { + System.out.println("track ended"); + java.util.Arrays.fill(out, 0, count * 2, (byte) 0); + } + return count; + } + + // Sets fade start and length, in seconds. Must be set after call to startTrack(). + public final void setFade(int start, int length) { + fadeStart = sampleRate_ * start; + fadeStep = sampleRate_ * length / (fadeBlockSize * fadeShift); + if (fadeStep < 1) + fadeStep = 1; + } + + // Number of seconds current track has been played + public final int currentTime() { + return currentTime_ / sampleRate_; + } + + // True if track has reached end or setFade()'s fade has finished + public final boolean trackEnded() { + return trackEnded_; + } + +// protected + + // must be defined in derived class + protected int setSampleRate_(int rate) { + return rate; + } + + protected int loadFile_(byte[] in) { + return 0; + } + + protected int play_(byte[] out, int count) { + return 0; + } + + // Reports error string as exception + protected void error(String str) { + throw new Error(str); + } + + // Sets end of track flag and stops emulating file + protected void setTrackEnded() { + trackEnded_ = true; + } + + // Stops track and notes emulation error + protected void logError() { + if (!trackEnded_) { + trackEnded_ = true; + System.out.println("emulation error"); + } + } + + // Reads 16 bit little endian int starting at in [pos] + protected static int getLE16(byte[] in, int pos) { + return (in[pos] & 0xFF) | + (in[pos + 1] & 0xFF) << 8; + } + + // Reads 32 bit little endian int starting at in [pos] + protected static int getLE32(byte[] in, int pos) { + return (in[pos] & 0xFF) | + (in[pos + 1] & 0xFF) << 8 | + (in[pos + 2] & 0xFF) << 16 | + (in[pos + 3] & 0xFF) << 24; + } + + // True if first bytes of file match expected string + protected static boolean isHeader(byte[] header, String expected) { + for (int i = expected.length(); --i >= 0; ) + if ((byte) expected.charAt(i) != header[i]) + return false; + return true; + } + +// private + + int sampleRate_; + int trackCount_; + int currentTrack_; + int currentTime_; + int fadeStart; + int fadeStep; + boolean trackEnded_; + + static final int fadeBlockSize = 512; + static final int fadeShift = 8; // fade ends with gain at 1.0 / (1 << fadeShift) + + // unit / pow( 2.0, (double) x / step ) + static int int_log(int x, int step, int unit) { + int shift = x / step; + int fraction = (x - shift * step) * unit / step; + return ((unit - fraction) + (fraction >> 1)) >> shift; + } + + static final int gainShift = 14; + static final int gainUnit = 1 << gainShift; + + // Scales count big-endian 16-bit samples from io [pos*2] by gain/gainUnit + static void scaleSamples(byte[] io, int pos, int count, int gain) { + pos <<= 1; + count = (count << 1) + pos; + do { + int s; + io[pos + 1] = (byte) (s = ((io[pos] << 8 | (io[pos + 1] & 0xFF)) * gain) >> gainShift); + io[pos] = (byte) (s >> 8); + } + while ((pos += 2) < count); + } + + private void applyFade(byte[] io, int count) { + // Apply successively smaller gains based on time since fade start + for (int i = 0; i < count; i += fadeBlockSize) { + // logarithmic progression + int gain = int_log((currentTime_ + i - fadeStart) / fadeBlockSize, fadeStep, gainUnit); + if (gain < (gainUnit >> fadeShift)) + setTrackEnded(); + + int n = count - i; + if (n > fadeBlockSize) + n = fadeBlockSize; + scaleSamples(io, i, n, gain); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/NesApu.java b/src/uk/me/fantastic/retro/music/gme/NesApu.java new file mode 100755 index 0000000..18cced8 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/NesApu.java @@ -0,0 +1,658 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo NES sound chip emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2010 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +class NesOsc { + static final int squareUnit = (int) (0.125 / 15 * 65535); + static final int triangleUnit = (int) (0.150 / 15 * 65535); + static final int noiseUnit = (int) (0.095 / 15 * 65535); + static final int dmcUnit = (int) (0.450 / 127 * 65535); + + final int[] regs = new int[4]; + final boolean[] regWritten = new boolean[4]; + int lengthCounter;// length counter (0 if unused by oscillator) + int delay; // delay until next (potential) transition + int lastAmp; // last amplitude oscillator was outputting + + void clockLength(int halt_mask) { + if (lengthCounter != 0 && (regs[0] & halt_mask) == 0) + lengthCounter--; + } + + int period() { + return (regs[3] & 7) * 0x100 + (regs[2] & 0xFF); + } + + void reset() { + delay = 0; + lastAmp = 0; + } + + int updateAmp(int amp) { + int delta = amp - lastAmp; + lastAmp = amp; + return delta; + } +} + +class NesEnvelope extends NesOsc { + int envVolume; + int envDelay; + + void clockEnvelope() { + int period = regs[0] & 15; + if (regWritten[3]) { + regWritten[3] = false; + envDelay = period; + envVolume = 15; + } else if (--envDelay < 0) { + envDelay = period; + if ((envVolume | (regs[0] & 0x20)) != 0) + envVolume = (envVolume - 1) & 15; + } + } + + int volume() { + if (lengthCounter == 0) + return 0; + + if ((regs[0] & 0x10) != 0) + return regs[0] & 0x0F; + + return envVolume; + } + + void reset() { + envVolume = 0; + envDelay = 0; + super.reset(); + } +} + +final class NesSquare extends NesEnvelope { + static final int negateMask = 0x08; + static final int shiftMask = 0x07; + static final int phaseRange = 8; + int phase; + int sweepDelay; + + void reset() { + sweepDelay = 0; + super.reset(); + } + + void clockSweep(int negative_adjust) { + int sweep = regs[1]; + + if (--sweepDelay < 0) { + regWritten[1] = true; + + int period = this.period(); + int shift = sweep & shiftMask; + if (shift != 0 && (sweep & 0x80) != 0 && period >= 8) { + int offset = period >> shift; + + if ((sweep & negateMask) != 0) + offset = negative_adjust - offset; + + if (period + offset < 0x800) { + period += offset; + // rewrite period + regs[2] = period & 0xFF; + regs[3] = (regs[3] & ~7) | ((period >> 8) & 7); + } + } + } + + if (regWritten[1]) { + regWritten[1] = false; + sweepDelay = (sweep >> 4) & 7; + } + } + + void run(BlipBuffer output, int time, int endTime) { + final int period = this.period(); + final int timer_period = (period + 1) * 2; + + int offset = period >> (regs[1] & shiftMask); + if ((regs[1] & negateMask) != 0) + offset = 0; + + final int volume = this.volume(); + if (volume == 0 || period < 8 || (period + offset) > 0x7FF) { + if (lastAmp != 0) { + output.addDelta(time, lastAmp * -squareUnit); + lastAmp = 0; + } + + time += delay; + + int remain = endTime - time; + if (remain > 0) { + int count = (remain + timer_period - 1) / timer_period; + phase = (phase + count) & (phaseRange - 1); + time += count * timer_period; + } + } else { + // handle duty select + int duty_select = (regs[0] >> 6) & 3; + int duty = 1 << duty_select; // 1, 2, 4, 2 + int amp = 0; + if (duty_select == 3) { + duty = 2; // negated 25% + amp = volume; + } + if (phase < duty) + amp ^= volume; + + { + int delta = updateAmp(amp); + if (delta != 0) + output.addDelta(time, delta * squareUnit); + } + + time += delay; + if (time < endTime) { + int phase = this.phase; // cache + int delta = (amp * 2 - volume) * squareUnit; + + do { + if ((phase = (phase + 1) & (phaseRange - 1)) == 0 || + phase == duty) + output.addDelta(time, delta = -delta); + } + while ((time += timer_period) < endTime); + + this.phase = phase; + lastAmp = (delta < 0 ? 0 : volume); + } + } + + delay = time - endTime; + } +} + +final class NesTriangle extends NesOsc { + static final int phaseRange = 16; + int phase; + int linearCounter; + + void reset() { + linearCounter = 0; + phase = phaseRange; + super.reset(); + } + + void clockLinearCounter() { + if (regWritten[3]) + linearCounter = regs[0] & 0x7F; + else if (linearCounter != 0) + linearCounter--; + + if ((regs[0] & 0x80) == 0) + regWritten[3] = false; + } + + int calc_amp() { + int amp = phaseRange - phase; + if (amp < 0) + amp = phase - (phaseRange + 1); + return amp; + } + + void run(BlipBuffer output, int time, int endTime) { + final int timer_period = period() + 1; + + // to do: track phase when period < 3 + // to do: Output 7.5 on dac when period < 2? More accurate, but results in more clicks. + + int delta = updateAmp(calc_amp()); + if (delta != 0) + output.addDelta(time, delta * triangleUnit); + + time += delay; + if (lengthCounter == 0 || linearCounter == 0 || timer_period < 3) { + time = endTime; + } else if (time < endTime) { + int volume = triangleUnit; + if (phase > phaseRange) { + phase -= phaseRange; + volume = -volume; + } + + do { + if (--phase != 0) { + output.addDelta(time, volume); + } else { + phase = phaseRange; + volume = -volume; + } + } + while ((time += timer_period) < endTime); + + if (volume < 0) + phase += phaseRange; + lastAmp = calc_amp(); + } + delay = time - endTime; + } +} + +final class NesNoise extends NesEnvelope { + int lfsr; + + static final int[] noisePeriods = { + 0x004, 0x008, 0x010, 0x020, 0x040, 0x060, 0x080, 0x0A0, + 0x0CA, 0x0FE, 0x17C, 0x1FC, 0x2FA, 0x3F8, 0x7F2, 0xFE4 + }; + + void run(BlipBuffer output, int time, int endTime) { + final int volume = this.volume(); + int amp = (lfsr & 1) != 0 ? volume : 0; + { + int delta = updateAmp(amp); + if (delta != 0) + output.addDelta(time, delta * noiseUnit); + } + + time += delay; + if (time < endTime) { + final int period = noisePeriods[regs[2] & 15]; + final int tap = (regs[2] & 0x80) != 0 ? 8 : 13; + + if (volume == 0) { + // round to next multiple of period + time += (endTime - time + period - 1) / period * period; + + // approximate noise cycling while muted, by shuffling up noise register + int feedback = (lfsr << tap) ^ (lfsr << 14); + lfsr = (feedback & 0x4000) | (lfsr >> 1); + } else { + int lfsr = this.lfsr; // cache + int delta = (amp * 2 - volume) * noiseUnit; + + do { + if (((lfsr + 1) & 2) != 0) + output.addDelta(time, delta = -delta); + + lfsr = ((lfsr << tap) ^ (lfsr << 14)) & 0x4000 | (lfsr >> 1); + } + while ((time += period) < endTime); + + this.lfsr = lfsr; + lastAmp = (delta < 0 ? 0 : volume); + } + } + + delay = time - endTime; + } + + void reset() { + lfsr = 1 << 14; + super.reset(); + } +} + +final class NesDmc extends NesOsc { + static final int loop_flag = 0x40; + + int address; // address of next byte to read + int period; + //int length_counter; // bytes remaining to play (already defined in NesOsc) + int buf; + int bits_remain; + int bits; + boolean buf_full; + boolean silence; + int dac; + int irqEnabled; + int irqFlag; + boolean palMode; + + // in DMC since it needs to clear it + int oscEnables; + NesCpu cpu; + + void reset() { + address = 0; + dac = 0; + buf = 0; + bits_remain = 1; + bits = 0; + buf_full = false; + silence = true; + + irqFlag = 0; + irqEnabled = 0; + + super.reset(); + period = 0x1AC; + } + + static final int[] dmc_period_table = { + 428, 380, 340, 320, 286, 254, 226, 214, // NTSC + 190, 160, 142, 128, 106, 84, 72, 54, + + 398, 354, 316, 298, 276, 236, 210, 198, // PAL + 176, 148, 132, 118, 98, 78, 66, 50 + }; + + void reload_sample() { + address = 0x4000 + regs[2] * 0x40; + lengthCounter = regs[3] * 0x10 + 1; + } + + static final int[] dac_table = + { + 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 15, 16, 17, 18, 19, 20, 20, 21, 22, 23, 24, 24, 25, 26, 27, + 27, 28, 29, 30, 31, 31, 32, 33, 33, 34, 35, 36, 36, 37, 38, 38, + 39, 40, 41, 41, 42, 43, 43, 44, 45, 45, 46, 47, 47, 48, 48, 49, + 50, 50, 51, 52, 52, 53, 53, 54, 55, 55, 56, 56, 57, 58, 58, 59, + 59, 60, 60, 61, 61, 62, 63, 63, 64, 64, 65, 65, 66, 66, 67, 67, + 68, 68, 69, 70, 70, 71, 71, 72, 72, 73, 73, 74, 74, 75, 75, 75, + 76, 76, 77, 77, 78, 78, 79, 79, 80, 80, 81, 81, 82, 82, 82, 83, + }; + + void write_register(int addr, int data) { + if (addr == 0) { + period = dmc_period_table[(data & 15) + (palMode ? 16 : 0)]; + + irqEnabled = 1; + if ((data & 0xC0) != 0x80) { + irqEnabled = 0; + irqFlag = 0; + } + } else if (addr == 1) { + // adjust lastAmp so that "pop" amplitude will be properly non-linear + // with respect to change in dac + data &= 0x7F; + lastAmp = data - dac_table[data] + dac_table[dac]; + dac = data; + } + } + + void start() { + reload_sample(); + fill_buffer(); + } + + void fill_buffer() { + if (!buf_full && lengthCounter != 0) { + // Read byte via CPU + buf = cpu.cpuRead(0x8000 + address); + address = (address + 1) & 0x7FFF; + buf_full = true; + + if (--lengthCounter == 0) // Reached end of sample + { + if ((regs[0] & loop_flag) != 0) { + reload_sample(); + } else { + oscEnables &= ~0x10; + irqFlag = irqEnabled; + } + } + } + } + + void run(BlipBuffer output, int time, int endTime) { + int delta = updateAmp(dac); + if (delta != 0) + output.addDelta(time, delta * dmcUnit); + + time += delay; + if (time < endTime) { + if (silence && !buf_full) { + int count = (endTime - time + period - 1) / period; + bits_remain = (bits_remain - 1 + 8 - (count % 8)) % 8 + 1; + time += count * period; + } else { + do { + if (!silence) { + int step; + int newDac = dac + (step = (bits << 2 & 4) - 2); + // if ( newDac >= 0 && newDac <= 0x7F ) + if ((byte) newDac >= 0) { + dac = newDac; + output.addDelta(time, step * dmcUnit); + } + bits >>= 1; + } + + if (--bits_remain == 0) { + bits_remain = 8; + silence = true; + if (buf_full) { + buf_full = false; + silence = false; + bits = buf; + fill_buffer(); + } + } + } + while ((time += period) < endTime); + + lastAmp = dac; + } + } + delay = time - endTime; + } +} + +public final class NesApu { + public NesApu() { + oscs[0] = square1; + oscs[1] = square2; + oscs[2] = triangle; + oscs[3] = noise; + oscs[4] = dmc; + } + + public void setOutput(BlipBuffer b) { + output = b; + } + + // Resets oscillators and internal state + public void reset(NesCpu cpu, boolean palMode) { + dmc.cpu = cpu; + dmc.palMode = palMode; + + framePeriod = palMode ? 8314 : 7458; + frameTime = framePeriod; + lastTime = 0; + irqFlag = 0; + dmc.oscEnables = 0; + + square1.reset(); + square2.reset(); + triangle.reset(); + noise.reset(); + dmc.reset(); + + write(0, 0x4017, 0x00); + write(0, 0x4015, 0x00); + + for (int addr = 0x4000; addr <= 0x4013; addr++) + write(0, addr, (addr & 3) != 0 ? 0x00 : 0x10); + + dmc.lastAmp = dmc.dac = 0; // prevents click + } + + // Writes data to address at specified time + public static final int startAddr = 0x4000; + public static final int endAddr = 0x4017; + + public void write(int time, int addr, int data) { + assert 0 <= data && data < 0x100; + assert startAddr <= addr && addr <= endAddr; + + runUntil(time); + + if (addr < 0x4014) { + // Write to channel + int index = (addr - startAddr) >> 2; + NesOsc osc = oscs[index]; + + int reg = addr & 3; + osc.regs[reg] = data; + osc.regWritten[reg] = true; + + if (index == 4) { + // handle DMC specially + dmc.write_register(reg, data); + } else if (reg == 3) { + // load length counter + if ((dmc.oscEnables >> index & 1) != 0) + osc.lengthCounter = length_table[data >> 3 & 0x1F]; + + // reset square phase + if (index < 2) + ((NesSquare) osc).phase = NesSquare.phaseRange - 1; + } + } else if (addr == 0x4015) { + // Channel enables + for (int i = oscCount; i-- > 0; ) + if ((data >> i & 1) == 0) + oscs[i].lengthCounter = 0; + + dmc.irqFlag = 0; + + int justEnabled = data & ~dmc.oscEnables; + dmc.oscEnables = data; + + if ((justEnabled & 0x10) != 0) + dmc.start(); + } else if (addr == 0x4017) { + // Frame mode + frameMode = data; + + if ((data & 0x40) != 0) + irqFlag = 0; + + // mode 1 + frameTime = time; + framePhase = 0; + + if ((data & 0x80) == 0) { + // mode 0 + framePhase = 1; + frameTime += framePeriod; + } + } + + } + + // Reads from status register at specified time + public int read(int time) { + runUntil(time); + + int result = (dmc.irqFlag << 7) | (irqFlag << 6); + irqFlag = 0; + + for (int i = 0; i < oscCount; i++) + if (oscs[i].lengthCounter != 0) + result |= 1 << i; + + return result; + } + + // Runs all oscillators up to specified time, ends current time frame, then + // starts a new frame at time 0 + public void endFrame(int endTime) { + if (endTime > lastTime) + runUntil(endTime); + + assert frameTime >= endTime; + frameTime -= endTime; + + assert lastTime >= endTime; + lastTime -= endTime; + } + + static final int[] length_table = { + 0x0A, 0xFE, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06, + 0xA0, 0x08, 0x3C, 0x0A, 0x0E, 0x0C, 0x1A, 0x0E, + 0x0C, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16, + 0xC0, 0x18, 0x48, 0x1A, 0x10, 0x1C, 0x20, 0x1E + }; + + static final int oscCount = 5; + final NesOsc[] oscs = new NesOsc[oscCount]; + final NesSquare square1 = new NesSquare(); + final NesSquare square2 = new NesSquare(); + final NesTriangle triangle = new NesTriangle(); + final NesNoise noise = new NesNoise(); + final NesDmc dmc = new NesDmc(); + BlipBuffer output; + int framePeriod; + int frameTime; + int framePhase; + int lastTime; + int frameMode; + int irqFlag; + + void runUntil(int endTime) { + assert endTime >= lastTime; // endTime must not be before previous time + if (endTime == lastTime) + return; + + while (true) { + // run oscillators + int time = endTime; + if (time > frameTime) + time = frameTime; + + square1.run(output, lastTime, time); + square2.run(output, lastTime, time); + triangle.run(output, lastTime, time); + noise.run(output, lastTime, time); + dmc.run(output, lastTime, time); + lastTime = time; + + if (time == endTime) + break; + + // run frame sequencer + frameTime += framePeriod; + switch (framePhase++) { + case 0: + if ((frameMode & 0xC0) == 0) + irqFlag = 1; + case 2: + // 120 Hz + square1.clockLength(0x20); + square2.clockLength(0x20); + triangle.clockLength(0x80); // different bit for halt flag on triangle + noise.clockLength(0x20); + + square1.clockSweep(-1); + square2.clockSweep(0); + break; + + case 3: + // 60 Hz + framePhase = 0; + if ((frameMode & 0x80) != 0) + frameTime += framePeriod; // frame 3 is almost twice as long in mode 1 + break; + } + + // 240 Hz + square1.clockEnvelope(); + square2.clockEnvelope(); + triangle.clockLinearCounter(); + noise.clockEnvelope(); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/NesCpu.java b/src/uk/me/fantastic/retro/music/gme/NesCpu.java new file mode 100755 index 0000000..c4df07e --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/NesCpu.java @@ -0,0 +1,948 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo NES 6502 CPU emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +public class NesCpu extends ClassicEmu { + // Resets registers and uses supplied physical memory + public final void reset(byte[] mem, int unmapped) { + this.mem = mem; + a = 0; + x = 0; + y = 0; + s = 0xFF; + pc = 0; + p = 0x04; + c = 0; + nz = 1; + + time = 0; + + for (int i = 0; i < pageCount + 1; i++) + mapPage(i, unmapped); + } + + static final int pageShift = 11; + static final int pageCount = 0x10000 >> pageShift; + public static final int pageSize = 1 << pageShift; + + // Maps address range to offset in physical memory + public final void mapMemory(int addr, int size, int offset) { + assert addr % pageSize == 0; + assert size % pageSize == 0; + int firstPage = addr / pageSize; + for (int i = size / pageSize; i-- > 0; ) + mapPage(firstPage + i, offset + i * pageSize); + } + + // Maps address to memory + public final int mapAddr(int addr) { + return pages[addr >> pageShift] + addr; + } + +// Emulation + + // Registers. NOT kept updated during runCpu() + public int a, x, y, p, s, pc; + + // Current time + public int time; + + // Memory read and write handlers + protected int cpuRead(int addr) { + return 0; + } + + protected void cpuWrite(int addr, int data) { + } + + final int pages[] = new int[pageCount + 1]; + int c, nz; + byte[] mem; + + final void mapPage(int page, int offset) { + if (debug) assert 0 <= page && page < pageCount + 1; + pages[page] = offset - page * pageSize; + } + + static final int[] instrTimes = + {// 0 1 2 3 4 5 6 7 8 9 A B C D E F + 7, 6, 2, 8, 3, 3, 5, 5, 3, 2, 2, 2, 4, 4, 6, 6,// 0 + 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,// 1 + 6, 6, 2, 8, 3, 3, 5, 5, 4, 2, 2, 2, 4, 4, 6, 6,// 2 + 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,// 3 + 6, 6, 2, 8, 3, 3, 5, 5, 3, 2, 2, 2, 3, 4, 6, 6,// 4 + 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,// 5 + 6, 6, 2, 8, 3, 3, 5, 5, 4, 2, 2, 2, 5, 4, 6, 6,// 6 + 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,// 7 + 2, 6, 2, 6, 3, 3, 3, 3, 2, 2, 2, 2, 4, 4, 4, 4,// 8 + 2, 6, 2, 6, 4, 4, 4, 4, 2, 5, 2, 5, 5, 5, 5, 5,// 9 + 2, 6, 2, 6, 3, 3, 3, 3, 2, 2, 2, 2, 4, 4, 4, 4,// A + 2, 5, 2, 5, 4, 4, 4, 4, 2, 4, 2, 4, 4, 4, 4, 4,// B + 2, 6, 2, 8, 3, 3, 5, 5, 2, 2, 2, 2, 4, 4, 6, 6,// C + 2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,// D + 2, 6, 2, 8, 3, 3, 5, 5, 2, 2, 2, 2, 4, 4, 6, 6,// E + 2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7 // F + }; // 0xF2 was 2 + + static final int[] illop_lens = { + 0x95, 0x95, 0x95, 0xD5, 0x95, 0x95, 0xD5, 0xF5 + }; + + static final int N80 = 0x80; + static final int V40 = 0x40; + static final int R20 = 0x20; + static final int B10 = 0x10; + static final int D08 = 0x08; + static final int I04 = 0x04; + static final int Z02 = 0x02; + static final int C01 = 0x01; + + // Runs until time >= 0 + public final void runCpu() { + // locals are faster, and first three are more efficient to access + final byte[] mem = this.mem; + int nz = this.nz; + int pc = this.pc; + + int time = this.time; + int a = this.a; + int x = this.x; + int y = this.y; + int sp = (this.s + 1) | 0x100; + int p = this.p; + int c = this.c; + final int pages[] = this.pages; + final int instrTimes[] = this.instrTimes; + + int addr = 0; + + loop: + while (time < 0) { + if (debug) { + assert 0 <= a && a < 0x100; + assert 0 <= x && x < 0x100; + assert 0 <= y && y < 0x100; + assert (p & ~(V40 | D08 | I04)) == 0; + assert 0 <= pc && pc < 0x10000; + assert 0x100 <= sp && sp < 0x200; + } + + int instr; + int opcode; + this.time = + (time += instrTimes[opcode = + 0xFF & mem[instr = + pages[pc >> pageShift] + pc]]); + instr++; + + // nz is used as variable for incoming data of instructions that + // will be modifying nz anyway. + + // Source + switch (opcode) { + + //////// Often used + + case 0xD0: {// BNE r + pc += 2; + if (((byte) nz) != 0) { + int old = pc; + time += (((pc += mem[instr]) ^ old) >> 8 & 1) + 1; + } + continue; + } + + case 0xF0: {// BEQ r + pc += 2; + if (((byte) nz) == 0) { + int old = pc; + time += (((pc += mem[instr]) ^ old) >> 8 & 1) + 1; + } + continue; + } + + case 0xBD: {// LDA a,X + pc += 3; + int lsb; + time += (lsb = (mem[instr] & 0xFF) + x) >> 8; + a = nz = cpuRead(((mem[instr + 1] & 0xFF) << 8) + lsb); + continue; + } + + case 0xC8: // INY + pc++; + y = (nz = y + 1) & 0xFF; + continue; + + case 0x85: // STA z + pc += 2; + mem[mem[instr] & 0xFF] = (byte) a; + continue; + + case 0xC9: // CMP #n + pc += 2; + c = ~(nz = a - (mem[instr] & 0xFF)); + nz = (byte) nz; + continue; + + case 0x20: {// JSR a + int t = pc + 2; + pc = (mem[instr + 1] & 0xFF) << 8 | (mem[instr] & 0xFF); + mem[(sp - 1) | 0x100] = (byte) (t >> 8); + mem[sp = (sp - 2) | 0x100] = (byte) t; + continue; + } + + //////// + + case 0x10: // BPL r + pc += 2; + if ((nz & 0x8080) == 0) + break; + continue; + + case 0x30: // BMI r + pc += 2; + if ((nz & 0x8080) != 0) + break; + continue; + + case 0x50: // BVC r + pc += 2; + if ((p & V40) == 0) + break; + continue; + + case 0x70: // BVS r + pc += 2; + if ((p & V40) != 0) + break; + continue; + + case 0x90: // BCC r + pc += 2; + if ((c & 0x100) == 0) + break; + continue; + + case 0xB0: // BCS r + pc += 2; + if ((c & 0x100) != 0) + break; + continue; + + case 0xBA: // TSX + pc++; + x = (nz = sp - 1) & 0xFF; + continue; + + case 0x9A: // TXS + pc++; + sp = (x + 1) | 0x100; + continue; + + case 0x18: // CLC + pc++; + c = 0; + continue; + + case 0x38: // SEC + pc++; + c = ~0; + continue; + + case 0xD8: // CLD + pc++; + p &= ~D08; + continue; + + case 0xB8: // CLV + pc++; + p &= ~V40; + continue; + + case 0x58: // CLI + pc++; + p &= ~I04; + continue; + + case 0x78: // SEI + pc++; + p |= I04; + continue; + + case 0xF8: // SED + pc++; + p |= D08; + continue; + + case 0x48: // PHA + pc++; + mem[sp = (sp - 1) | 0x100] = (byte) a; + continue; + + // SKW - Skip word + case 0x1C: + case 0x3C: + case 0x5C: + case 0x7C: + case 0xDC: + case 0xFC: + time += ((mem[instr] & 0xFF) + x) >> 8; + case 0x0C: + pc += 3; + continue; + + // SKB - Skip byte + case 0x74: + case 0x04: + case 0x14: + case 0x34: + case 0x44: + case 0x54: + case 0x64: + case 0x80: + case 0x82: + case 0x89: + case 0xC2: + case 0xD4: + case 0xE2: + case 0xF4: + pc += 2; + continue; + + // NOP + case 0xEA: + case 0x1A: + case 0x3A: + case 0x5A: + case 0x7A: + case 0xDA: + case 0xFA: + pc++; + continue; + + // Illegal + case 0xF2: + if (pc > 0xFFFF) { + // handle wrap-around (assumes caller has put 0xF2 at 0x1000-0x10FF) + pc &= 0xFFFF; + break; + } + case 0x02: + case 0x12: + case 0x22: + case 0x32: + case 0x42: + case 0x52: + case 0x62: + case 0x72: + case 0x92: + case 0xB2: + case 0xD2: + break loop; + + // Unimplemented + default: + /* + assert false; + case 0x03: case 0x07: case 0x0B: case 0x0F: + case 0x13: case 0x17: case 0x1B: case 0x1F: + case 0x23: case 0x27: case 0x2B: case 0x2F: + case 0x33: case 0x37: case 0x3B: case 0x3F: + case 0x43: case 0x47: case 0x4B: case 0x4F: + case 0x53: case 0x57: case 0x5B: case 0x5F: + case 0x63: case 0x67: case 0x6B: case 0x6F: + case 0x73: case 0x77: case 0x7B: case 0x7F: + case 0x83: case 0x87: case 0x8B: case 0x8F: + case 0x93: case 0x97: case 0x9B: case 0x9F: + case 0xA3: case 0xA7: case 0xAB: case 0xAF: + case 0xB3: case 0xB7: case 0xBB: case 0xBF: + case 0xC3: case 0xC7: case 0xCB: case 0xCF: + case 0xD3: case 0xD7: case 0xDB: case 0xDF: + case 0xE3: case 0xE7: case 0xEF: + case 0xF3: case 0xF7: case 0xFB: case 0xFF: + case 0x9C: case 0x9E: + */ + if ((opcode >> 4) == 0x0B) { + int t = mem[instr] & 0xFF; + if (opcode == 0xB3) + t = mem[t] & 0xFF; + if (opcode != 0xB7) + time += (t + y) >> 8; + } + + // skip over proper number of bytes + int len = illop_lens[opcode >> 2 & 7] >> (opcode << 1 & 6) & 3; + if (opcode == 0x9C) + len = 3; + pc += len; + continue; + + case 0x0A: // ASL + case 0x2A: // ROL + case 0x4A: // LSR + case 0x6A: // ROR + pc++; + nz = a; + break; + + case 0x09: // ORA #n + case 0x29: // AND #n + case 0x49: // EOR #n + case 0x69: // ADC #n + case 0xA0: // LDY #n + case 0xA2: // LDX #n + case 0xA9: // LDA #n + case 0xC0: // CPY #n + case 0xE0: // CPX #n + case 0xE9: // SBC #n + case 0xEB: // SBC #n (unofficial) + pc += 2; + nz = mem[instr] & 0xFF; + break; + + case 0x84: // STY z + case 0x86: // STX z + pc += 2; + addr = mem[instr] & 0xFF; + break; + + case 0x06: // ASL z + case 0x26: // ROL z + case 0x66: // ROR z + case 0xE6: // INC z + case 0xC6: // DEC z + case 0x46: // LSR z + pc += 2; + nz = mem[addr = mem[instr] & 0xFF] & 0xFF; + break; + + case 0x05: // ORA z + case 0x24: // BIT z + case 0x25: // AND z + case 0x45: // EOR z + case 0x65: // ADC z + case 0xA4: // LDY z + case 0xA5: // LDA z + case 0xA6: // LDX z + case 0xC4: // CPY z + case 0xC5: // CMP z + case 0xE4: // CPX z + case 0xE5: // SBC z + pc += 2; + nz = mem[mem[instr] & 0xFF] & 0xFF; + break; + + case 0x11: // ORA (z),Y + case 0x31: // AND (z),Y + case 0x51: // EOR (z),Y + case 0x71: // ADC (z),Y + case 0xB1: // LDA (z),Y + case 0xD1: // CMP (z),Y + case 0xF1: // SBC (z),Y + case 0x91: {// STA (z),Y + pc += 2; + int z = mem[instr]; + int lsb = (mem[z & 0xFF] & 0xFF) + y; + addr = ((mem[(z + 1) & 0xFF] & 0xFF) << 8) + lsb; + if (opcode != 0x91) { + time += lsb >> 8; + nz = cpuRead(addr); + } + break; + } + + case 0x01: // ORA (z,X) + case 0x21: // AND (z,X) + case 0x41: // EOR (z,X) + case 0x61: // ADC (z,X) + case 0xA1: // LDA (z,X) + case 0xC1: // CMP (z,X) + case 0xE1: // SBC (z,X) + case 0x81: {// STA (z,X) + pc += 2; + int z = mem[instr] + x; + addr = (mem[(z + 1) & 0xFF] & 0xFF) << 8 | (mem[z & 0xFF] & 0xFF); + if (opcode != 0x81) + nz = cpuRead(addr); + break; + } + + case 0x6C: // JMP (a) + case 0x8C: // STY a + case 0x8D: // STA a + case 0x8E: // STX a + case 0x4C: // JMP a + pc += 3; + addr = (mem[instr + 1] & 0xFF) << 8 | (mem[instr] & 0xFF); + break; + + case 0x0D: // ORA a + case 0x0E: // ASL a + case 0x2C: // BIT a + case 0x2D: // AND a + case 0x2E: // ROL a + case 0x4D: // EOR a + case 0x4E: // LSR a + case 0x6D: // ADC a + case 0x6E: // ROR a + case 0xAC: // LDY a + case 0xAD: // LDA a + case 0xAE: // LDX a + case 0xCC: // CPY a + case 0xCD: // CMP a + case 0xCE: // DEC a + case 0xEC: // CPX a + case 0xED: // SBC a + case 0xEE: // INC a + pc += 3; + nz = cpuRead(addr = (mem[instr + 1] & 0xFF) << 8 | (mem[instr] & 0xFF)); + break; + + case 0x1E: // ASL a,X + case 0x3E: // ROL a,X + case 0x5E: // LSR a,X + case 0x7E: // ROR a,X + case 0xDE: // DEC a,X + case 0xFE: // INC a,X + pc += 3; + nz = cpuRead(addr = ((mem[instr + 1] & 0xFF) << 8 | (mem[instr] & 0xFF)) + x); + // RMW instructions have no extra clock for page crossing + break; + + case 0x1D: // ORA a,X + case 0x3D: // AND a,X + case 0x5D: // EOR a,X + case 0x7D: // ADC a,X + case 0xBC: // LDY a,X + case 0xDD: // CMP a,X + case 0xFD: // SBC a,X + case 0x9D: {// STA a,X + pc += 3; + int lsb = (mem[instr] & 0xFF) + x; + addr = ((mem[instr + 1] & 0xFF) << 8) + lsb; + if (opcode != 0x9D) { + time += lsb >> 8; + nz = cpuRead(addr); + } + break; + } + + case 0x19: // ORA a,Y + case 0x39: // AND a,Y + case 0x59: // EOR a,Y + case 0x79: // ADC a,Y + case 0xB9: // LDA a,Y + case 0xBE: // LDX a,Y + case 0xD9: // CMP a,Y + case 0xF9: // SBC a,Y + case 0x99: {// STA a,Y + pc += 3; + int lsb = (mem[instr] & 0xFF) + y; + addr = ((mem[instr + 1] & 0xFF) << 8) + lsb; + if (opcode != 0x99) { + time += lsb >> 8; + nz = cpuRead(addr); + } + break; + } + + case 0x15: // ORA z,X + case 0x16: // ASL z,X + case 0x35: // AND z,X + case 0x36: // ROL z,X + case 0x55: // EOR z,X + case 0x56: // LSR z,X + case 0x75: // ADC z,X + case 0x76: // ROR z,X + case 0xB4: // LDY z,X + case 0xB5: // LDA z,X + case 0xD5: // CMP z,X + case 0xD6: // DEC z,X + case 0xF5: // SBC z,X + case 0xF6: // INC z,X + pc += 2; + nz = mem[addr = (mem[instr] + x) & 0xFF] & 0xFF; + break; + + case 0x94: // STY z,X + case 0x95: // STA z,X + pc += 2; + addr = (mem[instr] + x) & 0xFF; + break; + + case 0xB6: // LDX z,Y + case 0x96: // STX z,Y + pc += 2; + addr = (mem[instr] + y) & 0xFF; + if (opcode != 0x96) + nz = mem[addr] & 0xFF; + break; + + case 0xAA: // TAX + case 0xA8: // TAY + pc++; + nz = a; + break; + + case 0xCA: // DEX + case 0xE8: // INX + case 0x8A: // TXA + pc++; + nz = x; + break; + + case 0x88: // DEY + case 0x98: // TYA + pc++; + nz = y; + break; + + case 0x28: // PLP + case 0x68: // PLA + pc++; + nz = mem[sp] & 0xFF; + sp = (sp - 0xFF) | 0x100; + break; + + case 0x40: // RTI + nz = mem[sp] & 0xFF; + sp = (sp - 0xFF) | 0x100; + case 0x60: // RTS + pc = ((mem[(sp - 0xFF) | 0x100] & 0xFF) << 8 | (mem[sp] & 0xFF)) + 1; + sp = (sp - 0xFE) | 0x100; + break; + + case 0x00: // BRK #n + case 0x08: // PHP + break; + } + + // Operation + switch (opcode) { + case 0x60: // RTS + continue; + + case 0x91: // STA (z),Y + case 0x81: // STA (z,X) + case 0x8D: // STA a + case 0x9D: // STA a,X + case 0x99: // STA a,Y + if (addr > 0x7FF) { + cpuWrite(addr, a); + continue; + } + case 0x95: // STA z,X + mem[addr] = (byte) a; + continue; + + case 0x8E: // STX a + if (addr > 0x7FF) { + cpuWrite(addr, x); + continue; + } + case 0x86: // STX z + case 0x96: // STX z,Y + mem[addr] = (byte) x; + continue; + + case 0x8C: // STY a + if (addr > 0x7FF) { + cpuWrite(addr, y); + continue; + } + case 0x84: // STY z + case 0x94: // STY z,X + mem[addr] = (byte) y; + continue; + + case 0x10: // BPL r + case 0x30: // BMI r + case 0x50: // BVC r + case 0x70: // BVS r + case 0x90: // BCC r + case 0xB0: {// BCS r + int old = pc; + time += (((pc += mem[instr]) ^ old) >> 8 & 1) + 1; + continue; + } + + case 0xEC: // CPX a + case 0xE4: // CPX z + case 0xE0: // CPX #n + c = ~(nz = x - nz); + nz = (byte) nz; + continue; + + case 0xCC: // CPY a + case 0xC4: // CPY z + case 0xC0: // CPY #n + c = ~(nz = y - nz); + nz = (byte) nz; + continue; + + case 0xD1: // CMP (z),Y + case 0xC1: // CMP (z,X) + case 0xCD: // CMP a + case 0xDD: // CMP a,X + case 0xD9: // CMP a,Y + case 0xC5: // CMP z + case 0xD5: // CMP z,X + c = ~(nz = a - nz); + nz = (byte) nz; + continue; + + case 0xF1: // SBC (z),Y + case 0xE1: // SBC (z,X) + case 0xED: // SBC a + case 0xFD: // SBC a,X + case 0xF9: // SBC a,Y + case 0xE5: // SBC z + case 0xF5: // SBC z,X + case 0xE9: // SBC #n + case 0xEB: // SBC #n (unofficial) + nz ^= 0xFF; + case 0x71: // ADC (z),Y + case 0x61: // ADC (z,X) + case 0x6D: // ADC a + case 0x7D: // ADC a,X + case 0x79: // ADC a,Y + case 0x65: // ADC z + case 0x75: // ADC z,X + case 0x69: {// ADC #n + int t = nz ^ a; + c = (nz += (c >> 8 & 1) + a); + a = nz & 0xFF; + p = (p & ~V40) | (((t ^ nz) + 0x80) >> 2 & V40); + continue; + } + + case 0x31: // AND (z),Y + case 0x21: // AND (z,X) + case 0x2D: // AND a + case 0x3D: // AND a,X + case 0x39: // AND a,Y + case 0x25: // AND z + case 0x35: // AND z,X + case 0x29: // AND #n + a = (nz &= a); + continue; + + case 0x11: // ORA (z),Y + case 0x01: // ORA (z,X) + case 0x0D: // ORA a + case 0x1D: // ORA a,X + case 0x19: // ORA a,Y + case 0x05: // ORA z + case 0x15: // ORA z,X + case 0x09: // ORA #n + a = (nz |= a); + continue; + + case 0x51: // EOR (z),Y + case 0x41: // EOR (z,X) + case 0x4D: // EOR a + case 0x5D: // EOR a,X + case 0x59: // EOR a,Y + case 0x45: // EOR z + case 0x55: // EOR z,X + case 0x49: // EOR #n + a = (nz ^= a); + continue; + + case 0x2C: // BIT a + case 0x24: // BIT z + p = (p & ~V40) | (nz & V40); + if ((a & nz) == 0) + nz <<= 8; // result must be zero, even if N bit is set + continue; + + case 0x6C: // JMP (a) + pc = cpuRead(addr + 1 - (((addr & 0xFF) + 1) & 0x100)) << 8 | cpuRead(addr); + continue; + + case 0x4C: // JMP a + pc = addr; + continue; + + case 0x40: // RTI + pc--; + case 0x28: // PLP + p = nz & (V40 | D08 | I04); + nz = (c = nz << 8) | (~nz & Z02); + continue; + + case 0x00: {// BRK #n + int t = pc + 2; + pc = cpuRead(0xFFFF) << 8 | cpuRead(0xFFFE); + mem[(sp - 1) | 0x100] = (byte) (t >> 8); + mem[sp = (sp - 2) | 0x100] = (byte) t; + break; + } + + case 0x4E: // LSR a + case 0x5E: // LSR a,X + case 0x46: // LSR z + case 0x56: // LSR z,X + case 0x4A: // LSR + c = nz << 8; + nz >>= 1; + break; + + case 0x0E: // ASL a + case 0x1E: // ASL a,X + case 0x06: // ASL z + case 0x16: // ASL z,X + case 0x0A: // ASL + nz = (c = nz << 1) & 0xFF; + break; + + case 0x6E: // ROR a + case 0x7E: // ROR a,X + case 0x66: // ROR z + case 0x76: // ROR z,X + case 0x6A: {// ROR + int t = c & 0x100; + c = nz << 8; + nz = (nz | t) >> 1; + break; + } + + case 0x2E: // ROL a + case 0x3E: // ROL a,X + case 0x26: // ROL z + case 0x36: // ROL z,X + case 0x2A: {// ROL + int t = c >> 8 & 1; + nz = ((c = nz << 1) & 0xFF) | t; + break; + } + + case 0xCA: // DEX + case 0x88: // DEY + case 0xCE: // DEC a + case 0xDE: // DEC a,X + case 0xC6: // DEC z + case 0xD6: // DEC z,X + nz = (nz - 1) & 0xFF; + break; + + case 0xE8: // INX + case 0xEE: // INC a + case 0xFE: // INC a,X + case 0xE6: // INC z + case 0xF6: // INC z,X + nz = (nz + 1) & 0xFF; + break; + } + + // Destination + switch (opcode) { + case 0x2A: // ROL + case 0x0A: // ASL + case 0x6A: // ROR + case 0x4A: // LSR + case 0x8A: // TXA + case 0x98: // TYA + case 0xB1: // LDA (z),Y + case 0xA1: // LDA (z,X) + case 0xAD: // LDA a + case 0xB9: // LDA a,Y + case 0xA5: // LDA z + case 0xB5: // LDA z,X + case 0xA9: // LDA #n + case 0x68: // PLA + a = nz; + continue; + + case 0xCA: // DEX + case 0xE8: // INX + case 0xAA: // TAX + case 0xAE: // LDX a + case 0xBE: // LDX a,Y + case 0xA6: // LDX z + case 0xB6: // LDX z,Y + case 0xA2: // LDX #n + x = nz; + continue; + + case 0x88: // DEY + case 0xA8: // TAY + case 0xAC: // LDY a + case 0xBC: // LDY a,X + case 0xA4: // LDY z + case 0xB4: // LDY z,X + case 0xA0: // LDY #n + y = nz; + continue; + + case 0x08: // PHP + pc++; + case 0x00: {// BRK #n + int t = p | R20 | B10 | (c >> 8 & C01) | (((nz >> 8) | nz) & N80); + if (((byte) nz) == 0) + t |= Z02; + mem[sp = (sp - 1) | 0x100] = (byte) t; + continue; + } + + default: + /* + assert false; + case 0x2E: // ROL a + case 0x3E: // ROL a,X + case 0x26: // ROL z + case 0x36: // ROL z,X + case 0x0E: // ASL a + case 0x1E: // ASL a,X + case 0x06: // ASL z + case 0x16: // ASL z,X + case 0xEE: // INC a + case 0xFE: // INC a,X + case 0xE6: // INC z + case 0xF6: // INC z,X + case 0xCE: // DEC a + case 0xDE: // DEC a,X + case 0xC6: // DEC z + case 0xD6: // DEC z,X + case 0x6E: // ROR a + case 0x7E: // ROR a,X + case 0x66: // ROR z + case 0x76: // ROR z,X + case 0x4E: // LSR a + case 0x5E: // LSR a,X + case 0x46: // LSR z + case 0x56: // LSR z,X + */ + if (addr <= 0x7FF) { + mem[addr] = (byte) nz; + continue; + } + cpuWrite(addr, nz); + continue; + } + } + + stop: + this.a = a; + this.x = x; + this.y = y; + this.s = (sp - 1) & 0xFF; + this.pc = pc; + this.p = p; + this.c = c; + this.nz = nz; + this.time = time; + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/NsfEmu.java b/src/uk/me/fantastic/retro/music/gme/NsfEmu.java new file mode 100755 index 0000000..c2936a7 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/NsfEmu.java @@ -0,0 +1,232 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo NSF music file emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +final class NsfEmu extends NesCpu { + // header offsets + static final int trackCountOff = 0x06; + static final int loadAddrOff = 0x08; + static final int initAddrOff = 0x0A; + static final int playAddrOff = 0x0C; + static final int ntscSpeedOff = 0x6E; + static final int banksOff = 0x70; + static final int palSpeedOff = 0x78; + static final int speedFlagsOff = 0x7A; + static final int chipFlagsOff = 0x7B; + + // memory addresses + static final int sramAddr = 0x6000; + static final int bankSelectAddr = 0x5FF8; + static final int idleAddr = bankSelectAddr; + static final int romStart = 0x8000; + + static final int sramOffset = 0x800; // offset in ram [] + static final int sramSize = 0x2000; + static final int unmapped4000Offset = sramAddr + sramSize; + static final int ramSize = unmapped4000Offset + 0x100; + + static final int bankSize = 0x1000; + static final int bankCount = 8; + + byte[] ram; + final MemPager rom = new MemPager(bankSize, ramSize); + final byte[] header = new byte[0x80]; + final int[] initialBanks = new int[8]; + final NesApu apu = new NesApu(); + + int palOnly; + int endTime; + int playPeriod; + int nextPlay; + + protected int loadFile_(byte[] in) { + if (!isHeader(in, "NESM")) + error("Not an NSF file"); + + // Load ROM data + final int loadAddr = getLE16(in, loadAddrOff); + ram = rom.load(in, header, loadAddr % bankSize, 0xF2); + + if (header[chipFlagsOff] != 0) + error("Extra sound chips not supported"); + + // Copy initial banks + int nonZero = 0; + for (int i = 0; i < bankCount; i++) { + int bank = header[banksOff + i] & 0xFF; + initialBanks[i] = bank; + nonZero |= bank; + } + + // Use default banks if initial banks were all zero + if (nonZero == 0) { + int totalBanks = rom.size() / bankSize; + int firstBank = (loadAddr - romStart) / bankSize; + for (int i = 0; i < bankCount; i++) { + int bank = i - firstBank; + if (bank < 0 || totalBanks <= bank) + bank = 0; + initialBanks[i] = bank; + } + } + + // NTSC rate + int playbackRate = getLE16(header, ntscSpeedOff); + double clockRate = 1789772.727273; + int standardRate = 0x411A; + playPeriod = 29781; + palOnly = 0; + + if ((header[speedFlagsOff] & 3) == 1) { + // PAL rate + playbackRate = getLE16(header, palSpeedOff); + clockRate = 1662607.125; + standardRate = 0x4E20; + playPeriod = 33247; + palOnly = 1; + } + + // Custom rate + if (playbackRate != standardRate && playbackRate != 0) + playPeriod = (int) (playbackRate * clockRate * (1.0 / 1000000.0) + 0.5); + + setClockRate((int) (clockRate + 0.5)); + + apu.setOutput(buf.center()); + + return header[trackCountOff] & 0xFF; + } + + private void cpuCall(int addr) { + pc = addr; + p |= 0x04; + ram[s | 0x100] = (byte) ((idleAddr - 1) >> 8); + ram[(s + 0xFF) | 0x100] = (byte) (idleAddr - 1); + s = (s - 2) & 0xFF; + } + + public void startTrack(int track) { + super.startTrack(track); + + // APU + apu.reset(this, (palOnly != 0)); + apu.write(0, 0x4015, 0x0F); + + // Memory + java.util.Arrays.fill(ram, 0, ramSize, (byte) 0); + reset(ram, rom.unmapped()); + mapMemory(0, sramOffset, 0); + mapMemory(sramAddr, sramSize, sramOffset); + // some NSF rips expect to read back 0 from 0x4016 and 0x4017 (Maniac Mansion) + mapMemory(0x4000, pageSize, unmapped4000Offset); + for (int i = 0; i < bankCount; ++i) + cpuWrite(bankSelectAddr + i, initialBanks[i]); + + nextPlay = playPeriod; + + // CPU + a = track; + x = palOnly; + y = 0; + p = 0; + s = 0xFF; + pc = idleAddr; + cpuCall(getLE16(header, initAddrOff)); + } + + protected int runClocks(int clockCount) { + endTime = clockCount; + time = -endTime; + + while (true) { + runCpu(); + if (time >= 0) + break; + + if (pc != idleAddr) { + logError(); + return endTime; + } + + // Next play call + int next = nextPlay - endTime; + if (time < next) { + time = 0; + if (next > 0) + break; + time = next; + } + + nextPlay += playPeriod; + cpuCall(getLE16(header, playAddrOff)); + } + + // End time frame + endTime += time; + nextPlay -= endTime; + if (nextPlay < 0) // could go negative if routine is taking too long to return + nextPlay = 0; + apu.endFrame(endTime); + + return endTime; + } + + protected final int cpuRead(int addr) { + if (addr <= 0x7FF) // 90% + return ram[addr] & 0xFF; + + // APU + if (addr == 0x4015) + return apu.read(time + endTime); + + // TODO: return addr >> 8 for unmapped areas? + + if (addr < 0x10000) + return ram[mapAddr(addr)] & 0xFF; + + // address wrapped around + return ram[addr - 0x10000] & 0xFF; + } + + protected final void cpuWrite(int addr, int data) { + if (debug) assert 0 <= data && data < 0x100; + if (debug) assert 0 <= addr && addr < 0x10100; + + // SRAM + int offset; + if ((offset = addr ^ sramAddr) < sramSize) { + ram[sramOffset + offset] = (byte) data; + return; + } + + // APU + if ((addr ^ 0x4000) <= 0x17) { + apu.write(time + endTime, addr, data); + return; + } + + // Bank + int bank = addr - bankSelectAddr; + if (0 <= bank && bank < bankCount) { + mapMemory(bank * bankSize + romStart, bankSize, rom.mapAddr(data * bankSize)); + return; + } + + // RAM + if ((addr & 0xF800) == 0) // addr <= 0x7FF || addr >= 0x10000 + { + ram[addr & 0x7FF] = (byte) data; + return; + } + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/SmsApu.java b/src/uk/me/fantastic/retro/music/gme/SmsApu.java new file mode 100755 index 0000000..99d00e2 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/SmsApu.java @@ -0,0 +1,261 @@ +package uk.me.fantastic.retro.music.gme;// Sega Master System SN76489 PSG sound chip emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +class SmsOsc { + static final int masterVolume = (int) (0.40 * 65536 / 128); + + BlipBuffer output; + int outputSelect; + final BlipBuffer[] outputs = new BlipBuffer[4]; + int delay; + int lastAmp; + int volume; + + void reset() { + delay = 0; + lastAmp = 0; + volume = 0; + outputSelect = 3; + output = outputs[outputSelect]; + } +} + +final class SmsSquare extends SmsOsc { + int period; + int phase; + + void reset() { + period = 0; + phase = 0; + super.reset(); + } + + void run(int time, int endTime) { + final int period = this.period; + + int amp = volume; + if (period > 128) + amp = (amp * 2) & -phase; + + { + int delta = amp - lastAmp; + if (delta != 0) { + lastAmp = amp; + output.addDelta(time, delta * masterVolume); + } + } + + time += delay; + delay = 0; + if (period != 0) { + if (time < endTime) { + if (volume == 0 || period <= 128) // ignore 16kHz and higher + { + // keep calculating phase + int count = (endTime - time + period - 1) / period; + phase = (phase + count) & 1; + time += count * period; + } else { + final BlipBuffer output = this.output; + int delta = (amp - volume) * (2 * masterVolume); + do { + output.addDelta(time, delta = -delta); + } + while ((time += period) < endTime); + + phase = (delta >= 0 ? 1 : 0); + lastAmp = volume * (phase << 1); + } + } + delay = time - endTime; + } + } +} + +final class SmsNoise extends SmsOsc { + int shifter; + int feedback; + int select; + + void reset() { + select = 0; + shifter = 0x8000; + feedback = 0x9000; + super.reset(); + } + + void run(int time, int endTime, int period) { + // TODO: probably also not zero-centered + final BlipBuffer output = this.output; + + int amp = volume; + if ((shifter & 1) != 0) + amp = -amp; + + { + int delta = amp - lastAmp; + if (delta != 0) { + lastAmp = amp; + output.addDelta(time, delta * masterVolume); + } + } + + time += delay; + if (volume == 0) + time = endTime; + + if (time < endTime) { + final int feedback = this.feedback; + int shifter = this.shifter; + int delta = amp * (2 * masterVolume); + if ((period *= 2) == 0) + period = 16; + + do { + int changed = shifter + 1; + shifter = (feedback & -(shifter & 1)) ^ (shifter >> 1); + if ((changed & 2) != 0) // true if bits 0 and 1 differ + output.addDelta(time, delta = -delta); + } + while ((time += period) < endTime); + + this.shifter = shifter; + lastAmp = (delta < 0 ? -volume : volume); + } + delay = time - endTime; + } +} + +final class SmsApu { + int lastTime; + int latch; + int noiseFeedback; + int loopedFeedback; + + static final int oscCount = 4; + final SmsSquare[] squares = new SmsSquare[3]; + final SmsNoise noise = new SmsNoise(); + final SmsOsc[] oscs = new SmsOsc[oscCount]; + + static final int[] noisePeriods = {0x100, 0x200, 0x400}; + + private void runUntil(int endTime) { + if (endTime > lastTime) { + // run oscillators + for (int i = oscCount; --i >= 0; ) { + SmsOsc osc = oscs[i]; + if (osc.output != null) { + if (i < 3) { + squares[i].run(lastTime, endTime); + } else { + int period = squares[2].period; + if (noise.select < 3) + period = noisePeriods[noise.select]; + noise.run(lastTime, endTime, period); + } + } + } + + lastTime = endTime; + } + } + + public SmsApu() { + for (int i = 0; i < 3; i++) + oscs[i] = squares[i] = new SmsSquare(); + oscs[3] = noise; + } + + public void setOutput(BlipBuffer center, BlipBuffer left, BlipBuffer right) { + for (int i = 0; i < oscCount; i++) { + SmsOsc osc = oscs[i]; + osc.outputs[1] = right; + osc.outputs[2] = left; + osc.outputs[3] = center; + osc.output = osc.outputs[osc.outputSelect]; + } + } + + public void reset(int feedback, int noiseWidth) { + lastTime = 0; + latch = 0; + + // convert to "Galios configuration" + loopedFeedback = 1 << (noiseWidth - 1); + noiseFeedback = 0; + while (--noiseWidth >= 0) { + noiseFeedback = (noiseFeedback << 1) | (feedback & 1); + feedback >>= 1; + } + + squares[0].reset(); + squares[1].reset(); + squares[2].reset(); + noise.reset(); + } + + public void reset() { + reset(0x0009, 16); + } + + public void writeGG(int time, int data) { + runUntil(time); + + for (int i = 0; i < oscCount; i++) { + SmsOsc osc = oscs[i]; + int flags = data >> i; + BlipBuffer oldOutput = osc.output; + osc.outputSelect = (flags >> 3 & 2) | (flags & 1); + osc.output = osc.outputs[osc.outputSelect]; + if (osc.output != oldOutput && osc.lastAmp != 0) { + if (oldOutput != null) + oldOutput.addDelta(time, -osc.lastAmp * SmsOsc.masterVolume); + osc.lastAmp = 0; + } + } + } + + static final int[] volumes = { + 64, 50, 39, 31, 24, 19, 15, 12, 9, 7, 5, 4, 3, 2, 1, 0 + }; + + public void writeData(int time, int data) { + runUntil(time); + + if ((data & 0x80) != 0) + latch = data; + + int index = (latch >> 5) & 3; + if ((latch & 0x10) != 0) { + oscs[index].volume = volumes[data & 15]; + } else if (index < 3) { + SmsSquare sq = squares[index]; + if ((data & 0x80) != 0) + sq.period = (sq.period & 0xFF00) | (data << 4 & 0x00FF); + else + sq.period = (sq.period & 0x00FF) | (data << 8 & 0x3F00); + } else { + noise.select = data & 3; + noise.feedback = ((data & 0x04) != 0) ? noiseFeedback : loopedFeedback; + noise.shifter = 0x8000; + } + } + + public void endFrame(int endTime) { + if (endTime > lastTime) + runUntil(endTime); + + lastTime -= endTime; + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/SpcCpu.java b/src/uk/me/fantastic/retro/music/gme/SpcCpu.java new file mode 100755 index 0000000..061b2f8 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/SpcCpu.java @@ -0,0 +1,1238 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo SNES SPC-700 CPU emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +public class SpcCpu extends MusicEmu { + // Registers. NOT kept updated during runCpu() + public int a, x, y, psw, sp, pc; + + // Current time + public int time; + + // Memory read and write handlers + protected int cpuRead(int addr) { + return 0; + } + + protected void cpuWrite(int addr, int data) { + } + + // Resets registers and uses supplied physical memory + public final void reset(byte[] mem) { + this.mem = mem; + a = 0; + x = 0; + y = 0; + sp = 0xFF; + pc = 0; + psw = 0x04; + + time = 0; + } + + public final void setPsw(int psw) { + this.psw = psw; + } + + private byte[] mem; + + static final int[] instrTimes = + {// 0 1 2 3 4 5 6 7 8 9 A B C D E F + 2, 8, 4, 5, 3, 4, 3, 6, 2, 6, 5, 4, 5, 4, 6, 8, // 0 + 2, 8, 4, 5, 4, 5, 5, 6, 5, 5, 6, 5, 2, 2, 4, 6, // 1 + 2, 8, 4, 5, 3, 4, 3, 6, 2, 6, 5, 4, 5, 4, 5, 2, // 2 + 2, 8, 4, 5, 4, 5, 5, 6, 5, 5, 6, 5, 2, 2, 3, 8, // 3 + 2, 8, 4, 5, 3, 4, 3, 6, 2, 6, 4, 4, 5, 4, 6, 6, // 4 + 2, 8, 4, 5, 4, 5, 5, 6, 5, 5, 4, 5, 2, 2, 4, 3, // 5 + 2, 8, 4, 5, 3, 4, 3, 6, 2, 6, 4, 4, 5, 4, 5, 5, // 6 + 2, 8, 4, 5, 4, 5, 5, 6, 5, 5, 5, 5, 2, 2, 3, 6, // 7 + 2, 8, 4, 5, 3, 4, 3, 6, 2, 6, 5, 4, 5, 2, 4, 5, // 8 + 2, 8, 4, 5, 4, 5, 5, 6, 5, 5, 5, 5, 2, 2, 12, 5,// 9 + 3, 8, 4, 5, 3, 4, 3, 6, 2, 6, 4, 4, 5, 2, 4, 4, // A + 2, 8, 4, 5, 4, 5, 5, 6, 5, 5, 5, 5, 2, 2, 3, 4, // B + 3, 8, 4, 5, 4, 5, 4, 7, 2, 5, 6, 4, 5, 2, 4, 9, // C + 2, 8, 4, 5, 5, 6, 6, 7, 4, 5, 5, 5, 2, 2, 6, 3, // D + 2, 8, 4, 5, 3, 4, 3, 6, 2, 4, 5, 3, 4, 3, 4, 0, // E + 2, 8, 4, 5, 4, 5, 5, 6, 3, 4, 5, 4, 2, 2, 4, 0, // F + }; + + // Hex value in name to clarify code and bit shifting. + // Flag stored in indicated variable during emulation + static final int n80 = 0x80; // (nz & 0x880) != 0 + static final int v40 = 0x40; // psw + static final int p20 = 0x20; // psw, dp == 0x100 + static final int b10 = 0x10; // psw + static final int h08 = 0x08; // psw + static final int i04 = 0x04; // psw + static final int z02 = 0x02; // (byte) nz == 0 + static final int c01 = 0x01; // (c & 0x100) != 0 + + // Runs until time >= 0 + public final void runCpu() { + // locals are faster, and first three are more efficient to access + final byte[] mem = this.mem; + int nz; + int pc = this.pc; + + int a = this.a; + int x = this.x; + int y = this.y; + int psw = this.psw; + int sp = (this.sp + 1) | 0x100; + int time = this.time; + final int[] instrTimes = this.instrTimes; + + // unpack psw + int c, dp; + c = psw << 8; + dp = psw << 3 & 0x100; + nz = (psw << 4 & 0x800) | (~psw & z02); + + int data = 0; + int addr = 0; + + loop: + while (time < 0) { + if (debug) { + assert 0 <= a && a < 0x100; + assert 0 <= x && x < 0x100; + assert 0 <= y && y < 0x100; + assert 0 <= pc && pc < 0x10000; + assert 0x100 <= sp && sp < 0x200; + assert dp == 0 || dp == 0x100; + } + + int opcode; + this.time = (time += instrTimes[opcode = mem[pc] & 0xFF]); + switch (opcode) { + + //////// Often used + + case 0xE4: // MOV A, d + a = nz = cpuRead(mem[pc + 1] & 0xFF | dp); + pc += 2; + continue; + + case 0xF5: // MOV A, !a+X + a = nz = cpuRead(((mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF)) + x); + pc += 3; + continue; + + case 0xF4: // MOV A, d+X + a = nz = cpuRead((mem[pc + 1] + x) & 0xFF | dp); + pc += 2; + continue; + + case 0xEB: // MOV Y, d + y = nz = cpuRead(mem[pc + 1] & 0xFF | dp); + pc += 2; + continue; + + case 0x2F: // BRA r + pc += mem[pc + 1] + 2; + continue; + + case 0x90: // BCC r + pc += 2; + if ((c & 0x100) == 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0xB0: // BCS r + pc += 2; + if ((c & 0x100) != 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0xF0: // BEQ r + pc += 2; + if (((byte) nz) == 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0xD0: // BNE r + pc += 2; + if (((byte) nz) != 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0x30: // BMI r + pc += 2; + if ((nz & 0x880) != 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0x10: // BPL r + pc += 2; + if ((nz & 0x880) == 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0x50: // BVC r + pc += 2; + if ((psw & v40) == 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0x70: // BVS r + pc += 2; + if ((psw & v40) != 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + //////// Self-contained + + case 0xFE: // DBNZ Y, r + pc += 2; + if ((y = (y - 1) & 0xFF) != 0) + break; + continue; + + case 0xEF: // SLEEP + case 0xFF: // STOP + break loop; + + case 0x9C: // DEC A + pc++; + a = (nz = a - 1) & 0xFF; + continue; + + case 0xBC: // INC A + pc++; + a = (nz = a + 1) & 0xFF; + continue; + + case 0x1D: // DEC X + pc++; + x = (nz = x - 1) & 0xFF; + continue; + + case 0x3D: // INC X + pc++; + x = (nz = x + 1) & 0xFF; + continue; + + case 0xDC: // DEC Y + pc++; + y = (nz = y - 1) & 0xFF; + continue; + + case 0xFC: // INC Y + pc++; + y = (nz = y + 1) & 0xFF; + continue; + + case 0x1C: // ASL A + c = 0; + case 0x3C: {// ROL A + int t = c >> 8 & 1; + c = a << 1; + a = (nz = c | t) & 0xFF; + pc++; + continue; + } + + case 0x5C: // LSR A + c = 0; + case 0x7C: // ROR A + nz = ((c & 0x100) | a) >> 1; + c = a << 8; + a = nz; + pc++; + continue; + + case 0x9F: // XCN A + pc++; + a = nz = a >> 4 | ((a & 0x0F) << 4); + continue; + + case 0xDF: // DAA A + pc++; + if (a > 0x99 || (c & 0x100) != 0) { + a += 0x60; + c = 0x100; + } + + if ((a & 0x0F) > 9 || (psw & h08) != 0) + a += 0x06; + + nz = (a &= 0xFF); + continue; + + case 0xBE: // DAS A + pc++; + if (a > 0x99 || (c & 0x100) == 0) { + a -= 0x60; + c = 0; + } + + if ((a & 0x0F) > 9 || (psw & h08) == 0) + a -= 0x06; + + nz = (a &= 0xFF); + continue; + + case 0x9E: {// DIV YA, X + pc++; + int ya = y << 8 | a; + + psw &= ~(h08 | v40); + + if (y >= x) + psw |= v40; + + if ((y & 15) >= (x & 15)) + psw |= h08; + + if (y < (x << 1)) { + a = ya / x; + y = ya - a * x; + } else { + a = 255 - (ya - (x << 9)) / (256 - x); + y = x + (ya - (x << 9)) % (256 - x); + } + + nz = (a &= 0xFF); + continue; + } + + case 0xCF: {// MUL YA + pc++; + int t = y * a; + a = t & 0xFF; + nz = (t >> 1 | t) & 0x7F; + nz |= (y = t >> 8); + continue; + } + + case 0x00: // NOP + pc++; + continue; + + case 0x60: // CLRC + pc++; + c = 0; + continue; + + case 0x80: // SETC + pc++; + c = ~0; + continue; + + case 0xED: // NOTC + pc++; + c ^= 0x100; + continue; + + case 0x20: // CLRP + pc++; + dp = 0; + continue; + + case 0x40: // SETP + pc++; + dp = 0x100; + continue; + + case 0xE0: // CLRV + pc++; + psw &= ~(v40 | h08); + continue; + + case 0xC0: // DI + pc++; + psw |= i04; + continue; + + case 0xA0: // EI + pc++; + psw &= ~i04; + continue; + + case 0x5D: // MOV X, A + pc++; + x = nz = a; + continue; + + case 0xFD: // MOV Y, A + pc++; + y = nz = a; + continue; + + case 0x7D: // MOV A, X + pc++; + a = nz = x; + continue; + + case 0xDD: // MOV A, Y + pc++; + a = nz = y; + continue; + + case 0x9D: // MOV X, SP + pc++; + x = nz = (sp - 1) & 0xFF; + continue; + + case 0xBD: // MOV SP, X + pc++; + sp = (x + 1) | 0x100; + continue; + + case 0xBF: // MOV A, (X)+ + pc++; + a = nz = cpuRead(x + dp); + x = (x + 1) & 0xFF; + continue; + + case 0xD9: // MOV d+Y, X + cpuWrite((mem[pc + 1] + y) & 0xFF | dp, x); + pc += 2; + continue; + + case 0xD6: // MOV !a+Y, A + cpuWrite(((mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF)) + y, a); + pc += 3; + continue; + + case 0xD5: // MOV !a+X, A + cpuWrite(((mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF)) + x, a); + pc += 3; + continue; + + case 0xF9: // MOV X, d+Y + x = nz = cpuRead((mem[pc + 1] + y) & 0xFF | dp); + pc += 2; + continue; + + case 0xD7: {// MOV [d]+Y, A + int t = mem[pc + 1]; + cpuWrite(((mem[(t + 1) & 0xFF | dp] & 0xFF) << 8 | (mem[t & 0xFF | dp] & 0xFF)) + y, a); + pc += 2; + continue; + } + + case 0xC7: {// MOV [d+X], A + int t = mem[pc + 1] + x; + cpuWrite((mem[(t + 1) & 0xFF | dp] & 0xFF) << 8 | (mem[t & 0xFF | dp] & 0xFF), a); + pc += 2; + continue; + } + + case 0xC6: // MOV (X), A + pc++; + cpuWrite(x + dp, a); + continue; + + case 0xAF: // MOV (X)+, A + pc++; + cpuWrite(x + dp, a); + x = (x + 1) & 0xFF; + continue; + + case 0x8F: // MOV d, #i + cpuWrite(mem[pc + 2] & 0xFF | dp, mem[pc + 1]); + pc += 3; + continue; + + case 0xFA: // MOV dd, ds + cpuWrite(mem[pc + 2] & 0xFF | dp, cpuRead(mem[pc + 1] & 0xFF | dp)); + pc += 3; + continue; + + case 0xCA: // MOV1 m.b, C + data = mem[pc + 2]; + addr = (data & 0x1F) << 8 | (mem[pc + 1] & 0xFF); + data = data >> 5 & 7; + cpuWrite(addr, cpuRead(addr) & ~(1 << data) | ((c & 0x100) >> (8 - data))); + pc += 3; + continue; + + case 0xEA: // NOT1 m.b + data = mem[pc + 2]; + addr = (data & 0x1F) << 8 | (mem[pc + 1] & 0xFF); + cpuWrite(addr, cpuRead(addr) ^ (1 << (data >> 5 & 7))); + pc += 3; + continue; + + case 0x4A: // AND1 C, m.b + case 0xAA: // MOV1 C, m.b + case 0x0A: // OR1 C, m.b + case 0x6A: // AND1 C, /m.b + case 0x2A: // OR1 C, /m.b + case 0x8A: // EOR1 C, m.b + data = mem[pc + 2]; + data = cpuRead((data & 0x1F) << 8 | (mem[pc + 1] & 0xFF)) << (8 - (data >> 5 & 7)); + pc += 3; + switch (opcode) { + case 0x4A: // AND1 C, m.b + c &= data; + continue; + + case 0xAA: // MOV1 C, m.b + c = data; + continue; + + case 0x0A: // OR1 C, m.b + c |= data; + continue; + + case 0x6A: // AND1 C, /m.b + c &= ~data; + continue; + + case 0x2A: // OR1 C, /m.b + c |= ~data; + continue; + + default: + //case 0x8A: // EOR1 C, m.b + c ^= data; + continue; + } + + case 0x02: // SET1 d.0 + case 0x22: // SET1 d.1 + case 0x42: // SET1 d.2 + case 0x62: // SET1 d.3 + case 0x82: // SET1 d.4 + case 0xA2: // SET1 d.5 + case 0xC2: // SET1 d.6 + case 0xE2: // SET1 d.7 + case 0x12: // CLR1 d.0 + case 0x32: // CLR1 d.1 + case 0x52: // CLR1 d.2 + case 0x72: // CLR1 d.3 + case 0x92: // CLR1 d.4 + case 0xB2: // CLR1 d.5 + case 0xD2: // CLR1 d.6 + case 0xF2: {// CLR1 d.7 + data = cpuRead(addr = mem[pc + 1] & 0xFF | dp); + int t = 1 << (opcode >> 5); + data |= t; + if ((opcode & 0x10) != 0) + data ^= t; + cpuWrite(addr, data); + pc += 2; + continue; + } + + case 0x2D: // PUSH A + pc++; + mem[sp = (sp - 1) | 0x100] = (byte) a; + continue; + + case 0x4D: // PUSH X + pc++; + mem[sp = (sp - 1) | 0x100] = (byte) x; + continue; + + case 0x6D: // PUSH Y + pc++; + mem[sp = (sp - 1) | 0x100] = (byte) y; + continue; + + case 0x0D: // PUSH PSW + case 0x0F: {// BRK + // calculate PSW + int t = psw & ~(n80 | p20 | z02 | c01); + t |= c >> 8 & c01; + t |= dp >> 3 & p20; + t |= ((nz >> 4) | nz) & n80; + if (((byte) nz) == 0) t |= z02; + + pc++; + if (opcode == 0x0F) // BRK + { + mem[(sp - 1) | 0x100] = (byte) (pc >> 8); + mem[sp = (sp - 2) | 0x100] = (byte) pc; + pc = (mem[0xFFDF] & 0xFF) << 8 | (mem[0xFFDE] & 0xFF); + psw = (psw | b10) & ~i04; + } + mem[sp = (sp - 1) | 0x100] = (byte) t; + continue; + } + + case 0xAE: // POP A + pc++; + a = mem[sp] & 0xFF; + sp = (sp + 1) | 0x100; + continue; + + case 0xCE: // POP X + pc++; + x = mem[sp] & 0xFF; + sp = (sp + 1) | 0x100; + continue; + + case 0xEE: // POP Y + pc++; + y = mem[sp] & 0xFF; + sp = (sp + 1) | 0x100; + continue; + + case 0x8E: // POP PSW + case 0x7F: // RET1 + pc++; + psw = mem[sp]; + sp = (sp - 0xFF) | 0x100; + + if (opcode == 0x7F) // RET1 + { + pc = (mem[(sp - 0xFF) | 0x100] & 0xFF) << 8 | (mem[sp] & 0xFF); + sp = (sp - 0xFE) | 0x100; + } + + // unpack psw + c = psw << 8; + dp = psw << 3 & 0x100; + nz = (psw << 4 & 0x800) | (~psw & z02); + continue; + + case 0x6F: // RET + pc = (mem[(sp - 0xFF) | 0x100] & 0xFF) << 8 | (mem[sp] & 0xFF); + sp = (sp - 0xFE) | 0x100; + continue; + + case 0xDA: {// MOVW d, YA + int t = mem[pc + 1]; + cpuWrite(t & 0xFF | dp, a); + cpuWrite((t + 1) & 0xFF | dp, y); + pc += 2; + continue; + } + + case 0x1A: // DECW d + case 0x3A: // INCW d + case 0x5A: // CMPW YA, d + case 0x7A: // ADDW YA, d + case 0x9A: // SUBW YA, d + case 0xBA: {// MOVW YA, d + addr = mem[pc + 1] & 0xFF | dp; + data = (mem[addr + 1] & 0xFF) << 8 | (mem[addr] & 0xFF); + + // addr >= 0xEF || addr <= 0xFF + if ((addr ^ 0xFF) <= 0x11) // 1% + data = cpuRead(addr + 1) << 8 | cpuRead(addr); + + pc += 2; + switch (opcode) { + case 0x1A: // DECW d + data -= 2; + case 0x3A: // INCW d + data++; + nz = (data & 0x7F) | (data >> 1 & 0x7F) | (data >> 8); + + mem[addr] = (byte) data; + mem[addr + 1] = (byte) (data >> 8); + + // addr >= 0xEF || addr <= 0xFF + if ((addr ^ 0xFF) <= 0x11) // 1% + { + cpuWrite(addr, data); + cpuWrite(addr + 1, data >> 8); + } + continue; + + case 0xBA: // MOVW YA, d + nz = 0x7F & (a = data & 0xFF); + nz |= (a >> 1) | (y = data >> 8); + continue; + + case 0x5A: // CMPW YA, d + data = (y << 8 | a) - data; + nz = (data & 0x7F) | (data >> 1 & 0x7F); + nz = (byte) (nz | (data >>= 8)); + c = ~data; + continue; + + case 0x9A: // SUBW YA, d + data = -data & 0xFFFF; + default: { + //case 0x7A: // ADDW YA, d + int t = (data >> 8) ^ y; + a = 0xFF & (data += y << 8 | a); + t ^= (c = data >> 8); + nz = (a & 0x7F) | (a >> 1) | (y = c & 0xFF); + psw = (psw & ~(v40 | h08)) | + (t >> 1 & h08) | + ((t + 0x80) >> 2 & v40); + continue; + } + } + } + + //////// Misc + + case 0x13: // BBC d.0, r + case 0x33: // BBC d.1, r + case 0x53: // BBC d.2, r + case 0x73: // BBC d.3, r + case 0x93: // BBC d.4, r + case 0xB3: // BBC d.5, r + case 0xD3: // BBC d.6, r + case 0xF3: // BBC d.7, r + case 0x03: // BBS d.0, r + case 0x23: // BBS d.1, r + case 0x43: // BBS d.2, r + case 0x63: // BBS d.3, r + case 0x83: // BBS d.4, r + case 0xA3: // BBS d.5, r + case 0xC3: // BBS d.6, r + case 0xE3: // BBS d.7, r + case 0x6E: // DBNZ d, r + case 0x2E: // CBNE d, r + data = cpuRead((addr = mem[pc + 1] & 0xFF | dp)); + pc += 3; + break; + + case 0xDE: // CBNE d+X, r + data = cpuRead((addr = (mem[pc + 1] + x) & 0xFF | dp)); + pc += 3; + break; + + case 0x1F: // JMP [!a+X] + case 0x3F: // CALL !a + case 0x5F: // JMP !a + case 0xC5: // MOV !a, A + case 0xC9: // MOV !a, X + case 0xCC: // MOV !a, Y + addr = (mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF); + pc += 3; + break; + + case 0x4F: // PCALL u + addr = 0xFF00 | (mem[pc + 1] & 0xFF); + pc += 2; + break; + + //////// nz = operand, data = operand 2, addr = address of operand 2 + + case 0x38: // AND d, #i + case 0x58: // EOR d, #i + case 0x78: // CMP d, #i + case 0x98: // ADC d, #i + case 0xB8: // SBC d, #i + case 0x18: // OR d, #i + nz = mem[pc + 1] & 0xFF; + data = cpuRead(addr = mem[pc + 2] & 0xFF | dp); + pc += 3; + break; + + case 0x39: // AND (X), (Y) + case 0x59: // EOR (X), (Y) + case 0x79: // CMP (X), (Y) + case 0x99: // ADC (X), (Y) + case 0xB9: // SBC (X), (Y) + case 0x19: // OR (X), (Y) + nz = cpuRead(y + dp); + data = cpuRead(addr = x + dp); + pc++; + break; + + case 0x29: // AND dd, ds + case 0x49: // EOR dd, ds + case 0x69: // CMP dd, ds + case 0x89: // ADC dd, ds + case 0xA9: // SBC dd, ds + case 0x09: // OR dd, ds + nz = cpuRead(mem[pc + 1] & 0xFF | dp); + data = cpuRead(addr = mem[pc + 2] & 0xFF | dp); + pc += 3; + break; + + //////// nz = operand + + case 0x25: // AND A, !a + case 0x45: // EOR A, !a + case 0x65: // CMP A, !a + case 0x85: // ADC A, !a + case 0xA5: // SBC A, !a + case 0x05: // OR A, !a + case 0xE5: // MOV A, !a + case 0x0E: // TSET1 !a + case 0x4E: // TCLR1 !a + case 0x0C: // ASL !a + case 0x2C: // ROL !a + case 0x4C: // LSR !a + case 0x6C: // ROR !a + case 0x8C: // DEC !a + case 0xAC: // INC !a + case 0x1E: // CMP X, !a + case 0xE9: // MOV X, !a + case 0x5E: // CMP Y, !a + case 0xEC: // MOV Y, !a + nz = cpuRead(addr = (mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF)); + pc += 3; + break; + + case 0x35: // AND A, !a+X + case 0x55: // EOR A, !a+X + case 0x75: // CMP A, !a+X + case 0x95: // ADC A, !a+X + case 0xB5: // SBC A, !a+X + case 0x15: // OR A, !a+X + nz = cpuRead(((mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF)) + x); + pc += 3; + break; + + case 0x36: // AND A, !a+Y + case 0x56: // EOR A, !a+Y + case 0x76: // CMP A, !a+Y + case 0x96: // ADC A, !a+Y + case 0xB6: // SBC A, !a+Y + case 0x16: // OR A, !a+Y + case 0xF6: // MOV A, !a+Y + nz = cpuRead(((mem[pc + 2] & 0xFF) << 8 | (mem[pc + 1] & 0xFF)) + y); + pc += 3; + break; + + case 0x26: // AND A, (X) + case 0x46: // EOR A, (X) + case 0x66: // CMP A, (X) + case 0x86: // ADC A, (X) + case 0xA6: // SBC A, (X) + case 0x06: // OR A, (X) + case 0xE6: // MOV A, (X) + nz = cpuRead(x + dp); + pc++; + break; + + case 0x24: // AND A, d + case 0x44: // EOR A, d + case 0x64: // CMP A, d + case 0x84: // ADC A, d + case 0xA4: // SBC A, d + case 0x04: // OR A, d + case 0x0B: // ASL d + case 0x2B: // ROL d + case 0x4B: // LSR d + case 0x6B: // ROR d + case 0x8B: // DEC d + case 0xAB: // INC d + case 0x3E: // CMP X, d + case 0xF8: // MOV X, d + case 0x7E: // CMP Y, d + nz = cpuRead(addr = mem[pc + 1] & 0xFF | dp); + pc += 2; + break; + + case 0x34: // AND A, d+X + case 0x54: // EOR A, d+X + case 0x74: // CMP A, d+X + case 0x94: // ADC A, d+X + case 0xB4: // SBC A, d+X + case 0x14: // OR A, d+X + case 0x1B: // ASL d+X + case 0x3B: // ROL d+X + case 0x5B: // LSR d+X + case 0x7B: // ROR d+X + case 0x9B: // DEC d+X + case 0xBB: // INC d+X + case 0xFB: // MOV Y, d+X + nz = cpuRead(addr = (mem[pc + 1] + x) & 0xFF | dp); + pc += 2; + break; + + case 0x37: // AND A, [d]+Y + case 0x57: // EOR A, [d]+Y + case 0x77: // CMP A, [d]+Y + case 0x97: // ADC A, [d]+Y + case 0xB7: // SBC A, [d]+Y + case 0x17: // OR A, [d]+Y + case 0xF7: {// MOV A, [d]+Y + int t = mem[pc + 1]; + nz = cpuRead(((mem[(t + 1) & 0xFF | dp] & 0xFF) << 8 | (mem[t & 0xFF | dp] & 0xFF)) + y); + pc += 2; + break; + } + + case 0x27: // AND A, [d+X] + case 0x47: // EOR A, [d+X] + case 0x67: // CMP A, [d+X] + case 0x87: // ADC A, [d+X] + case 0xA7: // SBC A, [d+X] + case 0x07: // OR A, [d+X] + case 0xE7: {// MOV A, [d+X] + int t = mem[pc + 1] + x; + nz = cpuRead((mem[(t + 1) & 0xFF | dp] & 0xFF) << 8 | (mem[t & 0xFF | dp] & 0xFF)); + pc += 2; + break; + } + + case 0x28: // AND A, #i + case 0x48: // EOR A, #i + case 0x68: // CMP A, #i + case 0x88: // ADC A, #i + case 0xA8: // SBC A, #i + case 0x08: // OR A, #i + case 0xE8: // MOV A, #i + case 0xC8: // CMP X, #i + case 0xCD: // MOV X, #i + case 0x8D: // MOV Y, #i + case 0xAD: // CMP Y, #i + nz = mem[pc + 1] & 0xFF; + pc += 2; + break; + + case 0xC4: // MOV d, A + case 0xD8: // MOV d, X + case 0xCB: // MOV d, Y + addr = mem[pc + 1] & 0xFF | dp; + pc += 2; + break; + + case 0xD4: // MOV d+X, A + case 0xDB: // MOV d+X, Y + addr = (mem[pc + 1] + x) & 0xFF | dp; + pc += 2; + break; + } + + // Operation + switch (opcode) { + case 0x5F: // JMP !a + pc = addr; + continue; + + case 0x1F: // JMP [!a+X] + addr += x; + pc = (mem[addr + 1] & 0xFF) << 8 | (mem[addr] & 0xFF); + continue; + + case 0x13: // BBC d.0, r + case 0x33: // BBC d.1, r + case 0x53: // BBC d.2, r + case 0x73: // BBC d.3, r + case 0x93: // BBC d.4, r + case 0xB3: // BBC d.5, r + case 0xD3: // BBC d.6, r + case 0xF3: // BBC d.7, r + case 0x03: // BBS d.0, r + case 0x23: // BBS d.1, r + case 0x43: // BBS d.2, r + case 0x63: // BBS d.3, r + case 0x83: // BBS d.4, r + case 0xA3: // BBS d.5, r + case 0xC3: // BBS d.6, r + case 0xE3: // BBS d.7, r + if ((((data >> (opcode >> 5)) ^ (opcode >> 4)) & 1) != 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0x6E: // DBNZ d, r + cpuWrite(addr, --data); + if (data != 0) { + pc += mem[pc - 1]; + time += 2; + } + continue; + + case 0xDE: // CBNE d+X, r + case 0x2E: // CBNE d, r + if (data == a) + continue; + case 0xFE: // DBNZ Y, r + time += 2; + pc += mem[pc - 1]; + continue; + + case 0x01: // TCALL 0 + case 0x11: // TCALL 1 + case 0x21: // TCALL 2 + case 0x31: // TCALL 3 + case 0x41: // TCALL 4 + case 0x51: // TCALL 5 + case 0x61: // TCALL 6 + case 0x71: // TCALL 7 + case 0x81: // TCALL 8 + case 0x91: // TCALL 9 + case 0xA1: // TCALL 10 + case 0xB1: // TCALL 11 + case 0xC1: // TCALL 12 + case 0xD1: // TCALL 13 + case 0xE1: // TCALL 14 + case 0xF1: // TCALL 15 + addr = 0xFFDE - (opcode >> 3); + addr = (mem[addr + 1] & 0xFF) << 8 | (mem[addr] & 0xFF); + case 0x4F: // PCALL u + case 0x3F: // CALL !a + mem[(sp - 1) | 0x100] = (byte) (pc >> 8); + mem[sp = (sp - 2) | 0x100] = (byte) pc; + pc = addr; + continue; + + case 0x65: // CMP A, !a + case 0x75: // CMP A, !a+X + case 0x76: // CMP A, !a+Y + case 0x68: // CMP A, #i + case 0x66: // CMP A, (X) + case 0x67: // CMP A, [d+X] + case 0x77: // CMP A, [d]+Y + case 0x64: // CMP A, d + case 0x74: // CMP A, d+X + c = ~(nz = a - nz); + nz = (byte) nz; + continue; + + case 0x1E: // CMP X, !a + case 0xC8: // CMP X, #i + case 0x3E: // CMP X, d + c = ~(nz = x - nz); + nz = (byte) nz; + continue; + + case 0x5E: // CMP Y, !a + case 0xAD: // CMP Y, #i + case 0x7E: // CMP Y, d + c = ~(nz = y - nz); + nz = (byte) nz; + continue; + + case 0x78: // CMP d, #i + case 0x69: // CMP dd, ds + case 0x79: // CMP (X), (Y) + c = ~(nz = data - nz); + nz = (byte) nz; + continue; + + case 0xA5: // SBC A, !a + case 0xB5: // SBC A, !a+X + case 0xB6: // SBC A, !a+Y + case 0xA8: // SBC A, #i + case 0xA6: // SBC A, (X) + case 0xA7: // SBC A, [d+X] + case 0xB7: // SBC A, [d]+Y + case 0xA4: // SBC A, d + case 0xB4: // SBC A, d+X + nz ^= 0xFF; + case 0x85: // ADC A, !a + case 0x95: // ADC A, !a+X + case 0x96: // ADC A, !a+Y + case 0x88: // ADC A, #i + case 0x86: // ADC A, (X) + case 0x87: // ADC A, [d+X] + case 0x97: // ADC A, [d]+Y + case 0x84: // ADC A, d + case 0x94: {// ADC A, d+X + int flags = a ^ nz; + flags ^= (c = nz += a + (c >> 8 & 1)); + psw = (psw & ~(v40 | h08)) | + (flags >> 1 & h08) | + ((flags + 0x80) >> 2 & v40); + a = nz & 0xFF; + continue; + } + + case 0xB9: // SBC (X), (Y) + case 0xB8: // SBC d, #i + case 0xA9: // SBC dd, ds + nz ^= 0xFF; + case 0x99: // ADC (X), (Y) + case 0x98: // ADC d, #i + case 0x89: {// ADC dd, ds + int flags = nz ^ data; + flags ^= (c = nz += data + (c >> 8 & 1)); + psw = (psw & ~(v40 | h08)) | + (flags >> 1 & h08) | + ((flags + 0x80) >> 2 & v40); + cpuWrite(addr, nz); + continue; + } + + case 0x25: // AND A, !a + case 0x35: // AND A, !a+X + case 0x36: // AND A, !a+Y + case 0x28: // AND A, #i + case 0x26: // AND A, (X) + case 0x27: // AND A, [d+X] + case 0x37: // AND A, [d]+Y + case 0x24: // AND A, d + case 0x34: // AND A, d+X + nz = a &= nz; + continue; + + case 0x39: // AND (X), (Y) + case 0x38: // AND d, #i + case 0x29: // AND dd, ds + cpuWrite(addr, nz &= data); + continue; + + case 0x05: // OR A, !a + case 0x15: // OR A, !a+X + case 0x16: // OR A, !a+Y + case 0x08: // OR A, #i + case 0x06: // OR A, (X) + case 0x07: // OR A, [d+X] + case 0x17: // OR A, [d]+Y + case 0x04: // OR A, d + case 0x14: // OR A, d+X + nz = a |= nz; + continue; + + case 0x19: // OR (X), (Y) + case 0x18: // OR d, #i + case 0x09: // OR dd, ds + cpuWrite(addr, nz |= data); + continue; + + case 0x45: // EOR A, !a + case 0x55: // EOR A, !a+X + case 0x56: // EOR A, !a+Y + case 0x48: // EOR A, #i + case 0x46: // EOR A, (X) + case 0x47: // EOR A, [d+X] + case 0x57: // EOR A, [d]+Y + case 0x44: // EOR A, d + case 0x54: // EOR A, d+X + nz = a ^= nz; + continue; + + case 0x59: // EOR (X), (Y) + case 0x58: // EOR d, #i + case 0x49: // EOR dd, ds + cpuWrite(addr, nz ^= data); + continue; + + case 0x8C: // DEC !a + case 0x8B: // DEC d + case 0x9B: // DEC d+X + cpuWrite(addr, --nz); + continue; + + case 0xAC: // INC !a + case 0xAB: // INC d + case 0xBB: // INC d+X + cpuWrite(addr, ++nz); + continue; + + case 0x0C: // ASL !a + case 0x0B: // ASL d + case 0x1B: // ASL d+X + c = 0; + case 0x2C: // ROL !a + case 0x2B: // ROL d + case 0x3B: {// ROL d+X + int t = c >> 8 & 1; + c = nz << 1; + cpuWrite(addr, nz = c | t); + continue; + } + + case 0x4C: // LSR !a + case 0x4B: // LSR d + case 0x5B: // LSR d+X + c = 0; + case 0x6C: // ROR !a + case 0x6B: // ROR d + case 0x7B: {// ROR d+X + int t = c & 0x100; + c = nz << 8; + cpuWrite(addr, nz = (nz | t) >> 1); + continue; + } + + case 0x4E: {// TCLR1 !a + int t = nz & ~a; + nz = (byte) (a - nz); + cpuWrite(addr, t); + continue; + } + + case 0x0E: {// TSET1 !a + int t = nz | a; + nz = (byte) (a - nz); + cpuWrite(addr, t); + continue; + } + + case 0xC4: // MOV d, A + case 0xD4: // MOV d+X, A + case 0xC5: // MOV !a, A + cpuWrite(addr, a); + continue; + + case 0xD8: // MOV d, X + case 0xC9: // MOV !a, X + cpuWrite(addr, x); + continue; + + case 0xCB: // MOV d, Y + case 0xDB: // MOV d+X, Y + case 0xCC: // MOV !a, Y + cpuWrite(addr, y); + continue; + + case 0xE5: // MOV A, !a + case 0xF6: // MOV A, !a+Y + case 0xE8: // MOV A, #i + case 0xE6: // MOV A, (X) + case 0xBF: // MOV A, (X)+ + case 0x7D: // MOV A, X + case 0xDD: // MOV A, Y + case 0xE7: // MOV A, [d+X] + case 0xF7: // MOV A, [d]+Y + a = nz; + continue; + + case 0xE9: // MOV X, !a + case 0xCD: // MOV X, #i + case 0x5D: // MOV X, A + case 0x9D: // MOV X, SP + case 0xF8: // MOV X, d + x = nz; + continue; + + case 0xEC: // MOV Y, !a + case 0x8D: // MOV Y, #i + case 0xFD: // MOV Y, A + case 0xFB: // MOV Y, d+X + y = nz; + continue; + } + } + + // calculate PSW + psw &= ~(n80 | p20 | z02 | c01); + psw |= c >> 8 & c01; + psw |= dp >> 3 & p20; + psw |= ((nz >> 4) | nz) & n80; + if (((byte) nz) == 0) psw |= z02; + + this.pc = pc; + this.a = a; + this.x = x; + this.y = y; + this.psw = psw; + this.sp = (sp - 1) & 0xFF; + this.time = time; + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/SpcDsp.java b/src/uk/me/fantastic/retro/music/gme/SpcDsp.java new file mode 100755 index 0000000..8d45429 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/SpcDsp.java @@ -0,0 +1,599 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo SPC-700 DSP emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +public final class SpcDsp { + // Initializes DSP with new RAM and 128 bytes of register state (beginning at regs [regs_offset]). + // Keeps reference to ram_64k. + public void init(byte[] ram_64k, byte[] regs, int regs_offset) { + this.ram = ram_64k; + for (int i = register_count; --i >= 0; ) + this.regs[i] = regs[i + regs_offset]; + + java.util.Arrays.fill(echo_hist, 0, echo_hist.length, 0); + + echo_hist_pos = 0; + every_other_sample = 1; + kon = 0; + lfsr = 0x4000; + echo_offset = 0; + echo_length = 0; + new_kon = this.regs[r_kon]; + t_koff = 0; + + // counters start out with this synchronization + counter0.i = 1; + counter1.i = 0; + counter2.i = -32; + counter3.i = 11; + + // Internal state + for (int i = voice_count; --i >= 0; ) { + Voice v = new Voice(); + voices[i] = v; + v.brr_offset = 1; + } + } + + // Sets output volume, where 1.0 is normal and 2.0 is twice as loud + public void setVolume(double v) { + volume = (int) (v * 0x8000); + } + + // Sets buffer to write samples into + public void setOutput(byte[] out) { + this.out = out; + out_pos = 0; + } + + // Number of samples written into buffer (stereo, so always a multiple of 2) + public int sampleCount() { + return out_pos >> 1; + } + + // Writes to DSP register + public void write(int addr, int data) { + if (addr == r_endx) // always cleared, regardless of data written + data = 0; + + regs[addr] = (byte) data; + + if (addr == r_kon) + new_kon = (byte) data; + } + + // DSP registers + static final int r_mvoll = 0x0C; + static final int r_mvolr = 0x1C; + static final int r_evoll = 0x2C; + static final int r_evolr = 0x3C; + static final int r_kon = 0x4C; + static final int r_koff = 0x5C; + static final int r_flg = 0x6C; + static final int r_endx = 0x7C; + static final int r_efb = 0x0D; + static final int r_pmon = 0x2D; + static final int r_non = 0x3D; + static final int r_eon = 0x4D; + static final int r_dir = 0x5D; + static final int r_esa = 0x6D; + static final int r_edl = 0x7D; + static final int r_fir = 0x0F; + + // Voice registers + static final int v_voll = 0x00; + static final int v_volr = 0x01; + static final int v_pitchl = 0x02; + static final int v_pitchh = 0x03; + static final int v_srcn = 0x04; + static final int v_adsr0 = 0x05; + static final int v_adsr1 = 0x06; + static final int v_gain = 0x07; + static final int v_envx = 0x08; + static final int v_outx = 0x09; + + public static final int register_count = 128; + public final byte[] regs = new byte[register_count]; + + // Runs DSP for sampleCount/32000 of a second + public void run(int sampleCount) { + // locals are faster, and first three are more efficient to access + final byte[] regs = this.regs; + Voice v; + + final byte[] ram = this.ram; + final Rate[] rates = this.rates; + final Voice[] voices = this.voices; + final int flg = regs[r_flg]; + + final int dir = (regs[r_dir] & 0xFF) << 8; + final int slow_gaussian = ((regs[r_pmon] & 0xFF) >> 1) | regs[r_non]; + final Rate noise_rate = rates[flg & 0x1F]; + + // Global volumes + final int volume = (flg & 0x40) == 0 ? this.volume : 0; + final int mvoll = (regs[r_mvoll] * volume) >> 15; + final int mvolr = (regs[r_mvolr] * volume) >> 15; + final int evoll = (regs[r_evoll] * volume) >> 15; + final int evolr = (regs[r_evolr] * volume) >> 15; + + final byte[] out = this.out; + int out_pos = this.out_pos; + final int out_end = out_pos + (sampleCount << 2); + + do { + // KON/KOFF reading + if ((every_other_sample ^= 1) != 0) { + kon = (new_kon &= ~kon); + t_koff = regs[r_koff]; + } + + // run counters + { + int n = counter1.i; + if ((n & 7) == 0) n -= 6 - 1; + counter1.i = n - 1; + } + { + int n = counter2.i; + if ((n & 7) == 0) n -= 6 - 2; + counter2.i = n - 1; + } + { + int n = counter3.i; + if ((n & 7) == 0) n -= 6 - 3; + counter3.i = n - 1; + } + + // Noise + if ((noise_rate.c.i & noise_rate.m) == 0) + lfsr = (lfsr >> 1) ^ (-(lfsr & 2) & 0xC000); + + // Voices + int pmon_input = 0; + int main_out_l = 0; + int main_out_r = 0; + int echo_out_l = 0; + int echo_out_r = 0; + int voice = -1; + do { + v = voices[++voice]; + final int vbit = 1 << voice; + final int v_regs = voice << 4; + + // Pitch + int pitch = (regs[v_regs + v_pitchh] & 0x3F) << 8 | (regs[v_regs + v_pitchl] & 0xFF); + if ((regs[r_pmon] & vbit) != 0) + pitch += ((pmon_input >> 5) * pitch) >> 10; + + int brr_header = ram[v.brr_addr]; + + // KON phases + if (v.kon_delay > 0) { + final int kon_delay = --v.kon_delay; + + // Disable BRR decoding until last three samples + v.interp_pos = (kon_delay & 3) != 0 ? 0x4000 : 0; + + // Get ready to start BRR decoding on next sample + if (kon_delay == 4) { + int addr = dir + ((regs[v_regs + v_srcn] & 0xFF) << 2); + v.brr_addr = (ram[addr + 1] & 0xFF) << 8 | (ram[addr] & 0xFF); + v.brr_offset = 1; + v.buf_pos = 0; + brr_header = 0; // header is ignored on this sample + } + + // Envelope is never run during KON + v.env = 0; + v.hidden_env = 0; + + // Pitch is never added during KON + pitch = 0; + } + + int env; + regs[v_regs + v_envx] = (byte) ((env = v.env) >> 4); + + // Gaussian interpolation + { + int output = 0; + if (env != 0) { + int whole = v.buf_pos + (v.interp_pos >> 12); + int fract = v.interp_pos >> 3 & 0x1FE; + if ((slow_gaussian & vbit) == 0) // 99% + { + // Faster approximation when exact sample value isn't necessary for pitch mod + output = (((gauss[fract] * v.buf[whole] + + gauss[fract + 1] * v.buf[whole + 1] + + gauss[511 - fract] * v.buf[whole + 2] + + gauss[510 - fract] * v.buf[whole + 3]) >> 11) * env) >> 11; + } else { + output = (short) (lfsr << 1); + if ((regs[r_non] & vbit) == 0) { + output = (gauss[fract] * v.buf[whole]) >> 11; + output += (gauss[fract + 1] * v.buf[whole + 1]) >> 11; + output += (gauss[511 - fract] * v.buf[whole + 2]) >> 11; + output = (short) output; + output += (gauss[510 - fract] * v.buf[whole + 3]) >> 11; + + if ((short) output != output) output = (output >> 24) ^ 0x7FFF; // 16-bit clamp + output &= ~1; + } + pmon_input = output = (output * env) >> 11 & ~1; + } + + regs[v_regs + v_outx] = (byte) (output >> 8); + + // Output + int l, r; + main_out_l += (l = output * regs[v_regs + v_voll]); + main_out_r += (r = output * regs[v_regs + v_volr]); + + if ((regs[r_eon] & vbit) != 0) { + echo_out_l += l; + echo_out_r += r; + } + } + } + + // Soft reset or end of sample + if (flg < 0 || (brr_header & 3) == 1) { + v.env_mode = env_release; + env = 0; + } + + if (every_other_sample != 0) { + // KOFF + if ((t_koff & vbit) != 0) + v.env_mode = env_release; + + // KON + if ((kon & vbit) != 0) { + v.kon_delay = 5; + v.env_mode = env_attack; + regs[r_endx] &= ~vbit; + } + } + + // Envelope + if (v.kon_delay == 0) { + if (v.env_mode == env_release) // 97% + { + if ((v.env = (env -= 0x8)) <= 0) { + v.env = 0; + continue; // no BRR decoding for you! + } + } else do // 3% + { + int rate; + int env_data = regs[v_regs + v_adsr1] & 0xFF; + int adsr0; + if ((adsr0 = regs[v_regs + v_adsr0]) < 0) // 97% ADSR + { + if (v.env_mode > env_decay) // 89% + { + // optimized handling + v.hidden_env = (env -= (env >> 8) + 1); + Rate r = rates[env_data & 0x1F]; + if ((r.c.i & r.m) == 0) + v.env = env; + break; + } else if (v.env_mode == env_decay) { + env -= (env >> 8) + 1; + rate = (adsr0 >> 3 & 0x0E) + 0x10; + } else // env_attack + { + rate = ((adsr0 & 0x0F) << 1) + 1; + env += rate < 31 ? 0x20 : 0x400; + } + } else // GAIN + { + int mode; + env_data = regs[v_regs + v_gain] & 0xFF; + mode = env_data >> 5; + if (mode < 4) // direct + { + env = env_data << 4; + rate = 31; + } else { + rate = env_data & 0x1F; + if (mode == 4) // 4: linear decrease + { + env -= 0x20; + } else if (mode < 6) // 5: exponential decrease + { + env -= (env >> 8) + 1; + } else // 6,7: linear increase + { + env += 0x20; + if (mode > 6 && (v.hidden_env < 0 || v.hidden_env >= 0x600)) + env += 0x8 - 0x20; // 7: two-slope linear increase + } + } + } + + // Sustain level + if ((env >> 8) == (env_data >> 5) && v.env_mode == env_decay) + v.env_mode = env_sustain; + + v.hidden_env = env; + + if (env < 0 || env > 0x7FF) { + env = (env < 0 ? 0 : 0x7FF); + if (v.env_mode == env_attack) + v.env_mode = env_decay; + } + + Rate r = rates[rate]; + if ((r.c.i & r.m) == 0) + v.env = env; // nothing else is controlled by the counter + } + while (false); + } + + // Apply pitch + int old_pos; + int interp_pos = ((old_pos = v.interp_pos) & 0x3FFF) + pitch; + if (interp_pos > 0x7FFF) + interp_pos = 0x7FFF; + v.interp_pos = interp_pos; + + // BRR decode if necessary + if (old_pos > 0x4000 - 1) { + // Arrange the four input nybbles in 0xABCD order for easy decoding + int brr_addr = v.brr_addr; + int brr_offset = v.brr_offset; + int nybbles = ram[brr_addr + brr_offset] << 8 | (ram[brr_addr + brr_offset + 1] & 0xFF); + + // Advance read position + final int brr_block_size = 9; + if ((brr_offset += 2) >= brr_block_size) { + // Next BRR block + brr_addr = (brr_addr + brr_block_size) & 0xFFFF; + //assert brr_offset == brr_block_size; + if ((brr_header & 1) != 0) { + int addr = dir + ((regs[v_regs + v_srcn] & 0xFF) << 2); + brr_addr = (ram[addr + 3] & 0xFF) << 8 | (ram[addr + 2] & 0xFF); + if (v.kon_delay == 0) + regs[r_endx] |= vbit; + } + v.brr_addr = brr_addr; + brr_offset = 1; + } + v.brr_offset = brr_offset; + + // Decode + + final int scale = brr_header >> 4 & 0x0F; + final int right_shift = brr_shifts[scale]; + final int left_shift = brr_shifts[scale + 16]; + + final int filter = brr_header & 0x0C; + + // Decode and write to next four samples in circular buffer + int pos = v.buf_pos; + int p1 = v.buf[pos + (brr_buf_size - 1)]; + int p2 = v.buf[pos + (brr_buf_size - 2)] >> 1; + final int end = pos + 4; + do { + // Extract upper nybble and scale appropriately + int s = ((short) nybbles >> right_shift) << left_shift; + nybbles <<= 4; + + // Apply IIR filter (8 is the most commonly used) + if (filter >= 8) { + if (filter == 8) // s += p1 * 0.953125 - p2 * 0.46875 + s += p1 - p2 + (p2 >> 4) + ((p1 * -3) >> 6); + else // s += p1 * 0.8984375 - p2 * 0.40625 + s += p1 - p2 + ((p1 * -13) >> 7) + ((p2 * 3) >> 4); + } else if (filter != 0) // s += p1 * 0.46875 + { + s += (p1 >> 1) + ((-p1) >> 5); + } + p2 = p1 >> 1; + + // Adjust and write sample + if ((short) s != s) s = (s >> 24) ^ 0x7FFF; // 16-bit clamp + v.buf[pos + brr_buf_size] = v.buf[pos] = p1 = (short) (s << 1); + // second copy simplifies wrap-around + } + while (++pos < end); + + if (pos >= brr_buf_size) + pos = 0; + v.buf_pos = pos; + } + } + while (voice < 7); + + // Echo position + int echo_offset; + int echo_ptr = ((regs[r_esa] << 8) + (echo_offset = this.echo_offset)) & 0xFFFF; + if (echo_offset == 0) + echo_length = (regs[r_edl] & 0x0F) << 11; + if ((echo_offset += 4) >= echo_length) + echo_offset = 0; + this.echo_offset = echo_offset; + + // FIR + int echo_hist_pos; + this.echo_hist_pos = echo_hist_pos = (this.echo_hist_pos + 2) & (echo_hist_half - 1); + + int echo_in_l = ram[echo_ptr + 1] << 8 | (ram[echo_ptr] & 0xFF); + echo_hist[echo_hist_pos] = echo_hist[echo_hist_pos + echo_hist_half] = echo_in_l; + + int echo_in_r = ram[echo_ptr + 3] << 8 | (ram[echo_ptr + 2] & 0xFF); + echo_hist[echo_hist_pos + 1] = echo_hist[echo_hist_pos + echo_hist_half + 1] = echo_in_r; + + echo_in_l = regs[r_fir + 0x70] * echo_in_l + + regs[r_fir] * echo_hist[echo_hist_pos + 2] + + regs[r_fir + 0x10] * echo_hist[echo_hist_pos + 4] + + regs[r_fir + 0x20] * echo_hist[echo_hist_pos + 6] + + regs[r_fir + 0x30] * echo_hist[echo_hist_pos + 8] + + regs[r_fir + 0x40] * echo_hist[echo_hist_pos + 10] + + regs[r_fir + 0x50] * echo_hist[echo_hist_pos + 12] + + regs[r_fir + 0x60] * echo_hist[echo_hist_pos + 14]; + + echo_in_r = regs[r_fir + 0x70] * echo_in_r + + regs[r_fir] * echo_hist[echo_hist_pos + 3] + + regs[r_fir + 0x10] * echo_hist[echo_hist_pos + 5] + + regs[r_fir + 0x20] * echo_hist[echo_hist_pos + 7] + + regs[r_fir + 0x30] * echo_hist[echo_hist_pos + 9] + + regs[r_fir + 0x40] * echo_hist[echo_hist_pos + 11] + + regs[r_fir + 0x50] * echo_hist[echo_hist_pos + 13] + + regs[r_fir + 0x60] * echo_hist[echo_hist_pos + 15]; + + // Echo out + if ((flg & 0x20) == 0) { + final int efb = regs[r_efb]; + int l = (echo_out_l >> 7) + ((echo_in_l * efb) >> 14); + if ((short) l != l) l = (l >> 24) ^ 0x7FFF; // 16-bit clamp + ram[echo_ptr] = (byte) l; + ram[echo_ptr + 1] = (byte) (l >> 8); + + int r = (echo_out_r >> 7) + ((echo_in_r * efb) >> 14); + if ((short) r != r) r = (r >> 24) ^ 0x7FFF; // 16-bit clamp + ram[echo_ptr + 2] = (byte) r; + ram[echo_ptr + 3] = (byte) (r >> 8); + } + + // Sound out + int l = (main_out_l * mvoll + echo_in_l * evoll) >> 14; + if ((short) l != l) l = (l >> 24) ^ 0x7FFF; // 16-bit clamp + out[out_pos] = (byte) (l >> 8); + out[out_pos + 1] = (byte) l; + + int r = (main_out_r * mvolr + echo_in_r * evolr) >> 14; + if ((short) r != r) r = (r >> 24) ^ 0x7FFF; // 16-bit clamp + out[out_pos + 2] = (byte) (r >> 8); + out[out_pos + 3] = (byte) r; + } + while ((out_pos += 4) < out_end); + + this.out_pos = out_pos; + } + + public SpcDsp() { + int mask = 4095; + for (int i = 0; i < 32 - 2; i += 3) { + rates[i] = new Rate(counter2, mask); + rates[i + 1] = new Rate(counter1, mask); + rates[i + 2] = new Rate(counter3, mask); + mask >>= 1; + } + rates[0] = new Rate(counter0, 7); + rates[30] = new Rate(counter2, 1); + rates[31] = new Rate(counter2, 0); + + setVolume(2.0); // TODO: let line increase volume, not DSP + } + + static final int env_release = 0; + static final int env_attack = 1; + static final int env_decay = 2; + static final int env_sustain = 3; + + static final int voice_count = 8; + static final int brr_buf_size = 12; + static final int echo_hist_half = 16; + + private static final class Voice { + final int[] buf = new int[12 * 2];// decoded samples (twice the size to simplify wrap handling) + int buf_pos; // place in buffer where next samples will be decoded + int interp_pos; // relative fractional position in sample (0x1000 = 1.0) + int brr_addr; // address of current BRR block + int brr_offset; // current decoding offset in BRR block + int kon_delay; // KON delay/current setup phase + int env_mode; + int env; // current envelope level + int hidden_env; // used by GAIN mode 7, very obscure quirk + } + + private static final class Counter { + int i; + } + + private static final class Rate { + Counter c; + int m; + + Rate(Counter c, int m) { + this.c = c; + this.m = m; + } + } + + final Counter counter0 = new Counter(); + final Counter counter1 = new Counter(); + final Counter counter2 = new Counter(); + final Counter counter3 = new Counter(); + final Rate[] rates = new Rate[32]; + final Voice[] voices = new Voice[voice_count]; + final int[] echo_hist = new int[echo_hist_half * 2]; + + int echo_hist_pos; + int every_other_sample; // toggles every sample + int kon; // KON value when last checked + int lfsr; + int echo_offset; // offset from ESA in echo buffer + int echo_length; // number of bytes that echo_offset will stop at + int new_kon; + int t_koff; + int volume; + byte[] ram; // 64K shared RAM between DSP and SMP + byte[] out; // sample output + int out_pos; + + // 0: >>1 1: <<0 2: <<1 ... 12: <<11 13-15: >>4 <<11 + static final int[] brr_shifts = { + 13, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 16, 16, 16, + 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 11, 11 + }; + + static final int[] gauss = + { + 370, 1305, 366, 1305, 362, 1304, 358, 1304, 354, 1304, 351, 1304, 347, 1304, 343, 1303, + 339, 1303, 336, 1303, 332, 1302, 328, 1302, 325, 1301, 321, 1300, 318, 1300, 314, 1299, + 311, 1298, 307, 1297, 304, 1297, 300, 1296, 297, 1295, 293, 1294, 290, 1293, 286, 1292, + 283, 1291, 280, 1290, 276, 1288, 273, 1287, 270, 1286, 267, 1284, 263, 1283, 260, 1282, + 257, 1280, 254, 1279, 251, 1277, 248, 1275, 245, 1274, 242, 1272, 239, 1270, 236, 1269, + 233, 1267, 230, 1265, 227, 1263, 224, 1261, 221, 1259, 218, 1257, 215, 1255, 212, 1253, + 210, 1251, 207, 1248, 204, 1246, 201, 1244, 199, 1241, 196, 1239, 193, 1237, 191, 1234, + 188, 1232, 186, 1229, 183, 1227, 180, 1224, 178, 1221, 175, 1219, 173, 1216, 171, 1213, + 168, 1210, 166, 1207, 163, 1205, 161, 1202, 159, 1199, 156, 1196, 154, 1193, 152, 1190, + 150, 1186, 147, 1183, 145, 1180, 143, 1177, 141, 1174, 139, 1170, 137, 1167, 134, 1164, + 132, 1160, 130, 1157, 128, 1153, 126, 1150, 124, 1146, 122, 1143, 120, 1139, 118, 1136, + 117, 1132, 115, 1128, 113, 1125, 111, 1121, 109, 1117, 107, 1113, 106, 1109, 104, 1106, + 102, 1102, 100, 1098, 99, 1094, 97, 1090, 95, 1086, 94, 1082, 92, 1078, 90, 1074, + 89, 1070, 87, 1066, 86, 1061, 84, 1057, 83, 1053, 81, 1049, 80, 1045, 78, 1040, + 77, 1036, 76, 1032, 74, 1027, 73, 1023, 71, 1019, 70, 1014, 69, 1010, 67, 1005, + 66, 1001, 65, 997, 64, 992, 62, 988, 61, 983, 60, 978, 59, 974, 58, 969, + 56, 965, 55, 960, 54, 955, 53, 951, 52, 946, 51, 941, 50, 937, 49, 932, + 48, 927, 47, 923, 46, 918, 45, 913, 44, 908, 43, 904, 42, 899, 41, 894, + 40, 889, 39, 884, 38, 880, 37, 875, 36, 870, 36, 865, 35, 860, 34, 855, + 33, 851, 32, 846, 32, 841, 31, 836, 30, 831, 29, 826, 29, 821, 28, 816, + 27, 811, 27, 806, 26, 802, 25, 797, 24, 792, 24, 787, 23, 782, 23, 777, + 22, 772, 21, 767, 21, 762, 20, 757, 20, 752, 19, 747, 19, 742, 18, 737, + 17, 732, 17, 728, 16, 723, 16, 718, 15, 713, 15, 708, 15, 703, 14, 698, + 14, 693, 13, 688, 13, 683, 12, 678, 12, 674, 11, 669, 11, 664, 11, 659, + 10, 654, 10, 649, 10, 644, 9, 640, 9, 635, 9, 630, 8, 625, 8, 620, + 8, 615, 7, 611, 7, 606, 7, 601, 6, 596, 6, 592, 6, 587, 6, 582, + 5, 577, 5, 573, 5, 568, 5, 563, 4, 559, 4, 554, 4, 550, 4, 545, + 4, 540, 3, 536, 3, 531, 3, 527, 3, 522, 3, 517, 2, 513, 2, 508, + 2, 504, 2, 499, 2, 495, 2, 491, 2, 486, 1, 482, 1, 477, 1, 473, + 1, 469, 1, 464, 1, 460, 1, 456, 1, 451, 1, 447, 1, 443, 1, 439, + 0, 434, 0, 430, 0, 426, 0, 422, 0, 418, 0, 414, 0, 410, 0, 405, + 0, 401, 0, 397, 0, 393, 0, 389, 0, 385, 0, 381, 0, 378, 0, 374, + }; +} diff --git a/src/uk/me/fantastic/retro/music/gme/SpcEmu.java b/src/uk/me/fantastic/retro/music/gme/SpcEmu.java new file mode 100755 index 0000000..cbba963 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/SpcEmu.java @@ -0,0 +1,382 @@ +package uk.me.fantastic.retro.music.gme;// Nintendo SPC music file player +// http://www.slack.net/~ant/ + +/* Copyright (C) 2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +final class SpcEmu extends SpcCpu { + private static final class Timer { + int time; // time of next event + int prescaler; + int period; + int divider; + int enabled; + int counter; + } + + static final int ramSize = 0x10000; + static final int ramPadSize = 0x100; + + // header offsets + static final int cpuStateOff = 0x25; + static final int ramOff = 0x100; + static final int dspStateOff = 0x10100; + + static final int romAddr = 0xFFC0; + + // SMP registers + static final int testReg = 0x0; + static final int controlReg = 0x1; + static final int dspaddrReg = 0x2; + static final int dspdataReg = 0x3; + static final int cpuio0Reg = 0x4; + static final int cpuio1Reg = 0x5; + static final int cpuio2Reg = 0x6; + static final int cpuio3Reg = 0x7; + static final int f8Reg = 0x8; + static final int f9Reg = 0x9; + static final int t0targetReg = 0xA; + static final int t1targetReg = 0xB; + static final int t2targetReg = 0xC; + static final int t0outReg = 0xD; + static final int t1outReg = 0xE; + static final int t2outReg = 0xF; + + static final int romSize = 0x40; + static final int timerCount = 3; + static final int regCount = 0x10; + + int dspTime; + int romEnabled; + byte[] spc; + final byte[] rom = new byte[romSize]; + final byte[] hiRam = new byte[romSize]; + final byte[] ram = new byte[ramSize + ramPadSize]; + final int[] regs = new int[regCount]; + final int[] regsIn = new int[regCount]; + final SpcDsp dsp = new SpcDsp(); + final Timer[] timers = new Timer[timerCount]; + + protected int setSampleRate_(int rate) { + return 32000; + } + + protected int loadFile_(byte[] in) { + if (!isHeader(in, "SNES-SPC700 Sound File Data")) + error("Not an SPC file"); + + spc = in; + + // almost no SPC music rely on more than last two bytes of boot ROM + java.util.Arrays.fill(rom, 0, romSize, (byte) 0); + rom[0x3E] = (byte) 0xFF; + rom[0x3F] = (byte) 0xC0; + + // TODO: use SPC file's copy of ROM, if present? + + return 1; + } + + // Runs timer to present. Time must be >= t.time. + static void runTimer_(Timer t, int time) { + int elapsed = ((time - t.time) >> t.prescaler) + 1; + t.time += elapsed << t.prescaler; + + if (t.enabled != 0) { + int remain = ((t.period - t.divider - 1) & 0xFF) + 1; + int divider = t.divider + elapsed; + int over; + if ((over = elapsed - remain) >= 0) { + int n = over / t.period; + t.counter = (t.counter + 1 + n) & 0x0F; + divider = over - n * t.period; + } + t.divider = divider & 0xFF; + } + } + + // Runs timer to present if it's not already + static void runTimer(Timer t, int time) { + if (time >= t.time) + runTimer_(t, time); + } + + // Enables/disables boot ROM by swapping it out of RAM + private void enableRom(int enable) { + if (romEnabled != enable) { + romEnabled = enable; + if (enable != 0) { + System.arraycopy(ram, romAddr, hiRam, 0, romSize); + System.arraycopy(rom, 0, ram, romAddr, romSize); + } else { + System.arraycopy(hiRam, 0, ram, romAddr, romSize); + } + // TODO: ROM can still get overwritten when DSP writes to echo buffer + } + } + + public void startTrack(int track) { + super.startTrack(track); + + time = 0; + dspTime = 32; + + // RAM + java.util.Arrays.fill(ram, ramSize, ram.length, (byte) 0xFF); + System.arraycopy(spc, ramOff, ram, 0, ramSize); + + dsp.init(ram, spc, dspStateOff); + + // CPU + reset(ram); + pc = (spc[cpuStateOff + 1] & 0xFF) << 8 | (spc[cpuStateOff] & 0xFF); + a = spc[cpuStateOff + 2] & 0xFF; + x = spc[cpuStateOff + 3] & 0xFF; + y = spc[cpuStateOff + 4] & 0xFF; + sp = spc[cpuStateOff + 6] & 0xFF; + setPsw(spc[cpuStateOff + 5] & 0xFF); + + // SMP registers + for (int i = 0; i < regCount; i++) + regsIn[i] = regs[i] = ram[0xF0 + i] & 0xFF; + + regsIn[testReg] = 0; // these always read back as 0 + regsIn[controlReg] = 0; + regsIn[t0targetReg] = 0; + regsIn[t1targetReg] = 0; + regsIn[t2targetReg] = 0; + + // ROM + romEnabled = 0; + enableRom(regs[controlReg] & 0x80); + + // Timers + for (int i = 0; i < timerCount; i++) { + Timer t = timers[i] = new Timer(); + t.time = 1; + t.divider = 0; + t.period = ((regs[t0targetReg + i] - 1) & 0xFF) + 1; + t.enabled = regs[controlReg] >> i & 1; + t.counter = regsIn[t0outReg + i] & 0x0F; + } + + timers[2].prescaler = 4; + timers[1].prescaler = 4 + 3; + timers[0].prescaler = 4 + 3; + + // Clear echo + if ((dsp.regs[dsp.r_flg] & 0x20) == 0) { + int addr = (dsp.regs[dsp.r_esa] & 0xFF) << 8; + int end = addr + ((dsp.regs[dsp.r_edl] & 0x0F) << 11); + if (end > ramSize) + end = ramSize; + java.util.Arrays.fill(ram, addr, end, (byte) 0xFF); + } + } + + protected int play_(byte out[], int count) { + dsp.setOutput(out); + + // Run for count/2*32 clocks + extra to get DSP time half-way between samples, + // since CPU might run for slightly less than requested + int clockCount = count * (32 / 2) + 16 - ((time - dspTime) & 31); + time -= clockCount; + dspTime -= clockCount; + timers[0].time -= clockCount; + timers[1].time -= clockCount; + timers[2].time -= clockCount; + runCpu(); + + if (time < 0) // emulation error + { + logError(); + return 0; + } + + // Catch up to CPU + runTimer(timers[0], time); + runTimer(timers[1], time); + runTimer(timers[2], time); + + // Run DSP to present + int delta; + if ((delta = time - dspTime) >= 0) { + delta = (delta >> 5) + 1; + dspTime += delta << 5; + dsp.run(delta); + } + + assert dsp.sampleCount() == count; + return dsp.sampleCount(); + } + + // Writes to SMP register + private void writeReg(int addr, int data) { + switch (addr) { + case t0targetReg: + case t1targetReg: + case t2targetReg: { + Timer t = timers[addr - t0targetReg]; + int period = ((data - 1) & 0xFF) + 1; + if (t.period != period) { + runTimer(t, time); + t.period = period; + } + break; + } + + case t0outReg: + case t1outReg: + case t2outReg: + // TODO + //if ( data < no_read_before_write / 2 ) + // run_timer( &m.timers [addr - t0outReg], time - 1 )->counter = 0; + break; + + // Registers that act like RAM + case 0x8: + case 0x9: + regsIn[addr] = data; + break; + + case testReg: + //if ( (uint8_t) data != 0x0A ) + // dprintf( "SPC wrote to test register\n" ); + break; + + case controlReg: + // port clears + if ((data & 0x10) != 0) { + regsIn[cpuio0Reg] = 0; + regsIn[cpuio1Reg] = 0; + } + if ((data & 0x20) != 0) { + regsIn[cpuio2Reg] = 0; + regsIn[cpuio3Reg] = 0; + } + + // timers + for (int i = 0; i < timerCount; i++) { + Timer t = timers[i]; + int enabled = data >> i & 1; + if (t.enabled != enabled) { + runTimer(t, time); + t.enabled = enabled; + if (enabled != 0) { + t.divider = 0; + t.counter = 0; + } + } + } + enableRom(data & 0x80); + break; + } + } + + public final void cpuWrite(int addr, int data) { + // RAM + ram[addr] = (byte) data; + if ((addr -= 0xF0) >= 0) // 64% + { + // $F0-$FF + if (addr < regCount) // 87% + { + regs[addr] = (data &= 0xFF); + + // Ports + + // Registers other than $F2 and $F4-$F7 + //if ( addr != 2 && addr != 4 && addr != 5 && addr != 6 && addr != 7 ) + if ((~0x2F000000 << addr) < 0) // 36% + { + if (addr == dspdataReg) // 99% + { + // Run DSP to present + int delta; + if ((delta = time - dspTime) >= 0) // 95% + { + delta = (delta >> 5) + 1; + dspTime += delta << 5; + dsp.run(delta); + } + + int dspaddr; + if ((dspaddr = regs[dspaddrReg]) <= 0x7F) + dsp.write(dspaddr, data); + } else { + writeReg(addr, data); + } + } + } + // IPL ROM area or address wrapped around + else if ((addr -= romAddr - 0xF0) >= 0) // 1% in IPL ROM area or address wrapped around + { + if (addr < romSize) { + hiRam[addr] = (byte) data; + if (romEnabled != 0) + ram[addr + romAddr] = rom[addr]; // restore overwritten ROM + } else { + if (debug) assert ram[addr + romAddr] == (byte) data; + ram[addr + romAddr] = (byte) 0xFF; // restore overwritten padding + cpuWrite(data, addr - (ramSize - romAddr)); + } + } + } + } + + public final int cpuRead(int addr) { + // Low RAM + if (addr < 0xF0) // 60% + return ram[addr] & 0xFF; + + // Timers + if ((addr ^= 0xFF) < timerCount) // 68% + { + Timer t = timers[2 - addr]; // TODO: reorder timers to eliminate 2- + if (time >= t.time) + runTimer_(t, time); + int result = t.counter; + t.counter = 0; + return result; + } + + // Other registers + if ((addr ^= 0xFF) <= 0xFF) // 9% + { + if (addr == dspaddrReg + 0xF0) + return regs[dspaddrReg]; + + if (addr == dspdataReg + 0xF0) { + // DSP + + // Run to present + int delta; + if ((delta = time - dspTime) >= 0) // 1% + { + delta = (delta >> 5) + 1; + dspTime += delta << 5; + dsp.run(delta); + } + + return dsp.regs[regs[dspaddrReg] & 0x7F] & 0xFF; + } + + return regsIn[addr - 0xF0]; + } + + // RAM + if (addr <= 0xFFFF) // 99% + return ram[addr] & 0xFF; + + // Address wrapped around + return cpuRead(addr - 0x10000); + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/VGMPlayer.java b/src/uk/me/fantastic/retro/music/gme/VGMPlayer.java new file mode 100755 index 0000000..106034f --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/VGMPlayer.java @@ -0,0 +1,261 @@ +package uk.me.fantastic.retro.music.gme;// Video game music player that runs emulator and plays through speaker +// http://www.slack.net/~ant/ + +/* Load a music file into player, then start a track. Volume can be +adjusted, track can be paused and resumed, a new track can be started, +or a new file can be loaded at any time. + +The file is specified as an HTTP address and optional filename to use +if it's a ZIP archive. To avoid loading file more than necessary over +HTTP, the most recently loaded file is kept in memory and a load request +for the same URL is eliminated. This allows a web page to switch between +several tracks in a ZIP archive or of a multi-track music file, without +having to keep track of whether the file was already loaded. */ + +import javax.sound.sampled.*; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/* Copyright (C) 2007-2008 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +class EmuPlayer implements Runnable { + // Number of tracks + public int getTrackCount() { + return emu.trackCount(); + } + + // Starts new track playing, where 0 is the first track. + // After time seconds, the track starts fading. + public void startTrack(int track, int time) throws Exception { + pause(); + if (line != null) + line.flush(); + emu.startTrack(track); + // emu.setFade( time, 6 ); + play(); + } + + // Currently playing track + public int getCurrentTrack() { + return emu.currentTrack(); + } + + // Number of seconds played since last startTrack() call + public int getCurrentTime() { + return (emu == null ? 0 : emu.currentTime()); + } + + // Sets playback volume, where 1.0 is normal, 2.0 is twice as loud. + // Can be changed while track is playing. + public void setVolume(double v) { + volume_ = v; + + if (line != null) { + FloatControl mg = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN); + if (mg != null) + mg.setValue((float) (Math.log(v) / Math.log(10.0) * 20.0)); + } + } + + // Current playback volume + public double getVolume() { + return volume_; + } + + // Pauses if track was playing. + public void pause() throws Exception { + if (thread != null) { + playing_ = false; + thread.join(); + thread = null; + } + } + + // True if track is currently playing + public boolean isPlaying() { + return playing_; + } + + // Resumes playback where it was paused + public void play() throws Exception { + if (line == null) { + line = (SourceDataLine) AudioSystem.getLine(lineInfo); + line.open(audioFormat); + setVolume(volume_); + } + thread = new Thread(this); + playing_ = true; + thread.start(); + // thread.join(); + } + + // Stops playback and closes audio + public void stop() throws Exception { + pause(); + + if (line != null) { + line.close(); + line = null; + } + } + + // Called periodically when a track is playing + protected void idle() { + } + +// private + + // Sets music emulator to get samples from + void setEmu(MusicEmu emu, int sampleRate) throws Exception { + stop(); + this.emu = emu; + if (emu != null && line == null && this.sampleRate != sampleRate) { + audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, + sampleRate, 16, 2, 4, sampleRate, true); + lineInfo = new DataLine.Info(SourceDataLine.class, audioFormat); + this.sampleRate = sampleRate; + } + } + + private int sampleRate = 0; + AudioFormat audioFormat; + DataLine.Info lineInfo; + MusicEmu emu; + Thread thread; + volatile boolean playing_; + SourceDataLine line; + double volume_ = 1.0; + + public void run() { + line.start(); + + // play track until stop signal + byte[] buf = new byte[8192]; + while (playing_ && !emu.trackEnded()) { + int count = emu.play(buf, buf.length / 2); + line.write(buf, 0, count * 2); + idle(); + } + + playing_ = false; + line.stop(); + } +} + +class VGMPlayer extends EmuPlayer { + int sampleRate; + + public VGMPlayer(int sampleRate) { + this.sampleRate = sampleRate; + } + + VGMPlayer() { + this(44100); + + } + + // Stops playback and loads file from given URL (HTTP only). + // If it's an archive (.zip) then path specifies the file within + // the archive. + public void loadFile(String url, String path) throws Exception { + stop(); + + if (!loadedUrl.equals(url) || !loadedPath.equals(path)) { + byte[] data = readFile(url, path); + + String name = url.toUpperCase(); + if (name.endsWith(".ZIP")) + name = path.toUpperCase(); + + if (name.endsWith(".GZ")) + name = name.substring(0, name.length() - 3); + + MusicEmu emu = createEmu(name); + if (emu == null) + return; // TODO: throw exception? + int actualSampleRate = emu.setSampleRate(sampleRate); + emu.loadFile(data); + + // now that new emulator is ready, replace old one + setEmu(emu, actualSampleRate); + loadedUrl = url; + loadedPath = path; + } + } + + // Stops and closes current file and unloads things from memory + void closeFile() throws Exception { + stop(); + setEmu(null, 0); + archiveUrl = ""; + archiveData = null; + loadedUrl = ""; + loadedPath = ""; + } + +// private + + String loadedUrl = ""; // URL and path of file loaded into emulator + String loadedPath = ""; + + String archiveUrl = ""; // URL of (ZIP) file cached in archiveData + byte[] archiveData; + + // Creates appropriate emulator for given filename + MusicEmu createEmu(String name) { + if (name.endsWith(".VGM") || name.endsWith(".VGZ")) + return new VgmEmu(); + + if (name.endsWith(".GBS")) + return new GbsEmu(); + + if (name.endsWith(".NSF")) { + System.out.println("nsf emu"); + return new NsfEmu(); + } + + if (name.endsWith(".SPC")) + return new SpcEmu(); + + return null; + } + + // Loads given URL and file within archive, and caches archive for future access + byte[] readFile(String url, String path) throws Exception { + InputStream in = null; + String name = url.toUpperCase(); + if (!name.endsWith(".ZIP")) { + archiveData = null; // dump previously cached ZIP file + archiveUrl = ""; + + in = DataReader.openHttp(url); + System.out.println("!zip Load " + url); + } else { + if (!archiveUrl.equals(url)) { + archiveData = DataReader.loadData(DataReader.openHttp(url)); + archiveUrl = url; + System.out.println("zip Load " + url); + } + + in = DataReader.openZip(new ByteArrayInputStream(archiveData), path); + name = path.toUpperCase(); + //System.out.println( "Unzip " + url ); + } + + if (name.endsWith(".GZ") || name.endsWith(".VGZ")) { + in = DataReader.openGZIP(in); + System.out.println("vgz"); + } + + return DataReader.loadData(in); + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/VgmEmu.java b/src/uk/me/fantastic/retro/music/gme/VgmEmu.java new file mode 100755 index 0000000..1f356f4 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/VgmEmu.java @@ -0,0 +1,275 @@ +package uk.me.fantastic.retro.music.gme;// Sega Master System, BBC Micro VGM music file emulator +// http://www.slack.net/~ant/ + +/* Copyright (C) 2003-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +final class VgmEmu extends ClassicEmu { + protected int loadFile_(byte[] data) { + if (!isHeader(data, "Vgm ")) + error("Not a VGM file"); + + // TODO: use custom noise taps if present + + // Data and loop + this.data = data; + loopBegin = getLE32(data, 28) + 28; + if (loopBegin <= 28) { + loopBegin = data.length; + } else if (data[data.length - 1] != cmd_end) { + data = DataReader.resize(data, data.length + 1); + data[data.length - 1] = cmd_end; + } + + // PSG clock rate + int clockRate = getLE32(data, 0x0C); + if (clockRate == 0) + clockRate = 3579545; + psgFactor = (int) ((float) psgTimeUnit / vgmRate * clockRate + 0.5); + + // FM clock rate + fm_clock_rate = getLE32(data, 0x2C); + fm = null; + if (fm_clock_rate != 0) { + fm = new YM2612(); + buf.setVolume(0.7); + fm.init(fm_clock_rate, sampleRate()); + } else { + buf.setVolume(1.0); + } + + setClockRate(clockRate); + apu.setOutput(buf.center(), buf.left(), buf.right()); + + return 1; + } + +// private + + static final int vgmRate = 44100; + static final int psgTimeBits = 12; + static final int psgTimeUnit = 1 << psgTimeBits; + + final SmsApu apu = new SmsApu(); + YM2612 fm; + int fm_clock_rate; + int pos; + byte[] data; + int delay; + int psgFactor; + int loopBegin; + final int[] fm_buf_lr = new int[48000 / 10 * 2]; + int fm_pos; + int dac_disabled; // -1 if disabled + int pcm_data; + int pcm_pos; + int dac_amp; + + static final int cmd_gg_stereo = 0x4F; + static final int cmd_psg = 0x50; + static final int cmd_ym2612_port0 = 0x52; + static final int cmd_ym2612_port1 = 0x53; + static final int cmd_delay = 0x61; + static final int cmd_delay_735 = 0x62; + static final int cmd_delay_882 = 0x63; + static final int cmd_end = 0x66; + static final int cmd_data_block = 0x67; + static final int cmd_short_delay = 0x70; + static final int cmd_pcm_delay = 0x80; + static final int cmd_pcm_seek = 0xE0; + static final int ym2612_dac_port = 0x2A; + static final int pcm_block_type = 0x00; + + public void startTrack(int track) { + super.startTrack(track); + + pos = 0x40; + delay = 0; + pcm_data = pos; + pcm_pos = pos; + dac_amp = -1; + + apu.reset(); + if (fm != null) + fm.reset(); + } + + private int toPSGTime(int vgmTime) { + return (vgmTime * psgFactor + psgTimeUnit / 2) >> psgTimeBits; + } + + private int toFMTime(int vgmTime) { + return countSamples(toPSGTime(vgmTime)); + } + + private void runFM(int vgmTime) { + int count = toFMTime(vgmTime) - fm_pos; + if (count > 0) { + fm.update(fm_buf_lr, fm_pos, count); + fm_pos += count; + } + } + + private void write_pcm(int vgmTime, int amp) { + int blip_time = toPSGTime(vgmTime); + int old = dac_amp; + int delta = amp - old; + dac_amp = amp; + if (old >= 0) // first write is ignored, to avoid click + buf.center().addDelta(blip_time, delta * 300); + else + dac_amp |= dac_disabled; + } + + protected int runMsec(int msec) { + final int duration = vgmRate / 100 * msec / 10; + + { + int sampleCount = toFMTime(duration); + java.util.Arrays.fill(fm_buf_lr, 0, sampleCount * 2, 0); + } + fm_pos = 0; + + int time = delay; + while (time < duration) { + int cmd = cmd_end; + if (pos < data.length) + cmd = data[pos++] & 0xFF; + switch (cmd) { + case cmd_end: + pos = loopBegin; + break; + + case cmd_delay_735: + time += 735; + break; + + case cmd_delay_882: + time += 882; + break; + + case cmd_gg_stereo: + apu.writeGG(toPSGTime(time), data[pos++] & 0xFF); + break; + + case cmd_psg: + apu.writeData(toPSGTime(time), data[pos++] & 0xFF); + break; + + case cmd_ym2612_port0: + if (fm != null) { + int port = data[pos++] & 0xFF; + int val = data[pos++] & 0xFF; + if (port == ym2612_dac_port) { + write_pcm(time, val); + } else { + if (port == 0x2B) { + dac_disabled = (val >> 7 & 1) - 1; + dac_amp |= dac_disabled; + } + runFM(time); + fm.write0(port, val); + } + } + break; + + case cmd_ym2612_port1: + if (fm != null) { + runFM(time); + int port = data[pos++] & 0xFF; + fm.write1(port, data[pos++] & 0xFF); + } + break; + + case cmd_delay: + time += (data[pos + 1] & 0xFF) * 0x100 + (data[pos] & 0xFF); + pos += 2; + break; + + case cmd_data_block: + if (data[pos++] != cmd_end) + logError(); + int type = data[pos++]; + long size = getLE32(data, pos); + pos += 4; + if (type == pcm_block_type) + pcm_data = pos; + pos += size; + break; + + case cmd_pcm_seek: + pcm_pos = pcm_data + getLE32(data, pos); + pos += 4; + break; + + default: + switch (cmd & 0xF0) { + case cmd_pcm_delay: + write_pcm(time, data[pcm_pos++] & 0xFF); + time += cmd & 0x0F; + break; + + case cmd_short_delay: + time += (cmd & 0x0F) + 1; + break; + + case 0x50: + pos += 2; + break; + + default: + logError(); + break; + } + } + } + + if (fm != null) + runFM(duration); + + int endTime = toPSGTime(duration); + delay = time - duration; + apu.endFrame(endTime); + if (pos >= data.length) { + setTrackEnded(); + if (pos > data.length) { + pos = data.length; + logError(); // went past end + } + } + + fm_pos = 0; + + return endTime; + } + + protected void mixSamples(byte[] out, int out_off, int count) { + if (fm == null) + return; + + out_off *= 2; + int in_off = fm_pos; + + while (--count >= 0) { + int s = (out[out_off] << 8) + (out[out_off + 1] & 0xFF); + s = (s >> 2) + fm_buf_lr[in_off]; + in_off++; + if ((short) s != s) + s = (s >> 31) ^ 0x7FFF; + out[out_off] = (byte) (s >> 8); + out_off++; + out[out_off] = (byte) s; + out_off++; + } + + fm_pos = in_off; + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/YM2612.java b/src/uk/me/fantastic/retro/music/gme/YM2612.java new file mode 100755 index 0000000..9df9715 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/YM2612.java @@ -0,0 +1,1152 @@ +package uk.me.fantastic.retro.music.gme; + +/** + * Test port of Gens YM2612 core. + * Stephan Dittrich, 2005 + */ +public final class YM2612 { + static final int NULL_RATE_SIZE = 32; + + // YM2612 Hardware + private final class cSlot { + int[] DT; + int MUL; + int TL; + int TLL; + int SLL; + int KSR_S; + int KSR; + int SEG; + int AR; + int DR; + int SR; + int RR; + int Fcnt; + int Finc; + int Ecurp; + int Ecnt; + int Einc; + int Ecmp; + int EincA; + int EincD; + int EincS; + int EincR; + int INd; + int ChgEnM; + int AMS; + int AMSon; + } + + ; + + private final class cChannel { + final int[] S0_OUT = new int[4]; + int Old_OUTd; + int OUTd; + int LEFT; + int RIGHT; + int ALGO; + int FB; + int FMS; + int AMS; + final int[] FNUM = new int[4]; + final int[] FOCT = new int[4]; + final int[] KC = new int[4]; + final cSlot[] SLOT = new cSlot[4]; + int FFlag; + + public cChannel() { + for (int i = 0; i < 4; i++) SLOT[i] = new cSlot(); + } + } + + ; + + private final class cYM2612 { + int Clock; + int Rate; + int TimerBase; + int Status; + int LFOcnt; + int LFOinc; + int TimerA; + int TimerAL; + int TimerAcnt; + int TimerB; + int TimerBL; + int TimerBcnt; + int Mode; + int DAC; + double Frequency; + long Inter_Cnt; // UINT + long Inter_Step; // UINT + final cChannel[] CHANNEL = new cChannel[6]; + final int[][] REG = new int[2][0x100]; + + public cYM2612() { + for (int i = 0; i < 6; i++) CHANNEL[i] = new cChannel(); + } + } + + ; + + // Constants ( taken from MAME YM2612 core ) + private static final int UPD_SIZE = 4000; + private static final int OUTP_BITS = 16; + private static final double PI = Math.PI; + + private static final int ATTACK = 0; + private static final int DECAY = 1; + private static final int SUSTAIN = 2; + private static final int RELEASE = 3; + + private static final int SIN_HBITS = 12; + private static final int SIN_LBITS = ((26 - SIN_HBITS) <= 16) ? (26 - SIN_HBITS) : 16; + + private static final int ENV_HBITS = 12; + private static final int ENV_LBITS = (28 - ENV_HBITS); + + private static final int LFO_HBITS = 10; + private static final int LFO_LBITS = (28 - LFO_HBITS); + + private static final int SINLEN = (1 << SIN_HBITS); + private static final int ENVLEN = (1 << ENV_HBITS); + private static final int LFOLEN = (1 << LFO_HBITS); + + private static final int TLLEN = (ENVLEN * 3); + + private static final int SIN_MSK = (SINLEN - 1); + private static final int ENV_MSK = (ENVLEN - 1); + private static final int LFO_MSK = (LFOLEN - 1); + + private static final double ENV_STEP = (96.0 / ENVLEN); + + private static final int ENV_ATTACK = ((ENVLEN * 0) << ENV_LBITS); + private static final int ENV_DECAY = ((ENVLEN * 1) << ENV_LBITS); + private static final int ENV_END = ((ENVLEN * 2) << ENV_LBITS); + + private static final int MAX_OUT_BITS = (SIN_HBITS + SIN_LBITS + 2); + private static final int MAX_OUT = ((1 << MAX_OUT_BITS) - 1); + + private static final int OUT_BITS = (OUTP_BITS - 2); + private static final int FINAL_SHFT = (MAX_OUT_BITS - OUT_BITS) + 1; + private static final int LIMIT_CH_OUT = ((int) (((1 << OUT_BITS) * 1.5) - 1)); + + private static final int PG_CUT_OFF = ((int) (78.0 / ENV_STEP)); +// private static final int ENV_CUT_OFF = ((int) (68.0 / ENV_STEP)); + + private static final int AR_RATE = 399128; + private static final int DR_RATE = 5514396; + + private static final int LFO_FMS_LBITS = 9; + private static final int LFO_FMS_BASE = ((int) (0.05946309436 * 0.0338 * (double) (1 << LFO_FMS_LBITS))); + + private static final int S0 = 0; + private static final int S1 = 2; + private static final int S2 = 1; + private static final int S3 = 3; + + + private static final int[] DT_DEF_TAB = { + // FD = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + + // FD = 1 + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, + 2, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 8, 8, 8, + + // FD = 2 + 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, + 5, 6, 6, 7, 8, 8, 9, 10, 11, 12, 13, 14, 16, 16, 16, 16, + + // FD = 3 + 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, + 8, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 22, 22, 22 + }; + + private static final int[] FKEY_TAB = { + 0, 0, 0, 0, + 0, 0, 0, 1, + 2, 3, 3, 3, + 3, 3, 3, 3 + }; + + private static final int[] LFO_AMS_TAB = { + 31, 4, 1, 0 + }; + + private static final int[] LFO_FMS_TAB = { + LFO_FMS_BASE * 0, LFO_FMS_BASE * 1, + LFO_FMS_BASE * 2, LFO_FMS_BASE * 3, + LFO_FMS_BASE * 4, LFO_FMS_BASE * 6, + LFO_FMS_BASE * 12, LFO_FMS_BASE * 24 + }; + + // Variables + private final int[] SIN_TAB = new int[SINLEN]; + private final int[] TL_TAB = new int[TLLEN * 2]; + private final int[] ENV_TAB = new int[2 * ENVLEN + 8]; // uint + private final int[] DECAY_TO_ATTACK = new int[ENVLEN]; // uint + private final int[] FINC_TAB = new int[2048]; // uint + static final int AR_NULL_RATE = 128; + private final int[] AR_TAB = new int[AR_NULL_RATE + NULL_RATE_SIZE]; // uint + static final int DR_NULL_RATE = 96; + private final int[] DR_TAB = new int[DR_NULL_RATE + NULL_RATE_SIZE]; // uint + private final int[][] DT_TAB = new int[8][32]; // uint + private final int[] SL_TAB = new int[16]; // uint + private final int[] LFO_ENV_TAB = new int[LFOLEN]; + private final int[] LFO_FREQ_TAB = new int[LFOLEN]; + private final int[] LFO_ENV_UP = new int[UPD_SIZE]; + private final int[] LFO_FREQ_UP = new int[UPD_SIZE]; + private final int[] LFO_INC_TAB = new int[8]; + private int in0, in1, in2, in3; + private int en0, en1, en2, en3; + private int int_cnt; + + // Emultaion State + private boolean EnableSSGEG = false; + + private static final int MAIN_SHIFT = FINAL_SHFT; + + int YM2612_Clock; + int YM2612_Rate; + int YM2612_TimerBase; + int YM2612_Status; + int YM2612_LFOcnt; + int YM2612_LFOinc; + int YM2612_TimerA; + int YM2612_TimerAL; + int YM2612_TimerAcnt; + int YM2612_TimerB; + int YM2612_TimerBL; + int YM2612_TimerBcnt; + int YM2612_Mode; + int YM2612_DAC; + double YM2612_Frequency; + long YM2612_Inter_Cnt; // UINT + long YM2612_Inter_Step; // UINT + final cChannel[] YM2612_CHANNEL = new cChannel[6]; + final int[][] YM2612_REG = new int[2][0x100]; + + /** + * Creates a new instance of YM2612 + */ + public YM2612() { + for (int i = 0; i < 6; i++) YM2612_CHANNEL[i] = new cChannel(); + } + + // YM2612 Emulation Methods + + /*********************************************** + * + * Public Access + * + ***********************************************/ + + static private double log10(double x) { + return Math.log(x) / Math.log(10.0); + } + + public final int init(int Clock, int Rate) { + int i, j; + double x; + + if ((Rate == 0) || (Clock == 0)) return 1; + + YM2612_Clock = Clock; + YM2612_Rate = Rate; + + YM2612_Frequency = ((double) YM2612_Clock / (double) YM2612_Rate) / 144.0; + YM2612_TimerBase = (int) (YM2612_Frequency * 4096.0); + + YM2612_Inter_Step = 0x4000; + YM2612_Inter_Cnt = 0; + + // TL Table : + // [0 - 4095] = +output [4095 - ...] = +output overflow (fill with 0) + // [12288 - 16383] = -output [16384 - ...] = -output overflow (fill with 0) + + for (i = 0; i < TLLEN; i++) { + if (i >= PG_CUT_OFF) { + TL_TAB[TLLEN + i] = TL_TAB[i] = 0; + } else { + x = MAX_OUT; // Max output + x /= Math.pow(10, (ENV_STEP * i) / 20); + TL_TAB[i] = (int) x; + TL_TAB[TLLEN + i] = -TL_TAB[i]; + } + } + + // SIN Table : + // SIN_TAB[x][y] = sin(x) * y; + // x = phase and y = volume + + SIN_TAB[0] = PG_CUT_OFF; + SIN_TAB[SINLEN / 2] = PG_CUT_OFF; + + for (i = 1; i <= SINLEN / 4; i++) { + x = Math.sin(2.0 * PI * (double) (i) / (double) (SINLEN)); // Sinus + x = 20 * log10(1 / x); // convert to dB + + j = (int) (x / ENV_STEP); // Get TL range + + if (j > PG_CUT_OFF) j = (int) PG_CUT_OFF; + + SIN_TAB[i] = j; + SIN_TAB[(SINLEN / 2) - i] = j; + SIN_TAB[(SINLEN / 2) + i] = TLLEN + j; + SIN_TAB[SINLEN - i] = TLLEN + j; + } + + // LFO Table (LFO wav) : + + for (i = 0; i < LFOLEN; i++) { + x = Math.sin(2.0 * PI * (double) (i) / (double) (LFOLEN)); // Sinus + x += 1.0; + x /= 2.0; + x *= 11.8 / ENV_STEP; + LFO_ENV_TAB[i] = (int) x; + x = Math.sin(2.0 * PI * (double) (i) / (double) (LFOLEN)); // Sinus + x *= (double) ((1 << (LFO_HBITS - 1)) - 1); + LFO_FREQ_TAB[i] = (int) x; + } + + + for (i = 0; i < ENVLEN; i++) { + x = Math.pow(((double) ((ENVLEN - 1) - i) / (double) (ENVLEN)), 8); + x *= ENVLEN; + ENV_TAB[i] = (int) x; + x = Math.pow(((double) (i) / (double) (ENVLEN)), 1); + x *= ENVLEN; + ENV_TAB[ENVLEN + i] = (int) x; + } + + ENV_TAB[ENV_END >> ENV_LBITS] = ENVLEN - 1; + + // Table Decay and Decay + + for (i = 0, j = ENVLEN - 1; i < ENVLEN; i++) { + while (j != 0 && (ENV_TAB[j] < i)) j--; + DECAY_TO_ATTACK[i] = j << ENV_LBITS; + } + + // Sustain Level Table + + for (i = 0; i < 15; i++) { + x = i * 3; + x /= ENV_STEP; + + j = (int) x; + j <<= ENV_LBITS; + SL_TAB[i] = j + ENV_DECAY; + } + + j = ENVLEN - 1; // special case : volume off + j <<= ENV_LBITS; + SL_TAB[15] = j + ENV_DECAY; + + //Frequency Step Table + + for (i = 0; i < 2048; i++) { + x = (double) i * YM2612_Frequency; + + if ((SIN_LBITS + SIN_HBITS - (21 - 7)) < 0) { + x /= (double) (1 << ((21 - 7) - SIN_LBITS - SIN_HBITS)); + } else { + x *= (double) (1 << (SIN_LBITS + SIN_HBITS - (21 - 7))); + } + x /= 2.0; // because MUL = value * 2 + FINC_TAB[i] = (int) x; // (unsigned int) x; + } + + // Attack & Decay Rate Table + + for (i = 0; i < 4; i++) { + AR_TAB[i] = 0; + DR_TAB[i] = 0; + } + + for (i = 0; i < 60; i++) { + x = YM2612_Frequency; + x *= 1.0 + ((i & 3) * 0.25); // bits 0-1 : x1.00, x1.25, x1.50, x1.75 + x *= (double) (1 << ((i >> 2))); // bits 2-5 : shift bits (x2^0 - x2^15) + x *= (double) (ENVLEN << ENV_LBITS); // on ajuste pour le tableau ENV_TAB + + AR_TAB[i + 4] = (int) (x / AR_RATE); // (unsigned int) (x / AR_RATE); + DR_TAB[i + 4] = (int) (x / DR_RATE); // (unsigned int) (x / DR_RATE); + } + + for (i = 64; i < 96; i++) { + AR_TAB[i] = AR_TAB[63]; + DR_TAB[i] = DR_TAB[63]; + AR_TAB[i - 64 + AR_NULL_RATE] = 0; + DR_TAB[i - 64 + DR_NULL_RATE] = 0; + } + + // Detune Table + for (i = 0; i < 4; i++) { + for (j = 0; j < 32; j++) { + if ((SIN_LBITS + SIN_HBITS - 21) < 0) { + x = (double) DT_DEF_TAB[(i << 5) + j] * YM2612_Frequency / (double) (1 << (21 - SIN_LBITS - SIN_HBITS)); + } else { + x = (double) DT_DEF_TAB[(i << 5) + j] * YM2612_Frequency * (double) (1 << (SIN_LBITS + SIN_HBITS - 21)); + } + DT_TAB[i + 0][j] = (int) x; + DT_TAB[i + 4][j] = (int) -x; + } + } + + // LFO Table + j = (int) ((YM2612_Rate * YM2612_Inter_Step) / 0x4000); + + LFO_INC_TAB[0] = (int) (3.98 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[1] = (int) (5.56 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[2] = (int) (6.02 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[3] = (int) (6.37 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[4] = (int) (6.88 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[5] = (int) (9.63 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[6] = (int) (48.1 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + LFO_INC_TAB[7] = (int) (72.2 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / j); + + reset(); + return 0; + } + + public final int reset() { + int i, j; + + YM2612_LFOcnt = 0; + YM2612_TimerA = 0; + YM2612_TimerAL = 0; + YM2612_TimerAcnt = 0; + YM2612_TimerB = 0; + YM2612_TimerBL = 0; + YM2612_TimerBcnt = 0; + YM2612_DAC = 0; + + YM2612_Status = 0; + + YM2612_Inter_Cnt = 0; + + for (i = 0; i < 6; i++) { + YM2612_CHANNEL[i].Old_OUTd = 0; + YM2612_CHANNEL[i].OUTd = 0; + YM2612_CHANNEL[i].LEFT = 0xFFFFFFFF; + YM2612_CHANNEL[i].RIGHT = 0xFFFFFFFF; + YM2612_CHANNEL[i].ALGO = 0; + YM2612_CHANNEL[i].FB = 31; + YM2612_CHANNEL[i].FMS = 0; + YM2612_CHANNEL[i].AMS = 0; + + for (j = 0; j < 4; j++) { + YM2612_CHANNEL[i].S0_OUT[j] = 0; + YM2612_CHANNEL[i].FNUM[j] = 0; + YM2612_CHANNEL[i].FOCT[j] = 0; + YM2612_CHANNEL[i].KC[j] = 0; + + YM2612_CHANNEL[i].SLOT[j].Fcnt = 0; + YM2612_CHANNEL[i].SLOT[j].Finc = 0; + YM2612_CHANNEL[i].SLOT[j].Ecnt = ENV_END; // Put it at the end of Decay phase... + YM2612_CHANNEL[i].SLOT[j].Einc = 0; + YM2612_CHANNEL[i].SLOT[j].Ecmp = 0; + YM2612_CHANNEL[i].SLOT[j].Ecurp = RELEASE; + + YM2612_CHANNEL[i].SLOT[j].ChgEnM = 0; + } + } + + for (i = 0; i < 0x100; i++) { + YM2612_REG[0][i] = -1; + YM2612_REG[1][i] = -1; + } + + for (i = 0xB6; i >= 0xB4; i--) { + write0(i, 0xC0); + write1(i, 0xC0); + } + + for (i = 0xB2; i >= 0x22; i--) { + write0(i, 0); + write1(i, 0); + } + + write0(0x2A, 0x80); + + return 0; + } + + + public final int read() { + return (YM2612_Status); + } + + public final void write0(int addr, int data) { + if (addr < 0x30) { + YM2612_REG[0][addr] = data; + setYM(addr, data); + } else if (YM2612_REG[0][addr] != data) { + YM2612_REG[0][addr] = data; + + if (addr < 0xA0) + setSlot(addr, data); + else + setChannel(addr, data); + } + } + + public final void write1(int addr, int data) { + if (addr >= 0x30 && YM2612_REG[1][addr] != data) { + YM2612_REG[1][addr] = data; + + if (addr < 0xA0) + setSlot(addr + 0x100, data); + else + setChannel(addr + 0x100, data); + } + } + + public final void update(int[] buf_lr, int offset, int end) { + offset *= 2; + end = end * 2 + offset; + + if (YM2612_CHANNEL[0].SLOT[0].Finc == -1) calc_FINC_CH(YM2612_CHANNEL[0]); + if (YM2612_CHANNEL[1].SLOT[0].Finc == -1) calc_FINC_CH(YM2612_CHANNEL[1]); + if (YM2612_CHANNEL[2].SLOT[0].Finc == -1) { + if ((YM2612_Mode & 0x40) != 0) { + calc_FINC_SL((YM2612_CHANNEL[2].SLOT[S0]), FINC_TAB[YM2612_CHANNEL[2].FNUM[2]] >> (7 - YM2612_CHANNEL[2].FOCT[2]), YM2612_CHANNEL[2].KC[2]); + calc_FINC_SL((YM2612_CHANNEL[2].SLOT[S1]), FINC_TAB[YM2612_CHANNEL[2].FNUM[3]] >> (7 - YM2612_CHANNEL[2].FOCT[3]), YM2612_CHANNEL[2].KC[3]); + calc_FINC_SL((YM2612_CHANNEL[2].SLOT[S2]), FINC_TAB[YM2612_CHANNEL[2].FNUM[1]] >> (7 - YM2612_CHANNEL[2].FOCT[1]), YM2612_CHANNEL[2].KC[1]); + calc_FINC_SL((YM2612_CHANNEL[2].SLOT[S3]), FINC_TAB[YM2612_CHANNEL[2].FNUM[0]] >> (7 - YM2612_CHANNEL[2].FOCT[0]), YM2612_CHANNEL[2].KC[0]); + } else { + calc_FINC_CH(YM2612_CHANNEL[2]); + } + } + if (YM2612_CHANNEL[3].SLOT[0].Finc == -1) calc_FINC_CH(YM2612_CHANNEL[3]); + if (YM2612_CHANNEL[4].SLOT[0].Finc == -1) calc_FINC_CH(YM2612_CHANNEL[4]); + if (YM2612_CHANNEL[5].SLOT[0].Finc == -1) calc_FINC_CH(YM2612_CHANNEL[5]); + +// if(YM2612_Inter_Step & 0x04000) algo_type = 0; +// else algo_type = 16; + int algo_type = 0; + + if ((YM2612_LFOinc) != 0) { + // Precalculate LFO wave + for (int o = offset; o < end; o += 2) { + int i = o >> 1; + int j = ((YM2612_LFOcnt += YM2612_LFOinc) >> LFO_LBITS) & LFO_MSK; + + LFO_ENV_UP[i] = LFO_ENV_TAB[j]; + LFO_FREQ_UP[i] = LFO_FREQ_TAB[j]; + } + + algo_type |= 8; + } + + updateChannel((YM2612_CHANNEL[0].ALGO + algo_type), (YM2612_CHANNEL[0]), buf_lr, offset, end); + updateChannel((YM2612_CHANNEL[1].ALGO + algo_type), (YM2612_CHANNEL[1]), buf_lr, offset, end); + updateChannel((YM2612_CHANNEL[2].ALGO + algo_type), (YM2612_CHANNEL[2]), buf_lr, offset, end); + updateChannel((YM2612_CHANNEL[3].ALGO + algo_type), (YM2612_CHANNEL[3]), buf_lr, offset, end); + updateChannel((YM2612_CHANNEL[4].ALGO + algo_type), (YM2612_CHANNEL[4]), buf_lr, offset, end); + if (YM2612_DAC == 0) + updateChannel(YM2612_CHANNEL[5].ALGO + algo_type, YM2612_CHANNEL[5], buf_lr, offset, end); + + YM2612_Inter_Cnt = int_cnt; + } + + public final void synchronizeTimers(int length) { + int i; + + i = YM2612_TimerBase * length; + + if ((YM2612_Mode & 1) != 0) { +// if((YM2612_TimerAcnt -= 14073) <= 0){ // 13879=NTSC (old: 14475=NTSC 14586=PAL) + if ((YM2612_TimerAcnt -= i) <= 0) { + YM2612_Status |= (YM2612_Mode & 0x04) >> 2; + YM2612_TimerAcnt += YM2612_TimerAL; + + if ((YM2612_Mode & 0x80) != 0) CSM_Key_Control(); + } + } + if ((YM2612_Mode & 2) != 0) { +// if((YM2612_TimerBcnt -= 14073) <= 0){ // 13879=NTSC (old: 14475=NTSC 14586=PAL) + if ((YM2612_TimerBcnt -= i) <= 0) { + YM2612_Status |= (YM2612_Mode & 0x08) >> 2; + YM2612_TimerBcnt += YM2612_TimerBL; + } + } + } + + /*********************************************** + * + * Parameter Calculation + * + ***********************************************/ + + private final void calc_FINC_SL(cSlot SL, int finc, int kc) { + int ksr; + SL.Finc = (finc + SL.DT[kc]) * SL.MUL; + ksr = kc >> SL.KSR_S; + if (SL.KSR != ksr) { + SL.KSR = ksr; + SL.EincA = AR_TAB[SL.AR + ksr]; + SL.EincD = DR_TAB[SL.DR + ksr]; + SL.EincS = DR_TAB[SL.SR + ksr]; + SL.EincR = DR_TAB[SL.RR + ksr]; + if (SL.Ecurp == ATTACK) SL.Einc = SL.EincA; + else if (SL.Ecurp == DECAY) SL.Einc = SL.EincD; + else if (SL.Ecnt < ENV_END) { + if (SL.Ecurp == SUSTAIN) SL.Einc = SL.EincS; + else if (SL.Ecurp == RELEASE) SL.Einc = SL.EincR; + } + } + } + + private final void calc_FINC_CH(cChannel CH) { + int finc, kc; + finc = (int) (FINC_TAB[CH.FNUM[0]] >> (7 - CH.FOCT[0])); + kc = CH.KC[0]; + calc_FINC_SL(CH.SLOT[0], finc, kc); + calc_FINC_SL(CH.SLOT[1], finc, kc); + calc_FINC_SL(CH.SLOT[2], finc, kc); + calc_FINC_SL(CH.SLOT[3], finc, kc); + } + + /*********************************************** + * + * Settings + * + ***********************************************/ + + private final void KEY_ON(cChannel CH, int nsl) { + cSlot SL = CH.SLOT[nsl]; + if (SL.Ecurp == RELEASE) { + SL.Fcnt = 0; + // Fix Ecco 2 splash sound + SL.Ecnt = (DECAY_TO_ATTACK[ENV_TAB[SL.Ecnt >> ENV_LBITS]] + ENV_ATTACK) & SL.ChgEnM; + SL.ChgEnM = 0xFFFFFFFF; + SL.Einc = SL.EincA; + SL.Ecmp = ENV_DECAY; + SL.Ecurp = ATTACK; + } + } + + private final void KEY_OFF(cChannel CH, int nsl) { + cSlot SL = CH.SLOT[nsl]; + if (SL.Ecurp != RELEASE) { + if (SL.Ecnt < ENV_DECAY) { + SL.Ecnt = (ENV_TAB[SL.Ecnt >> ENV_LBITS] << ENV_LBITS) + ENV_DECAY; + } + SL.Einc = SL.EincR; + SL.Ecmp = ENV_END; + SL.Ecurp = RELEASE; + } + } + + private final void CSM_Key_Control() { + KEY_ON(YM2612_CHANNEL[2], 0); + KEY_ON(YM2612_CHANNEL[2], 1); + KEY_ON(YM2612_CHANNEL[2], 2); + KEY_ON(YM2612_CHANNEL[2], 3); + } + + private final int setSlot(int address, int data) { // INT, UCHAR + data &= 0xFF; // unsign + cChannel CH; + cSlot SL; + int nch, nsl; + + if ((nch = address & 3) == 3) return 1; + nsl = (address >> 2) & 3; + + if ((address & 0x100) != 0) nch += 3; + + CH = YM2612_CHANNEL[nch]; + SL = CH.SLOT[nsl]; + + switch (address & 0xF0) { + case 0x30: + if ((SL.MUL = (data & 0x0F)) != 0) SL.MUL <<= 1; + else SL.MUL = 1; + SL.DT = DT_TAB[(data >> 4) & 7]; // = DT_TAB[(data >> 4) & 7]; + CH.SLOT[0].Finc = -1; + break; + case 0x40: + SL.TL = data & 0x7F; + // SOR2 do a lot of TL adjustement and this fix R.Shinobi jump sound... + if ((ENV_HBITS - 7) < 0) SL.TLL = SL.TL >> (7 - ENV_HBITS); + else SL.TLL = SL.TL << (ENV_HBITS - 7); + break; + case 0x50: + SL.KSR_S = 3 - (data >> 6); + CH.SLOT[0].Finc = -1; + if ((data &= 0x1F) != 0) SL.AR = data << 1; // = &AR_TAB[data << 1]; + else SL.AR = AR_NULL_RATE; // &NULL_RATE[0]; + + SL.EincA = AR_TAB[SL.AR + SL.KSR]; // SL.AR[SL.KSR]; + if (SL.Ecurp == ATTACK) SL.Einc = SL.EincA; + break; + case 0x60: + if ((SL.AMSon = (data & 0x80)) != 0) SL.AMS = CH.AMS; + else SL.AMS = 31; + + if ((data &= 0x1F) != 0) SL.DR = data << 1; // = &DR_TAB[data << 1]; + else SL.DR = DR_NULL_RATE; // = &NULL_RATE[0]; + + SL.EincD = DR_TAB[SL.DR + SL.KSR]; // SL.DR[SL.KSR]; + if (SL.Ecurp == DECAY) SL.Einc = SL.EincD; + break; + case 0x70: + if ((data &= 0x1F) != 0) SL.SR = data << 1; // = &DR_TAB[data << 1]; + else SL.SR = DR_NULL_RATE; // = &NULL_RATE[0]; + SL.EincS = DR_TAB[SL.SR + SL.KSR]; + if ((SL.Ecurp == SUSTAIN) && (SL.Ecnt < ENV_END)) SL.Einc = SL.EincS; + break; + case 0x80: + SL.SLL = SL_TAB[data >> 4]; + SL.RR = ((data & 0xF) << 2) + 2; // = &DR_TAB[((data & 0xF) << 2) + 2]; + SL.EincR = DR_TAB[SL.RR + SL.KSR]; // [SL.KSR]; + if ((SL.Ecurp == RELEASE) && (SL.Ecnt < ENV_END)) SL.Einc = SL.EincR; + break; + case 0x90: + if (EnableSSGEG) { + if ((data & 0x08) != 0) SL.SEG = data & 0x0F; + else SL.SEG = 0; + } + break; + } + return 0; + } + + private final int setChannel(int address, int data) { // INT,UCHAR + data &= 0xFF; // unsign + cChannel CH; + int num; + + if ((num = address & 3) == 3) return 1; + + switch (address & 0xFC) { + case 0xA0: + if ((address & 0x100) != 0) num += 3; + CH = YM2612_CHANNEL[num]; + CH.FNUM[0] = (CH.FNUM[0] & 0x700) + data; + CH.KC[0] = (CH.FOCT[0] << 2) | FKEY_TAB[CH.FNUM[0] >> 7]; + CH.SLOT[0].Finc = -1; + break; + case 0xA4: + if ((address & 0x100) != 0) num += 3; + CH = YM2612_CHANNEL[num]; + CH.FNUM[0] = (CH.FNUM[0] & 0x0FF) + ((int) (data & 0x07) << 8); + CH.FOCT[0] = (data & 0x38) >> 3; + CH.KC[0] = (CH.FOCT[0] << 2) | FKEY_TAB[CH.FNUM[0] >> 7]; + CH.SLOT[0].Finc = -1; + break; + case 0xA8: + if (address < 0x100) { + num++; + YM2612_CHANNEL[2].FNUM[num] = (YM2612_CHANNEL[2].FNUM[num] & 0x700) + data; + YM2612_CHANNEL[2].KC[num] = (YM2612_CHANNEL[2].FOCT[num] << 2) | FKEY_TAB[YM2612_CHANNEL[2].FNUM[num] >> 7]; + YM2612_CHANNEL[2].SLOT[0].Finc = -1; + } + break; + case 0xAC: + if (address < 0x100) { + num++; + YM2612_CHANNEL[2].FNUM[num] = (YM2612_CHANNEL[2].FNUM[num] & 0x0FF) + ((int) (data & 0x07) << 8); + YM2612_CHANNEL[2].FOCT[num] = (data & 0x38) >> 3; + YM2612_CHANNEL[2].KC[num] = (YM2612_CHANNEL[2].FOCT[num] << 2) | FKEY_TAB[YM2612_CHANNEL[2].FNUM[num] >> 7]; + YM2612_CHANNEL[2].SLOT[0].Finc = -1; + } + break; + case 0xB0: + if ((address & 0x100) != 0) num += 3; + CH = YM2612_CHANNEL[num]; + if (CH.ALGO != (data & 7)) { + CH.ALGO = data & 7; + CH.SLOT[0].ChgEnM = 0; + CH.SLOT[1].ChgEnM = 0; + CH.SLOT[2].ChgEnM = 0; + CH.SLOT[3].ChgEnM = 0; + } + CH.FB = 9 - ((data >> 3) & 7); // Real thing ? + // if(CH.FB = ((data >> 3) & 7)) CH.FB = 9 - CH.FB; // Thunder force 4 (music stage 8), Gynoug, Aladdin bug sound... + // else CH.FB = 31; + break; + case 0xB4: + if ((address & 0x100) != 0) num += 3; + CH = YM2612_CHANNEL[num]; + if ((data & 0x80) != 0) CH.LEFT = 0xFFFFFFFF; + else CH.LEFT = 0; + if ((data & 0x40) != 0) CH.RIGHT = 0xFFFFFFFF; + else CH.RIGHT = 0; + CH.AMS = LFO_AMS_TAB[(data >> 4) & 3]; + CH.FMS = LFO_FMS_TAB[data & 7]; + if (CH.SLOT[0].AMSon != 0) CH.SLOT[0].AMS = CH.AMS; + else CH.SLOT[0].AMS = 31; + if (CH.SLOT[1].AMSon != 0) CH.SLOT[1].AMS = CH.AMS; + else CH.SLOT[1].AMS = 31; + if (CH.SLOT[2].AMSon != 0) CH.SLOT[2].AMS = CH.AMS; + else CH.SLOT[2].AMS = 31; + if (CH.SLOT[3].AMSon != 0) CH.SLOT[3].AMS = CH.AMS; + else CH.SLOT[3].AMS = 31; + break; + } + return 0; + } + + + private final int setYM(int address, int data) { // INT, UCHAR + cChannel CH; + int nch; + + switch (address) { + case 0x22: + if ((data & 8) != 0) { + // Cool Spot music 1, LFO modified severals time which + // distorts the sound, have to check that on a real genesis... + YM2612_LFOinc = LFO_INC_TAB[data & 7]; + } else { + YM2612_LFOinc = YM2612_LFOcnt = 0; + } + break; + case 0x24: + YM2612_TimerA = (YM2612_TimerA & 0x003) | (((int) data) << 2); + if (YM2612_TimerAL != ((1024 - YM2612_TimerA) << 12)) { + YM2612_TimerAcnt = YM2612_TimerAL = (1024 - YM2612_TimerA) << 12; + } +// System.out.println("Timer AH: " + Integer.toHexString(YM2612_TimerA)); + break; + case 0x25: + YM2612_TimerA = (YM2612_TimerA & 0x3fc) | (data & 3); + if (YM2612_TimerAL != ((1024 - YM2612_TimerA) << 12)) { + YM2612_TimerAcnt = YM2612_TimerAL = (1024 - YM2612_TimerA) << 12; + } +// System.out.println("Timer AL: " + Integer.toHexString(YM2612_TimerA)); + break; + case 0x26: + YM2612_TimerB = data; + if (YM2612_TimerBL != ((256 - YM2612_TimerB) << (4 + 12))) { + YM2612_TimerBcnt = YM2612_TimerBL = (256 - YM2612_TimerB) << (4 + 12); + } +// System.out.println("Timer B : " + Integer.toHexString(YM2612_TimerB)); + break; + case 0x27: + if (((data ^ YM2612_Mode) & 0x40) != 0) { + // We changed the channel 2 mode, so recalculate phase step + // This fix the punch sound in Street of Rage 2 + YM2612_CHANNEL[2].SLOT[0].Finc = -1; // recalculate phase step + } + YM2612_Status &= (~data >> 4) & (data >> 2); // Reset Status + YM2612_Mode = data; + break; + case 0x28: + if ((nch = data & 3) == 3) return 1; + if ((data & 4) != 0) nch += 3; + CH = YM2612_CHANNEL[nch]; + if ((data & 0x10) != 0) KEY_ON(CH, S0); + else KEY_OFF(CH, S0); + if ((data & 0x20) != 0) KEY_ON(CH, S1); + else KEY_OFF(CH, S1); + if ((data & 0x40) != 0) KEY_ON(CH, S2); + else KEY_OFF(CH, S2); + if ((data & 0x80) != 0) KEY_ON(CH, S3); + else KEY_OFF(CH, S3); + break; + case 0x2A: + break; + case 0x2B: + YM2612_DAC = data & 0x80; + break; + } + return 0; + } + + + /*********************************************** + * + * Generation Methods + * + ***********************************************/ + + private final void Env_NULL_Next(cSlot SL) { + } + + private final void Env_Attack_Next(cSlot SL) { + SL.Ecnt = ENV_DECAY; + SL.Einc = SL.EincD; + SL.Ecmp = SL.SLL; + SL.Ecurp = DECAY; + } + + private final void Env_Decay_Next(cSlot SL) { + SL.Ecnt = SL.SLL; + SL.Einc = SL.EincS; + SL.Ecmp = ENV_END; + SL.Ecurp = SUSTAIN; + } + + private final void Env_Sustain_Next(cSlot SL) { + if (EnableSSGEG) { + if ((SL.SEG & 8) != 0) { + if ((SL.SEG & 1) != 0) { + SL.Ecnt = ENV_END; + SL.Einc = 0; + SL.Ecmp = ENV_END + 1; + } else { + SL.Ecnt = 0; + SL.Einc = SL.EincA; + SL.Ecmp = ENV_DECAY; + SL.Ecurp = ATTACK; + } + SL.SEG ^= (SL.SEG & 2) << 1; + } else { + SL.Ecnt = ENV_END; + SL.Einc = 0; + SL.Ecmp = ENV_END + 1; + } + } else { + SL.Ecnt = ENV_END; + SL.Einc = 0; + SL.Ecmp = ENV_END + 1; + } + } + + private final void Env_Release_Next(cSlot SL) { + SL.Ecnt = ENV_END; + SL.Einc = 0; + SL.Ecmp = ENV_END + 1; + } + + private final void ENV_NEXT_EVENT(int which, cSlot SL) { + switch (which) { + case 0: + Env_Attack_Next(SL); + return; + case 1: + Env_Decay_Next(SL); + return; + case 2: + Env_Sustain_Next(SL); + return; + case 3: + Env_Release_Next(SL); + return; + //default: Env_NULL_Next(SL); return; + } + } + + private final void calcChannel(int ALGO, cChannel CH) { + // DO_FEEDBACK + in0 += (CH.S0_OUT[0] + CH.S0_OUT[1]) >> CH.FB; + CH.S0_OUT[1] = CH.S0_OUT[0]; + CH.S0_OUT[0] = TL_TAB[SIN_TAB[(in0 >> SIN_LBITS) & SIN_MSK] + en0]; + switch (ALGO) { + case 0: + in1 += CH.S0_OUT[1]; + in2 += TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1]; + in3 += TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]; + CH.OUTd = TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] >> MAIN_SHIFT; + break; + case 1: + in2 += CH.S0_OUT[1] + TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1]; + in3 += TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]; + CH.OUTd = TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] >> MAIN_SHIFT; + break; + case 2: + in2 += TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1]; + in3 += CH.S0_OUT[1] + TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]; + CH.OUTd = TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] >> MAIN_SHIFT; + break; + case 3: + in1 += CH.S0_OUT[1]; + in3 += TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1] + TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]; + CH.OUTd = TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] >> MAIN_SHIFT; + break; + case 4: + in1 += CH.S0_OUT[1]; + in3 += TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]; + CH.OUTd = (TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] + + TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1]) >> MAIN_SHIFT; + break; + case 5: + in1 += CH.S0_OUT[1]; + in2 += CH.S0_OUT[1]; + in3 += CH.S0_OUT[1]; + CH.OUTd = (TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] + + TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1] + + TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]) >> MAIN_SHIFT; + break; + case 6: + in1 += CH.S0_OUT[1]; + CH.OUTd = (TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] + + TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1] + + TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2]) >> MAIN_SHIFT; + break; + case 7: + CH.OUTd = (TL_TAB[SIN_TAB[(in3 >> SIN_LBITS) & SIN_MSK] + en3] + + TL_TAB[SIN_TAB[(in1 >> SIN_LBITS) & SIN_MSK] + en1] + + TL_TAB[SIN_TAB[(in2 >> SIN_LBITS) & SIN_MSK] + en2] + + CH.S0_OUT[1]) >> MAIN_SHIFT; + break; + } + // DO_LIMIT + if (CH.OUTd > LIMIT_CH_OUT) CH.OUTd = LIMIT_CH_OUT; + else if (CH.OUTd < -LIMIT_CH_OUT) CH.OUTd = -LIMIT_CH_OUT; + } + + private final void processChannel(cChannel CH, int[] buf_lr, int OFFSET, int END, int ALGO) { + if (ALGO < 4) { + if (CH.SLOT[S3].Ecnt == ENV_END) + return; + } else if (ALGO == 4) { + if ((CH.SLOT[S1].Ecnt == ENV_END) && (CH.SLOT[S3].Ecnt == ENV_END)) + return; + } else if (ALGO < 7) { + if ((CH.SLOT[S1].Ecnt == ENV_END) && (CH.SLOT[S2].Ecnt == ENV_END) && (CH.SLOT[S3].Ecnt == ENV_END)) + return; + } else { + if ((CH.SLOT[S0].Ecnt == ENV_END) && (CH.SLOT[S1].Ecnt == ENV_END) && + (CH.SLOT[S2].Ecnt == ENV_END) && (CH.SLOT[S3].Ecnt == ENV_END)) + return; + } + + do { + // GET_CURRENT_PHASE + in0 = CH.SLOT[S0].Fcnt; + in1 = CH.SLOT[S1].Fcnt; + in2 = CH.SLOT[S2].Fcnt; + in3 = CH.SLOT[S3].Fcnt; + // UPDATE_PHASE + CH.SLOT[S0].Fcnt += CH.SLOT[S0].Finc; + CH.SLOT[S1].Fcnt += CH.SLOT[S1].Finc; + CH.SLOT[S2].Fcnt += CH.SLOT[S2].Finc; + CH.SLOT[S3].Fcnt += CH.SLOT[S3].Finc; + // GET_CURRENT_ENV + if ((CH.SLOT[S0].SEG & 4) != 0) { + if ((en0 = ENV_TAB[(CH.SLOT[S0].Ecnt >> ENV_LBITS)] + CH.SLOT[S0].TLL) > ENV_MSK) en0 = 0; + else en0 ^= ENV_MSK; + } else en0 = ENV_TAB[(CH.SLOT[S0].Ecnt >> ENV_LBITS)] + CH.SLOT[S0].TLL; + if ((CH.SLOT[S1].SEG & 4) != 0) { + if ((en1 = ENV_TAB[(CH.SLOT[S1].Ecnt >> ENV_LBITS)] + CH.SLOT[S1].TLL) > ENV_MSK) en1 = 0; + else en1 ^= ENV_MSK; + } else en1 = ENV_TAB[(CH.SLOT[S1].Ecnt >> ENV_LBITS)] + CH.SLOT[S1].TLL; + if ((CH.SLOT[S2].SEG & 4) != 0) { + if ((en2 = ENV_TAB[(CH.SLOT[S2].Ecnt >> ENV_LBITS)] + CH.SLOT[S2].TLL) > ENV_MSK) en2 = 0; + else en2 ^= ENV_MSK; + } else en2 = ENV_TAB[(CH.SLOT[S2].Ecnt >> ENV_LBITS)] + CH.SLOT[S2].TLL; + if ((CH.SLOT[S3].SEG & 4) != 0) { + if ((en3 = ENV_TAB[(CH.SLOT[S3].Ecnt >> ENV_LBITS)] + CH.SLOT[S3].TLL) > ENV_MSK) en3 = 0; + else en3 ^= ENV_MSK; + } else en3 = ENV_TAB[(CH.SLOT[S3].Ecnt >> ENV_LBITS)] + CH.SLOT[S3].TLL; + // UPDATE_ENV + if ((CH.SLOT[S0].Ecnt += CH.SLOT[S0].Einc) >= CH.SLOT[S0].Ecmp) { + ENV_NEXT_EVENT(CH.SLOT[S0].Ecurp, CH.SLOT[S0]); + } + if ((CH.SLOT[S1].Ecnt += CH.SLOT[S1].Einc) >= CH.SLOT[S1].Ecmp) { + ENV_NEXT_EVENT(CH.SLOT[S1].Ecurp, CH.SLOT[S1]); + } + if ((CH.SLOT[S2].Ecnt += CH.SLOT[S2].Einc) >= CH.SLOT[S2].Ecmp) { + ENV_NEXT_EVENT(CH.SLOT[S2].Ecurp, CH.SLOT[S2]); + } + if ((CH.SLOT[S3].Ecnt += CH.SLOT[S3].Einc) >= CH.SLOT[S3].Ecmp) { + ENV_NEXT_EVENT(CH.SLOT[S3].Ecurp, CH.SLOT[S3]); + } + calcChannel(ALGO, CH); + //DO_OUTPUT + buf_lr[OFFSET] += (CH.OUTd & CH.LEFT); + buf_lr[OFFSET + 1] += (CH.OUTd & CH.RIGHT); + OFFSET += 2; + } + while (OFFSET < END); + } + + private final void processChannel_LFO(cChannel CH, int[] buf_lr, int OFFSET, int END, int ALGO) { + if (ALGO < 4) { + if (CH.SLOT[S3].Ecnt == ENV_END) + return; + } else if (ALGO == 4) { + if ((CH.SLOT[S1].Ecnt == ENV_END) && (CH.SLOT[S3].Ecnt == ENV_END)) + return; + } else if (ALGO < 7) { + if ((CH.SLOT[S1].Ecnt == ENV_END) && (CH.SLOT[S2].Ecnt == ENV_END) && (CH.SLOT[S3].Ecnt == ENV_END)) + return; + } else { + if ((CH.SLOT[S0].Ecnt == ENV_END) && (CH.SLOT[S1].Ecnt == ENV_END) && + (CH.SLOT[S2].Ecnt == ENV_END) && (CH.SLOT[S3].Ecnt == ENV_END)) + return; + } + + do { + final int i = OFFSET >> 1; + + // GET_CURRENT_PHASE + in0 = CH.SLOT[S0].Fcnt; + in1 = CH.SLOT[S1].Fcnt; + in2 = CH.SLOT[S2].Fcnt; + in3 = CH.SLOT[S3].Fcnt; + // UPDATE_PHASE_LFO + int freq_LFO = (CH.FMS * LFO_FREQ_UP[i]) >> (LFO_HBITS - 1); + if (freq_LFO != 0) { + CH.SLOT[S0].Fcnt += CH.SLOT[S0].Finc + ((CH.SLOT[S0].Finc * freq_LFO) >> LFO_FMS_LBITS); + CH.SLOT[S1].Fcnt += CH.SLOT[S1].Finc + ((CH.SLOT[S1].Finc * freq_LFO) >> LFO_FMS_LBITS); + CH.SLOT[S2].Fcnt += CH.SLOT[S2].Finc + ((CH.SLOT[S2].Finc * freq_LFO) >> LFO_FMS_LBITS); + CH.SLOT[S3].Fcnt += CH.SLOT[S3].Finc + ((CH.SLOT[S3].Finc * freq_LFO) >> LFO_FMS_LBITS); + } else { + CH.SLOT[S0].Fcnt += CH.SLOT[S0].Finc; + CH.SLOT[S1].Fcnt += CH.SLOT[S1].Finc; + CH.SLOT[S2].Fcnt += CH.SLOT[S2].Finc; + CH.SLOT[S3].Fcnt += CH.SLOT[S3].Finc; + } + // GET_CURRENT_ENV_LFO + int env_LFO = LFO_ENV_UP[i]; + if ((CH.SLOT[S0].SEG & 4) != 0) { + if ((en0 = ENV_TAB[(CH.SLOT[S0].Ecnt >> ENV_LBITS)] + CH.SLOT[S0].TLL) > ENV_MSK) en0 = 0; + else en0 = (en0 ^ ENV_MSK) + (env_LFO >> CH.SLOT[S0].AMS); + } else en0 = ENV_TAB[(CH.SLOT[S0].Ecnt >> ENV_LBITS)] + CH.SLOT[S0].TLL + (env_LFO >> CH.SLOT[S0].AMS); + if ((CH.SLOT[S1].SEG & 4) != 0) { + if ((en1 = ENV_TAB[(CH.SLOT[S1].Ecnt >> ENV_LBITS)] + CH.SLOT[S1].TLL) > ENV_MSK) en1 = 0; + else en1 = (en1 ^ ENV_MSK) + (env_LFO >> CH.SLOT[S1].AMS); + } else en1 = ENV_TAB[(CH.SLOT[S1].Ecnt >> ENV_LBITS)] + CH.SLOT[S1].TLL + (env_LFO >> CH.SLOT[S1].AMS); + if ((CH.SLOT[S2].SEG & 4) != 0) { + if ((en2 = ENV_TAB[(CH.SLOT[S2].Ecnt >> ENV_LBITS)] + CH.SLOT[S2].TLL) > ENV_MSK) en2 = 0; + else en2 = (en2 ^ ENV_MSK) + (env_LFO >> CH.SLOT[S2].AMS); + } else en2 = ENV_TAB[(CH.SLOT[S2].Ecnt >> ENV_LBITS)] + CH.SLOT[S2].TLL + (env_LFO >> CH.SLOT[S2].AMS); + if ((CH.SLOT[S3].SEG & 4) != 0) { + if ((en3 = ENV_TAB[(CH.SLOT[S3].Ecnt >> ENV_LBITS)] + CH.SLOT[S3].TLL) > ENV_MSK) en3 = 0; + else en3 = (en3 ^ ENV_MSK) + (env_LFO >> CH.SLOT[S3].AMS); + } else + en3 = ENV_TAB[(CH.SLOT[S3].Ecnt >> ENV_LBITS)] + CH.SLOT[S3].TLL + (env_LFO >> CH.SLOT[S3].AMS); + + // UPDATE_ENV + if ((CH.SLOT[S0].Ecnt += CH.SLOT[S0].Einc) >= CH.SLOT[S0].Ecmp) + ENV_NEXT_EVENT(CH.SLOT[S0].Ecurp, CH.SLOT[S0]); + + if ((CH.SLOT[S1].Ecnt += CH.SLOT[S1].Einc) >= CH.SLOT[S1].Ecmp) + ENV_NEXT_EVENT(CH.SLOT[S1].Ecurp, CH.SLOT[S1]); + + if ((CH.SLOT[S2].Ecnt += CH.SLOT[S2].Einc) >= CH.SLOT[S2].Ecmp) + ENV_NEXT_EVENT(CH.SLOT[S2].Ecurp, CH.SLOT[S2]); + + if ((CH.SLOT[S3].Ecnt += CH.SLOT[S3].Einc) >= CH.SLOT[S3].Ecmp) + ENV_NEXT_EVENT(CH.SLOT[S3].Ecurp, CH.SLOT[S3]); + + calcChannel(ALGO, CH); + //DO_OUTPUT + int left = (CH.OUTd & CH.LEFT); + int right = (CH.OUTd & CH.RIGHT); + + buf_lr[OFFSET] += left; + buf_lr[OFFSET + 1] += right; + OFFSET += 2; + } + while (OFFSET < END); + } + + private final void updateChannel(int ALGO, cChannel CH, int[] buf_lr, int OFFSET, int END) { + if (ALGO < 8) { + processChannel(CH, buf_lr, OFFSET, END, ALGO); + } else { + processChannel_LFO(CH, buf_lr, OFFSET, END, ALGO - 8); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/gme/gme.java b/src/uk/me/fantastic/retro/music/gme/gme.java new file mode 100755 index 0000000..9c26ace --- /dev/null +++ b/src/uk/me/fantastic/retro/music/gme/gme.java @@ -0,0 +1,269 @@ +package uk.me.fantastic.retro.music.gme;/* Simple front-end for VGMPlayer +To build gme.jar: + + javac -source 1.4 *.java + jar cf gme.jar *.class +*/ + +import java.applet.Applet; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; + +class PlayerList extends VGMPlayer { + public Label titleLabel; + public Label trackLabel; + int playlistIndex; + final ArrayList list = new ArrayList(); + + PlayerList(int rate) { + super(rate); + } + + PlayerList() { + super(); + } + + private void updateTrack() { + trackLabel.setText((playlistIndex + 1) + "/" + list.size()); + } + + public void prev() throws Exception { + if (getCurrentTime() < 4 && isPlaying() && playlistIndex > 0) + playlistIndex--; + playIndex(playlistIndex); + } + + public void next() throws Exception { + if (playlistIndex < list.size() - 1) { + playlistIndex++; + playIndex(playlistIndex); + } + } + + private static final class Entry { + String url; + String path; + int track; + String title; + int time; + } + + public void playIndex(int i) throws Exception { + playlistIndex = i; + updateTrack(); + Entry e = (Entry) list.get(i); + titleLabel.setText(e.title); + loadFile(e.url, e.path); + startTrack(e.track - 1, e.time); + } + + public void add(String url) throws Exception { + add(url, "", 1, "", -1, false); + } + + public void add(String url, String path, int track, String title, int time, boolean playNow) throws Exception { + if (title.length() == 0) { + title = path; + if (title.length() == 0) + title = url; + + title = title.substring(title.lastIndexOf('/') + 1); + } + + Entry e = new Entry(); + e.url = url; + e.path = path; + e.track = track; + e.title = title; + e.time = time; + list.add(e); + + if (playNow) + playIndex(list.size() - 1); + else + updateTrack(); + } +} + +final class PlayerWithUpdate extends PlayerList { + Label time; + + public PlayerWithUpdate(int sampleRate) { + super(sampleRate); + } + + public PlayerWithUpdate() { + super(); + trackLabel = new Label(" "); + titleLabel = new Label(" "); + time = new Label(" "); + setVolume(1.0); + } + + char[] str = new char[5]; + int last = -1; + + protected void idle() { + try { + super.idle(); + /* + if ( !isPlaying() ) + { + next(); + last = -1; + } + */ + int secs = getCurrentTime(); + if (last != secs) { + last = secs; + str[4] = (char) ('0' + secs % 10); + str[3] = (char) ('0' + secs / 10 % 6); + str[2] = ':'; + str[1] = (char) ('0' + secs / 60 % 10); + str[0] = (char) ((secs >= 600 ? '0' + secs / 600 : ' ')); + + time.setText(new String(str)); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} + +public final class gme extends Applet implements ActionListener { + // Plays file at given URL (HTTP only). If it's an archive (.zip) + // then path specifies the file within the archive. Track ranges + // from 1 to number of tracks in file. + public void playFile(String url, String path, int track, String title, int time) { + try { + player.add(url, path, track, title, time, !playlistEnabled.getState() || !player.isPlaying()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void playFile(String url, String path, int track, String title) { + playFile(url, path, track, title, 150); + } + + public void playFile(String url, String path, int track) { + playFile(url, path, track, ""); + } + + // Stops currently playing file, if any + public void stopFile() { + try { + player.stop(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +// Applet + + PlayerWithUpdate player; + boolean backgroundPlayback; + Checkbox playlistEnabled; + + private Button newBut(String name) { + Button b = new Button(name); + b.setActionCommand(name); + b.addActionListener(this); + add(b); + return b; + } + + void createGUI() { + add(player.time = new Label(" ")); + add(player.trackLabel = new Label(" ")); + + newBut("Prev"); + newBut("Next"); + newBut("Stop"); + + add(player.titleLabel = new Label(" ")); + + playlistEnabled = new Checkbox("Playlist"); + add(playlistEnabled); + } + + // Returns integer parameter passed to applet, or defaultValue if missing + int getIntParameter(String name, int defaultValue) { + String p = getParameter(name); + return (p != null ? Integer.parseInt(p) : defaultValue); + } + + // Returns string parameter passed to applet, or defaultValue if missing + String getStringParameter(String name, String defaultValue) { + String p = getParameter(name); + return (p != null ? p : defaultValue); + } + + // Called when applet is first loaded + public void init() { + try { + // Setup player and sample rate + int sampleRate = getIntParameter("SAMPLERATE", 44100); + player = new PlayerWithUpdate(sampleRate); + player.setVolume(1.0); + + backgroundPlayback = getIntParameter("BACKGROUND", 0) != 0; + if (getIntParameter("NOGUI", 0) == 0) + createGUI(); + + // Optionally start playing file immediately + String url = getParameter("PLAYURL"); + if (url != null) + playFile(url, getStringParameter("PLAYPATH", ""), + getIntParameter("PLAYTRACK", 1)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + static int rand(int range) { + return (int) (Math.random() * range + 0.5); + } + + // Called when button is clicked + public void actionPerformed(ActionEvent e) { + try { + String cmd = e.getActionCommand(); + if (cmd == "Stop") { + if (player.isPlaying()) + player.pause(); + else + player.play(); + return; + } + + if (cmd == "Prev") { + player.prev(); + return; + } + + if (cmd == "Next") { + player.next(); + return; + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + // Called when applet's page isn't active + public void stop() { + if (!backgroundPlayback) + stopFile(); + } + + public void destroy() { + try { + stopFile(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Channel.java b/src/uk/me/fantastic/retro/music/ibxm/Channel.java new file mode 100755 index 0000000..8cd34c8 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Channel.java @@ -0,0 +1,648 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Channel { + public static final int NEAREST = 0, LINEAR = 1, SINC = 2; + + private static int[] exp2Table = { + 32768, 32946, 33125, 33305, 33486, 33667, 33850, 34034, + 34219, 34405, 34591, 34779, 34968, 35158, 35349, 35541, + 35734, 35928, 36123, 36319, 36516, 36715, 36914, 37114, + 37316, 37518, 37722, 37927, 38133, 38340, 38548, 38757, + 38968, 39180, 39392, 39606, 39821, 40037, 40255, 40473, + 40693, 40914, 41136, 41360, 41584, 41810, 42037, 42265, + 42495, 42726, 42958, 43191, 43425, 43661, 43898, 44137, + 44376, 44617, 44859, 45103, 45348, 45594, 45842, 46091, + 46341, 46593, 46846, 47100, 47356, 47613, 47871, 48131, + 48393, 48655, 48920, 49185, 49452, 49721, 49991, 50262, + 50535, 50810, 51085, 51363, 51642, 51922, 52204, 52488, + 52773, 53059, 53347, 53637, 53928, 54221, 54515, 54811, + 55109, 55408, 55709, 56012, 56316, 56622, 56929, 57238, + 57549, 57861, 58176, 58491, 58809, 59128, 59449, 59772, + 60097, 60423, 60751, 61081, 61413, 61746, 62081, 62419, + 62757, 63098, 63441, 63785, 64132, 64480, 64830, 65182, + 65536 + }; + + private static final short[] sineTable = { + 0, 24, 49, 74, 97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253, + 255, 253, 250, 244, 235, 224, 212, 197, 180, 161, 141, 120, 97, 74, 49, 24 + }; + + private Module module; + private GlobalVol globalVol; + private Instrument instrument; + private Sample sample; + private boolean keyOn; + private int noteKey, noteIns, noteVol, noteEffect, noteParam; + private int sampleOffset, sampleIdx, sampleFra, freq, ampl, pann; + private int volume, panning, fadeOutVol, volEnvTick, panEnvTick; + private int period, portaPeriod, retrigCount, fxCount, autoVibratoCount; + private int portaUpParam, portaDownParam, tonePortaParam, offsetParam; + private int finePortaUpParam, finePortaDownParam, extraFinePortaParam; + private int arpeggioParam, vslideParam, globalVslideParam, panningSlideParam; + private int fineVslideUpParam, fineVslideDownParam; + private int retrigVolume, retrigTicks, tremorOnTicks, tremorOffTicks; + private int vibratoType, vibratoPhase, vibratoSpeed, vibratoDepth; + private int tremoloType, tremoloPhase, tremoloSpeed, tremoloDepth; + private int tremoloAdd, vibratoAdd, arpeggioAdd; + private int id, randomSeed; + public int plRow; + + public Channel(Module module, int id, GlobalVol globalVol) { + this.module = module; + this.id = id; + this.globalVol = globalVol; + panning = module.defaultPanning[id]; + instrument = new Instrument(); + sample = instrument.samples[0]; + randomSeed = (id + 1) * 0xABCDEF; + } + + public void resample(int[] outBuf, int offset, int length, int sampleRate, int interpolation) { + if (ampl <= 0) return; + int lAmpl = ampl * (255 - pann) >> 8; + int rAmpl = ampl * pann >> 8; + int step = (freq << (Sample.FP_SHIFT - 3)) / (sampleRate >> 3); + switch (interpolation) { + case NEAREST: + sample.resampleNearest(sampleIdx, sampleFra, step, lAmpl, rAmpl, outBuf, offset, length); + break; + case LINEAR: + default: + sample.resampleLinear(sampleIdx, sampleFra, step, lAmpl, rAmpl, outBuf, offset, length); + break; + case SINC: + sample.resampleSinc(sampleIdx, sampleFra, step, lAmpl, rAmpl, outBuf, offset, length); + break; + } + } + + public void updateSampleIdx(int length, int sampleRate) { + int step = (freq << (Sample.FP_SHIFT - 3)) / (sampleRate >> 3); + sampleFra += step * length; + sampleIdx = sample.normaliseSampleIdx(sampleIdx + (sampleFra >> Sample.FP_SHIFT)); + sampleFra &= Sample.FP_MASK; + } + + public void row(Note note) { + noteKey = note.key; + noteIns = note.instrument; + noteVol = note.volume; + noteEffect = note.effect; + noteParam = note.param; + retrigCount++; + vibratoAdd = tremoloAdd = arpeggioAdd = fxCount = 0; + if (!((noteEffect == 0x7D || noteEffect == 0xFD) && noteParam > 0)) { + /* Not note delay.*/ + trigger(); + } + switch (noteEffect) { + case 0x01: + case 0x86: /* Porta Up. */ + if (noteParam > 0) portaUpParam = noteParam; + portamentoUp(portaUpParam); + break; + case 0x02: + case 0x85: /* Porta Down. */ + if (noteParam > 0) portaDownParam = noteParam; + portamentoDown(portaDownParam); + break; + case 0x03: + case 0x87: /* Tone Porta. */ + if (noteParam > 0) tonePortaParam = noteParam; + break; + case 0x04: + case 0x88: /* Vibrato. */ + if ((noteParam >> 4) > 0) vibratoSpeed = noteParam >> 4; + if ((noteParam & 0xF) > 0) vibratoDepth = noteParam & 0xF; + vibrato(false); + break; + case 0x05: + case 0x8C: /* Tone Porta + Vol Slide. */ + if (noteParam > 0) vslideParam = noteParam; + volumeSlide(); + break; + case 0x06: + case 0x8B: /* Vibrato + Vol Slide. */ + if (noteParam > 0) vslideParam = noteParam; + vibrato(false); + volumeSlide(); + break; + case 0x07: + case 0x92: /* Tremolo. */ + if ((noteParam >> 4) > 0) tremoloSpeed = noteParam >> 4; + if ((noteParam & 0xF) > 0) tremoloDepth = noteParam & 0xF; + tremolo(); + break; + case 0x08: /* Set Panning.*/ + panning = (noteParam < 128) ? (noteParam << 1) : 255; + break; + case 0x0A: + case 0x84: /* Vol Slide. */ + if (noteParam > 0) vslideParam = noteParam; + volumeSlide(); + break; + case 0x0C: /* Set Volume. */ + volume = noteParam >= 64 ? 64 : noteParam & 0x3F; + break; + case 0x10: + case 0x96: /* Set Global Volume. */ + globalVol.volume = noteParam >= 64 ? 64 : noteParam & 0x3F; + break; + case 0x11: /* Global Volume Slide. */ + if (noteParam > 0) globalVslideParam = noteParam; + break; + case 0x14: /* Key Off. */ + keyOn = false; + break; + case 0x15: /* Set Envelope Tick. */ + volEnvTick = panEnvTick = noteParam & 0xFF; + break; + case 0x19: /* Panning Slide. */ + if (noteParam > 0) panningSlideParam = noteParam; + break; + case 0x1B: + case 0x91: /* Retrig + Vol Slide. */ + if ((noteParam >> 4) > 0) retrigVolume = noteParam >> 4; + if ((noteParam & 0xF) > 0) retrigTicks = noteParam & 0xF; + retrigVolSlide(); + break; + case 0x1D: + case 0x89: /* Tremor. */ + if ((noteParam >> 4) > 0) tremorOnTicks = noteParam >> 4; + if ((noteParam & 0xF) > 0) tremorOffTicks = noteParam & 0xF; + tremor(); + break; + case 0x21: /* Extra Fine Porta. */ + if (noteParam > 0) extraFinePortaParam = noteParam; + switch (extraFinePortaParam & 0xF0) { + case 0x10: + portamentoUp(0xE0 | (extraFinePortaParam & 0xF)); + break; + case 0x20: + portamentoDown(0xE0 | (extraFinePortaParam & 0xF)); + break; + } + break; + case 0x71: /* Fine Porta Up. */ + if (noteParam > 0) finePortaUpParam = noteParam; + portamentoUp(0xF0 | (finePortaUpParam & 0xF)); + break; + case 0x72: /* Fine Porta Down. */ + if (noteParam > 0) finePortaDownParam = noteParam; + portamentoDown(0xF0 | (finePortaDownParam & 0xF)); + break; + case 0x74: + case 0xF3: /* Set Vibrato Waveform. */ + if (noteParam < 8) vibratoType = noteParam; + break; + case 0x77: + case 0xF4: /* Set Tremolo Waveform. */ + if (noteParam < 8) tremoloType = noteParam; + break; + case 0x7A: /* Fine Vol Slide Up. */ + if (noteParam > 0) fineVslideUpParam = noteParam; + volume += fineVslideUpParam; + if (volume > 64) volume = 64; + break; + case 0x7B: /* Fine Vol Slide Down. */ + if (noteParam > 0) fineVslideDownParam = noteParam; + volume -= fineVslideDownParam; + if (volume < 0) volume = 0; + break; + case 0x7C: + case 0xFC: /* Note Cut. */ + if (noteParam <= 0) volume = 0; + break; + case 0x8A: /* Arpeggio. */ + if (noteParam > 0) arpeggioParam = noteParam; + break; + case 0x95: /* Fine Vibrato.*/ + if ((noteParam >> 4) > 0) vibratoSpeed = noteParam >> 4; + if ((noteParam & 0xF) > 0) vibratoDepth = noteParam & 0xF; + vibrato(true); + break; + case 0xF8: /* Set Panning. */ + panning = noteParam * 17; + break; + } + autoVibrato(); + calculateFrequency(); + calculateAmplitude(); + updateEnvelopes(); + } + + public void tick() { + vibratoAdd = 0; + fxCount++; + retrigCount++; + if (!(noteEffect == 0x7D && fxCount <= noteParam)) { + switch (noteVol & 0xF0) { + case 0x60: /* Vol Slide Down.*/ + volume -= noteVol & 0xF; + if (volume < 0) volume = 0; + break; + case 0x70: /* Vol Slide Up.*/ + volume += noteVol & 0xF; + if (volume > 64) volume = 64; + break; + case 0xB0: /* Vibrato.*/ + vibratoPhase += vibratoSpeed; + vibrato(false); + break; + case 0xD0: /* Pan Slide Left.*/ + panning -= noteVol & 0xF; + if (panning < 0) panning = 0; + break; + case 0xE0: /* Pan Slide Right.*/ + panning += noteVol & 0xF; + if (panning > 255) panning = 255; + break; + case 0xF0: /* Tone Porta.*/ + tonePortamento(); + break; + } + } + switch (noteEffect) { + case 0x01: + case 0x86: /* Porta Up. */ + portamentoUp(portaUpParam); + break; + case 0x02: + case 0x85: /* Porta Down. */ + portamentoDown(portaDownParam); + break; + case 0x03: + case 0x87: /* Tone Porta. */ + tonePortamento(); + break; + case 0x04: + case 0x88: /* Vibrato. */ + vibratoPhase += vibratoSpeed; + vibrato(false); + break; + case 0x05: + case 0x8C: /* Tone Porta + Vol Slide. */ + tonePortamento(); + volumeSlide(); + break; + case 0x06: + case 0x8B: /* Vibrato + Vol Slide. */ + vibratoPhase += vibratoSpeed; + vibrato(false); + volumeSlide(); + break; + case 0x07: + case 0x92: /* Tremolo. */ + tremoloPhase += tremoloSpeed; + tremolo(); + break; + case 0x0A: + case 0x84: /* Vol Slide. */ + volumeSlide(); + break; + case 0x11: /* Global Volume Slide. */ + globalVol.volume += (globalVslideParam >> 4) - (globalVslideParam & 0xF); + if (globalVol.volume < 0) globalVol.volume = 0; + if (globalVol.volume > 64) globalVol.volume = 64; + break; + case 0x19: /* Panning Slide. */ + panning += (panningSlideParam >> 4) - (panningSlideParam & 0xF); + if (panning < 0) panning = 0; + if (panning > 255) panning = 255; + break; + case 0x1B: + case 0x91: /* Retrig + Vol Slide. */ + retrigVolSlide(); + break; + case 0x1D: + case 0x89: /* Tremor. */ + tremor(); + break; + case 0x79: /* Retrig. */ + if (fxCount >= noteParam) { + fxCount = 0; + sampleIdx = sampleFra = 0; + } + break; + case 0x7C: + case 0xFC: /* Note Cut. */ + if (noteParam == fxCount) volume = 0; + break; + case 0x7D: + case 0xFD: /* Note Delay. */ + if (noteParam == fxCount) trigger(); + break; + case 0x8A: /* Arpeggio. */ + if (fxCount > 2) fxCount = 0; + if (fxCount == 0) arpeggioAdd = 0; + if (fxCount == 1) arpeggioAdd = arpeggioParam >> 4; + if (fxCount == 2) arpeggioAdd = arpeggioParam & 0xF; + break; + case 0x95: /* Fine Vibrato. */ + vibratoPhase += vibratoSpeed; + vibrato(true); + break; + } + autoVibrato(); + calculateFrequency(); + calculateAmplitude(); + updateEnvelopes(); + } + + private void updateEnvelopes() { + if (instrument.volumeEnvelope.enabled) { + if (!keyOn) { + fadeOutVol -= instrument.volumeFadeOut; + if (fadeOutVol < 0) fadeOutVol = 0; + } + volEnvTick = instrument.volumeEnvelope.nextTick(volEnvTick, keyOn); + } + if (instrument.panningEnvelope.enabled) + panEnvTick = instrument.panningEnvelope.nextTick(panEnvTick, keyOn); + } + + private void autoVibrato() { + int depth = instrument.vibratoDepth & 0x7F; + if (depth > 0) { + int sweep = instrument.vibratoSweep & 0x7F; + int rate = instrument.vibratoRate & 0x7F; + int type = instrument.vibratoType; + if (autoVibratoCount < sweep) depth = depth * autoVibratoCount / sweep; + vibratoAdd += waveform(autoVibratoCount * rate >> 2, type + 4) * depth >> 8; + autoVibratoCount++; + } + } + + private void volumeSlide() { + int up = vslideParam >> 4; + int down = vslideParam & 0xF; + if (down == 0xF && up > 0) { /* Fine slide up.*/ + if (fxCount == 0) volume += up; + } else if (up == 0xF && down > 0) { /* Fine slide down.*/ + if (fxCount == 0) volume -= down; + } else if (fxCount > 0 || module.fastVolSlides) /* Normal.*/ + volume += up - down; + if (volume > 64) volume = 64; + if (volume < 0) volume = 0; + } + + private void portamentoUp(int param) { + switch (param & 0xF0) { + case 0xE0: /* Extra-fine porta.*/ + if (fxCount == 0) period -= param & 0xF; + break; + case 0xF0: /* Fine porta.*/ + if (fxCount == 0) period -= (param & 0xF) << 2; + break; + default:/* Normal porta.*/ + if (fxCount > 0) period -= param << 2; + break; + } + if (period < 0) period = 0; + } + + private void portamentoDown(int param) { + if (period > 0) { + switch (param & 0xF0) { + case 0xE0: /* Extra-fine porta.*/ + if (fxCount == 0) period += param & 0xF; + break; + case 0xF0: /* Fine porta.*/ + if (fxCount == 0) period += (param & 0xF) << 2; + break; + default:/* Normal porta.*/ + if (fxCount > 0) period += param << 2; + break; + } + if (period > 65535) period = 65535; + } + } + + private void tonePortamento() { + if (period > 0) { + if (period < portaPeriod) { + period += tonePortaParam << 2; + if (period > portaPeriod) period = portaPeriod; + } else { + period -= tonePortaParam << 2; + if (period < portaPeriod) period = portaPeriod; + } + } + } + + private void vibrato(boolean fine) { + vibratoAdd = waveform(vibratoPhase, vibratoType & 0x3) * vibratoDepth >> (fine ? 7 : 5); + } + + private void tremolo() { + tremoloAdd = waveform(tremoloPhase, tremoloType & 0x3) * tremoloDepth >> 6; + } + + private int waveform(int phase, int type) { + int amplitude = 0; + switch (type) { + default: /* Sine. */ + amplitude = sineTable[phase & 0x1F]; + if ((phase & 0x20) > 0) amplitude = -amplitude; + break; + case 6: /* Saw Up.*/ + amplitude = (((phase + 0x20) & 0x3F) << 3) - 255; + break; + case 1: + case 7: /* Saw Down. */ + amplitude = 255 - (((phase + 0x20) & 0x3F) << 3); + break; + case 2: + case 5: /* Square. */ + amplitude = (phase & 0x20) > 0 ? 255 : -255; + break; + case 3: + case 8: /* Random. */ + amplitude = (randomSeed >> 20) - 255; + randomSeed = (randomSeed * 65 + 17) & 0x1FFFFFFF; + break; + } + return amplitude; + } + + private void tremor() { + if (retrigCount >= tremorOnTicks) tremoloAdd = -64; + if (retrigCount >= (tremorOnTicks + tremorOffTicks)) + tremoloAdd = retrigCount = 0; + } + + private void retrigVolSlide() { + if (retrigCount >= retrigTicks) { + retrigCount = sampleIdx = sampleFra = 0; + switch (retrigVolume) { + case 0x1: + volume = volume - 1; + break; + case 0x2: + volume = volume - 2; + break; + case 0x3: + volume = volume - 4; + break; + case 0x4: + volume = volume - 8; + break; + case 0x5: + volume = volume - 16; + break; + case 0x6: + volume = volume * 2 / 3; + break; + case 0x7: + volume = volume >> 1; + break; + case 0x8: /* ? */ + break; + case 0x9: + volume = volume + 1; + break; + case 0xA: + volume = volume + 2; + break; + case 0xB: + volume = volume + 4; + break; + case 0xC: + volume = volume + 8; + break; + case 0xD: + volume = volume + 16; + break; + case 0xE: + volume = volume * 3 / 2; + break; + case 0xF: + volume = volume << 1; + break; + } + if (volume < 0) volume = 0; + if (volume > 64) volume = 64; + } + } + + private void calculateFrequency() { + int per = period + vibratoAdd; + if (module.linearPeriods) { + per = per - (arpeggioAdd << 6); + if (per < 28 || per > 7680) per = 7680; + freq = ((module.c2Rate >> 4) * exp2(((4608 - per) << Sample.FP_SHIFT) / 768)) >> (Sample.FP_SHIFT - 4); + } else { + if (per > 29021) per = 29021; + per = (per << Sample.FP_SHIFT) / exp2((arpeggioAdd << Sample.FP_SHIFT) / 12); + if (per < 28) per = 29021; + freq = module.c2Rate * 1712 / per; + } + } + + private void calculateAmplitude() { + int envVol = keyOn ? 64 : 0; + if (instrument.volumeEnvelope.enabled) + envVol = instrument.volumeEnvelope.calculateAmpl(volEnvTick); + int vol = volume + tremoloAdd; + if (vol > 64) vol = 64; + if (vol < 0) vol = 0; + vol = (vol * module.gain * Sample.FP_ONE) >> 13; + vol = (vol * fadeOutVol) >> 15; + ampl = (vol * globalVol.volume * envVol) >> 12; + int envPan = 32; + if (instrument.panningEnvelope.enabled) + envPan = instrument.panningEnvelope.calculateAmpl(panEnvTick); + int panRange = (panning < 128) ? panning : (255 - panning); + pann = panning + (panRange * (envPan - 32) >> 5); + } + + private void trigger() { + if (noteIns > 0 && noteIns <= module.numInstruments) { + instrument = module.instruments[noteIns]; + Sample sam = instrument.samples[instrument.keyToSample[noteKey < 97 ? noteKey : 0]]; + volume = sam.volume >= 64 ? 64 : sam.volume & 0x3F; + if (sam.panning >= 0) panning = sam.panning & 0xFF; + if (period > 0 && sam.looped()) sample = sam; /* Amiga trigger.*/ + sampleOffset = volEnvTick = panEnvTick = 0; + fadeOutVol = 32768; + keyOn = true; + } + if (noteEffect == 0x09 || noteEffect == 0x8F) { /* Set Sample Offset. */ + if (noteParam > 0) offsetParam = noteParam; + sampleOffset = offsetParam << 8; + } + if (noteVol >= 0x10 && noteVol < 0x60) + volume = noteVol < 0x50 ? noteVol - 0x10 : 64; + switch (noteVol & 0xF0) { + case 0x80: /* Fine Vol Down.*/ + volume -= noteVol & 0xF; + if (volume < 0) volume = 0; + break; + case 0x90: /* Fine Vol Up.*/ + volume += noteVol & 0xF; + if (volume > 64) volume = 64; + break; + case 0xA0: /* Set Vibrato Speed.*/ + if ((noteVol & 0xF) > 0) vibratoSpeed = noteVol & 0xF; + break; + case 0xB0: /* Vibrato.*/ + if ((noteVol & 0xF) > 0) vibratoDepth = noteVol & 0xF; + vibrato(false); + break; + case 0xC0: /* Set Panning.*/ + panning = (noteVol & 0xF) * 17; + break; + case 0xF0: /* Tone Porta.*/ + if ((noteVol & 0xF) > 0) tonePortaParam = noteVol & 0xF; + break; + } + if (noteKey > 0) { + if (noteKey > 96) { + keyOn = false; + } else { + boolean isPorta = (noteVol & 0xF0) == 0xF0 || + noteEffect == 0x03 || noteEffect == 0x05 || + noteEffect == 0x87 || noteEffect == 0x8C; + if (!isPorta) sample = instrument.samples[instrument.keyToSample[noteKey]]; + int fineTune = sample.fineTune; + if (noteEffect == 0x75 || noteEffect == 0xF2) { /* Set Fine Tune. */ + fineTune = ((noteParam & 0xF) << 4) - 128; + } + int key = noteKey + sample.relNote; + if (key < 1) key = 1; + if (key > 120) key = 120; + int per = (key << 6) + (fineTune >> 1); + if (module.linearPeriods) { + portaPeriod = 7744 - per; + } else { + portaPeriod = 29021 * exp2((per << Sample.FP_SHIFT) / -768) >> Sample.FP_SHIFT; + } + if (!isPorta) { + period = portaPeriod; + sampleIdx = sampleOffset; + sampleFra = 0; + if (vibratoType < 4) vibratoPhase = 0; + if (tremoloType < 4) tremoloPhase = 0; + retrigCount = autoVibratoCount = 0; + } + } + } + } + + public static int exp2(int x) { + int x0 = (x & Sample.FP_MASK) >> (Sample.FP_SHIFT - 7); + int c = exp2Table[x0]; + int m = exp2Table[x0 + 1] - c; + int y = (m * (x & (Sample.FP_MASK >> 7)) >> 8) + c; + return (y << Sample.FP_SHIFT) >> (Sample.FP_SHIFT - (x >> Sample.FP_SHIFT)); + } + + public static int log2(int x) { + int y = 16 << Sample.FP_SHIFT; + for (int step = y; step > 0; step >>= 1) { + if (exp2(y - step) >= x) y -= step; + } + return y; + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Data.java b/src/uk/me/fantastic/retro/music/ibxm/Data.java new file mode 100755 index 0000000..1f3961b --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Data.java @@ -0,0 +1,152 @@ +package uk.me.fantastic.retro.music.ibxm; + +/* A data array dynamically loaded from an InputStream. */ +public class Data { + private int bufLen; + private byte[] buffer; + private java.io.InputStream stream; + + public Data(java.io.InputStream inputStream) throws java.io.IOException { + bufLen = 1 << 16; + buffer = new byte[bufLen]; + stream = inputStream; + readFully(stream, buffer, 0, bufLen); + } + + public Data(byte[] data) { + bufLen = data.length; + buffer = data; + } + + public byte sByte(int offset) throws java.io.IOException { + load(offset, 1); + return buffer[offset]; + } + + public int uByte(int offset) throws java.io.IOException { + load(offset, 1); + return buffer[offset] & 0xFF; + } + + public int ubeShort(int offset) throws java.io.IOException { + load(offset, 2); + return ((buffer[offset] & 0xFF) << 8) | (buffer[offset + 1] & 0xFF); + } + + public int uleShort(int offset) throws java.io.IOException { + load(offset, 2); + return (buffer[offset] & 0xFF) | ((buffer[offset + 1] & 0xFF) << 8); + } + + public int uleInt(int offset) throws java.io.IOException { + load(offset, 4); + int value = buffer[offset] & 0xFF; + value = value | ((buffer[offset + 1] & 0xFF) << 8); + value = value | ((buffer[offset + 2] & 0xFF) << 16); + value = value | ((buffer[offset + 3] & 0x7F) << 24); + return value; + } + + public String strLatin1(int offset, int length) throws java.io.IOException { + load(offset, length); + char[] str = new char[length]; + for (int idx = 0; idx < length; idx++) { + int chr = buffer[offset + idx] & 0xFF; + str[idx] = chr < 32 ? 32 : (char) chr; + } + return new String(str); + } + + public String strCp850(int offset, int length) throws java.io.IOException { + load(offset, length); + try { + char[] str = new String(buffer, offset, length, "Cp850").toCharArray(); + for (int idx = 0; idx < str.length; idx++) { + str[idx] = str[idx] < 32 ? 32 : str[idx]; + } + return new String(str); + } catch (java.io.UnsupportedEncodingException e) { + return strLatin1(offset, length); + } + } + + public short[] samS8(int offset, int length) throws java.io.IOException { + load(offset, length); + short[] sampleData = new short[length]; + for (int idx = 0; idx < length; idx++) { + sampleData[idx] = (short) (buffer[offset + idx] << 8); + } + return sampleData; + } + + public short[] samS8D(int offset, int length) throws java.io.IOException { + load(offset, length); + short[] sampleData = new short[length]; + int sam = 0; + for (int idx = 0; idx < length; idx++) { + sam += buffer[offset + idx]; + sampleData[idx] = (short) (sam << 8); + } + return sampleData; + } + + public short[] samU8(int offset, int length) throws java.io.IOException { + load(offset, length); + short[] sampleData = new short[length]; + for (int idx = 0; idx < length; idx++) { + sampleData[idx] = (short) (((buffer[offset + idx] & 0xFF) - 128) << 8); + } + return sampleData; + } + + public short[] samS16(int offset, int samples) throws java.io.IOException { + load(offset, samples * 2); + short[] sampleData = new short[samples]; + for (int idx = 0; idx < samples; idx++) { + sampleData[idx] = (short) ((buffer[offset + idx * 2] & 0xFF) | (buffer[offset + idx * 2 + 1] << 8)); + } + return sampleData; + } + + public short[] samS16D(int offset, int samples) throws java.io.IOException { + load(offset, samples * 2); + short[] sampleData = new short[samples]; + int sam = 0; + for (int idx = 0; idx < samples; idx++) { + sam += (buffer[offset + idx * 2] & 0xFF) | (buffer[offset + idx * 2 + 1] << 8); + sampleData[idx] = (short) sam; + } + return sampleData; + } + + public short[] samU16(int offset, int samples) throws java.io.IOException { + load(offset, samples * 2); + short[] sampleData = new short[samples]; + for (int idx = 0; idx < samples; idx++) { + int sam = (buffer[offset + idx * 2] & 0xFF) | ((buffer[offset + idx * 2 + 1] & 0xFF) << 8); + sampleData[idx] = (short) (sam - 32768); + } + return sampleData; + } + + private void load(int offset, int length) throws java.io.IOException { + while (offset + length > bufLen) { + int newBufLen = bufLen << 1; + byte[] newBuf = new byte[newBufLen]; + System.arraycopy(buffer, 0, newBuf, 0, bufLen); + if (stream != null) { + readFully(stream, newBuf, bufLen, newBufLen - bufLen); + } + bufLen = newBufLen; + buffer = newBuf; + } + } + + private static void readFully(java.io.InputStream inputStream, byte[] buffer, int offset, int length) throws java.io.IOException { + int read = 1, end = offset + length; + while (read > 0) { + read = inputStream.read(buffer, offset, end - offset); + offset += read; + } + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Envelope.java b/src/uk/me/fantastic/retro/music/ibxm/Envelope.java new file mode 100755 index 0000000..c4b01f4 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Envelope.java @@ -0,0 +1,45 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Envelope { + public boolean enabled = false, sustain = false, looped = false; + public int sustainTick = 0, loopStartTick = 0, loopEndTick = 0; + public int numPoints = 1; + public int[] pointsTick = new int[1]; + public int[] pointsAmpl = new int[1]; + + public int nextTick(int tick, boolean keyOn) { + tick++; + if (looped && tick >= loopEndTick) tick = loopStartTick; + if (sustain && keyOn && tick >= sustainTick) tick = sustainTick; + return tick; + } + + public int calculateAmpl(int tick) { + int ampl = pointsAmpl[numPoints - 1]; + if (tick < pointsTick[numPoints - 1]) { + int point = 0; + for (int idx = 1; idx < numPoints; idx++) + if (pointsTick[idx] <= tick) point = idx; + int dt = pointsTick[point + 1] - pointsTick[point]; + int da = pointsAmpl[point + 1] - pointsAmpl[point]; + ampl = pointsAmpl[point]; + ampl += ((da << 24) / dt) * (tick - pointsTick[point]) >> 24; + } + return ampl; + } + + public void toStringBuffer(StringBuffer out) { + out.append("Enabled: " + enabled + '\n'); + out.append("Sustain: " + sustain + '\n'); + out.append("Looped: " + looped + '\n'); + out.append("Sustain Tick: " + sustainTick + '\n'); + out.append("Loop Start Tick: " + loopStartTick + '\n'); + out.append("Loop End Tick: " + loopEndTick + '\n'); + out.append("Num Points: " + numPoints + '\n'); + out.append("Points: "); + for (int point = 0; point < numPoints; point++) { + out.append("(" + pointsTick[point] + ", " + pointsAmpl[point] + "), "); + } + out.append('\n'); + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/GlobalVol.java b/src/uk/me/fantastic/retro/music/ibxm/GlobalVol.java new file mode 100755 index 0000000..bcf5ac7 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/GlobalVol.java @@ -0,0 +1,5 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class GlobalVol { + public int volume; +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/IBXM.java b/src/uk/me/fantastic/retro/music/ibxm/IBXM.java new file mode 100755 index 0000000..0dd1c50 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/IBXM.java @@ -0,0 +1,288 @@ +package uk.me.fantastic.retro.music.ibxm; + +/* + ProTracker, Scream Tracker 3, FastTracker 2 Replay (c)2016 mumart@gmail.com +*/ +public class IBXM { + public static final String VERSION = "a72 (c)2016 mumart@gmail.com"; + + private Module module; + private int[] rampBuf; + private byte[][] playCount; + private Channel[] channels; + private int sampleRate, interpolation; + private int seqPos, breakSeqPos, row, nextRow, tick; + private int speed, tempo, plCount, plChannel; + private GlobalVol globalVol; + private Note note; + + /* Play the specified Module at the specified sampling rate. */ + public IBXM(Module module, int samplingRate) { + this.module = module; + setSampleRate(samplingRate); + interpolation = Channel.LINEAR; + rampBuf = new int[128]; + playCount = new byte[module.sequenceLength][]; + channels = new Channel[module.numChannels]; + globalVol = new GlobalVol(); + note = new Note(); + setSequencePos(0); + } + + /* Return the sampling rate of playback. */ + public int getSampleRate() { + return sampleRate; + } + + /* Set the sampling rate of playback. + This can be used with Module.c2Rate to adjust the tempo and pitch. */ + public void setSampleRate(int rate) { + if (rate < 8000 || rate > 128000) { + throw new IllegalArgumentException("Unsupported sampling rate!"); + } + sampleRate = rate; + } + + /* Set the resampling quality to one of + Channel.NEAREST, Channel.LINEAR, or Channel.SINC. */ + public void setInterpolation(int interpolation) { + this.interpolation = interpolation; + } + + /* Returns the length of the buffer required by getAudio(). */ + public int getMixBufferLength() { + return (calculateTickLen(32, 128000) + 65) * 4; + } + + /* Get the current row position. */ + public int getRow() { + return row; + } + + /* Get the current pattern position in the sequence. */ + public int getSequencePos() { + return seqPos; + } + + /* Set the pattern in the sequence to play. The tempo is reset to the default. */ + public void setSequencePos(int pos) { + if (pos >= module.sequenceLength) pos = 0; + breakSeqPos = pos; + nextRow = 0; + tick = 1; + globalVol.volume = module.defaultGVol; + speed = module.defaultSpeed > 0 ? module.defaultSpeed : 6; + tempo = module.defaultTempo > 0 ? module.defaultTempo : 125; + plCount = plChannel = -1; + for (int idx = 0; idx < playCount.length; idx++) { + int patIdx = module.sequence[idx]; + int numRows = (patIdx < module.numPatterns) ? module.patterns[patIdx].numRows : 0; + playCount[idx] = new byte[numRows]; + } + for (int idx = 0; idx < module.numChannels; idx++) + channels[idx] = new Channel(module, idx, globalVol); + for (int idx = 0; idx < 128; idx++) + rampBuf[idx] = 0; + tick(); + } + + /* Returns the song duration in samples at the current sampling rate. */ + public int calculateSongDuration() { + int duration = 0; + setSequencePos(0); + boolean songEnd = false; + while (!songEnd) { + duration += calculateTickLen(tempo, sampleRate); + songEnd = tick(); + } + setSequencePos(0); + return duration; + } + + /* Seek to approximately the specified sample position. + The actual sample position reached is returned. */ + public int seek(int samplePos) { + setSequencePos(0); + int currentPos = 0; + int tickLen = calculateTickLen(tempo, sampleRate); + while ((samplePos - currentPos) >= tickLen) { + for (int idx = 0; idx < module.numChannels; idx++) + channels[idx].updateSampleIdx(tickLen * 2, sampleRate * 2); + currentPos += tickLen; + tick(); + tickLen = calculateTickLen(tempo, sampleRate); + } + return currentPos; + } + + /* Seek to the specified position and row in the sequence. */ + public void seekSequencePos(int sequencePos, int sequenceRow) { + setSequencePos(0); + if (sequencePos < 0 || sequencePos >= module.sequenceLength) + sequencePos = 0; + if (sequenceRow >= module.patterns[module.sequence[sequencePos]].numRows) + sequenceRow = 0; + while (seqPos < sequencePos || row < sequenceRow) { + int tickLen = calculateTickLen(tempo, sampleRate); + for (int idx = 0; idx < module.numChannels; idx++) + channels[idx].updateSampleIdx(tickLen * 2, sampleRate * 2); + if (tick()) { + /* Song end reached. */ + setSequencePos(sequencePos); + return; + } + } + } + + /* Generate audio. + The number of samples placed into outputBuf is returned. + The output buffer length must be at least that returned by getMixBufferLength(). + A "sample" is a pair of 16-bit integer amplitudes, one for each of the stereo channels. */ + public int getAudio(int[] outputBuf) { + int tickLen = calculateTickLen(tempo, sampleRate); + /* Clear output buffer. */ + for (int idx = 0, end = (tickLen + 65) * 4; idx < end; idx++) + outputBuf[idx] = 0; + /* Resample. */ + for (int chanIdx = 0; chanIdx < module.numChannels; chanIdx++) { + Channel chan = channels[chanIdx]; + chan.resample(outputBuf, 0, (tickLen + 65) * 2, sampleRate * 2, interpolation); + chan.updateSampleIdx(tickLen * 2, sampleRate * 2); + } + downsample(outputBuf, tickLen + 64); + volumeRamp(outputBuf, tickLen); + tick(); + return tickLen; + } + + private int calculateTickLen(int tempo, int samplingRate) { + return (samplingRate * 5) / (tempo * 2); + } + + private void volumeRamp(int[] mixBuf, int tickLen) { + int rampRate = 256 * 2048 / sampleRate; + for (int idx = 0, a1 = 0; a1 < 256; idx += 2, a1 += rampRate) { + int a2 = 256 - a1; + mixBuf[idx] = (mixBuf[idx] * a1 + rampBuf[idx] * a2) >> 8; + mixBuf[idx + 1] = (mixBuf[idx + 1] * a1 + rampBuf[idx + 1] * a2) >> 8; + } + System.arraycopy(mixBuf, tickLen * 2, rampBuf, 0, 128); + } + + private void downsample(int[] buf, int count) { + /* 2:1 downsampling with simple but effective anti-aliasing. Buf must contain count * 2 + 1 stereo samples. */ + int outLen = count * 2; + for (int inIdx = 0, outIdx = 0; outIdx < outLen; inIdx += 4, outIdx += 2) { + buf[outIdx] = (buf[inIdx] >> 2) + (buf[inIdx + 2] >> 1) + (buf[inIdx + 4] >> 2); + buf[outIdx + 1] = (buf[inIdx + 1] >> 2) + (buf[inIdx + 3] >> 1) + (buf[inIdx + 5] >> 2); + } + } + + private boolean tick() { + if (--tick <= 0) { + tick = speed; + row(); + } else { + for (int idx = 0; idx < module.numChannels; idx++) channels[idx].tick(); + } + return playCount[seqPos][row] > 1; + } + + private void row() { + if (breakSeqPos >= 0) { + if (breakSeqPos >= module.sequenceLength) breakSeqPos = nextRow = 0; + while (module.sequence[breakSeqPos] >= module.numPatterns) { + breakSeqPos++; + if (breakSeqPos >= module.sequenceLength) breakSeqPos = nextRow = 0; + } + seqPos = breakSeqPos; + for (int idx = 0; idx < module.numChannels; idx++) channels[idx].plRow = 0; + breakSeqPos = -1; + } + Pattern pattern = module.patterns[module.sequence[seqPos]]; + row = nextRow; + if (row >= pattern.numRows) row = 0; + int count = playCount[seqPos][row]; + if (plCount < 0 && count < 127) { + playCount[seqPos][row] = (byte) (count + 1); + } + nextRow = row + 1; + if (nextRow >= pattern.numRows) { + breakSeqPos = seqPos + 1; + nextRow = 0; + } + int noteIdx = row * module.numChannels; + for (int chanIdx = 0; chanIdx < module.numChannels; chanIdx++) { + Channel channel = channels[chanIdx]; + pattern.getNote(noteIdx + chanIdx, note); + if (note.effect == 0xE) { + note.effect = 0x70 | (note.param >> 4); + note.param &= 0xF; + } + if (note.effect == 0x93) { + note.effect = 0xF0 | (note.param >> 4); + note.param &= 0xF; + } + if (note.effect == 0 && note.param > 0) note.effect = 0x8A; + channel.row(note); + switch (note.effect) { + case 0x81: /* Set Speed. */ + if (note.param > 0) + tick = speed = note.param; + break; + case 0xB: + case 0x82: /* Pattern Jump.*/ + if (plCount < 0) { + breakSeqPos = note.param; + nextRow = 0; + } + break; + case 0xD: + case 0x83: /* Pattern Break.*/ + if (plCount < 0) { + if (breakSeqPos < 0) + breakSeqPos = seqPos + 1; + nextRow = (note.param >> 4) * 10 + (note.param & 0xF); + } + break; + case 0xF: /* Set Speed/Tempo.*/ + if (note.param > 0) { + if (note.param < 32) + tick = speed = note.param; + else + tempo = note.param; + } + break; + case 0x94: /* Set Tempo.*/ + if (note.param > 32) + tempo = note.param; + break; + case 0x76: + case 0xFB: /* Pattern Loop.*/ + if (note.param == 0) /* Set loop marker on this channel. */ + channel.plRow = row; + if (channel.plRow < row) { /* Marker valid. Begin looping. */ + if (plCount < 0) { /* Not already looping, begin. */ + plCount = note.param; + plChannel = chanIdx; + } + if (plChannel == chanIdx) { /* Next Loop.*/ + if (plCount == 0) { /* Loop finished. */ + /* Invalidate current marker. */ + channel.plRow = row + 1; + } else { /* Loop and cancel any breaks on this row. */ + nextRow = channel.plRow; + breakSeqPos = -1; + } + plCount--; + } + } + break; + case 0x7E: + case 0xFE: /* Pattern Delay.*/ + tick = speed + speed * note.param; + break; + } + } + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/IBXMPlayer.java b/src/uk/me/fantastic/retro/music/ibxm/IBXMPlayer.java new file mode 100755 index 0000000..999bca8 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/IBXMPlayer.java @@ -0,0 +1,206 @@ +package uk.me.fantastic.retro.music.ibxm; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.audio.AudioDevice; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Vector; + +import static uk.me.fantastic.retro.GlobalsKt.log; + +public class IBXMPlayer { + private static final String[] EXTENSIONS = {"mod", "ft", "s3m", "xm"}; + private static final int SAMPLE_RATE = 48000, FADE_SECONDS = 16, REVERB_MILLIS = 50; + + + private Module module; + private IBXM ibxm; + private volatile boolean playing; + private int[] reverbBuf; + private int interpolation, reverbIdx, reverbLen; + private int sliderPos, samplePos, duration; + private Thread playThread; + + public IBXMPlayer() { + + + } + +// public void playMod(FileHandle f) throws IOException{ +// loadModule(Gdx.files.absolute("/Volumes/Home/rich/Downloads/woutervl_spcadv.mod").file()); +// } + + public void loadModule(InputStream inputStream) throws IOException { + // InputStream inputStream = new FileInputStream( modFile ); + try { + Module module = new Module(inputStream); + IBXM ibxm = new IBXM(module, SAMPLE_RATE); + ibxm.setInterpolation(interpolation); + duration = ibxm.calculateSongDuration(); + synchronized (this) { + samplePos = sliderPos = 0; + + String songName = module.songName.trim(); + + System.out.println("song: " + songName); + + Vector vector = new Vector(); + Instrument[] instruments = module.instruments; + for (int idx = 0, len = instruments.length; idx < len; idx++) { + String name = instruments[idx].name; + if (name.trim().length() > 0) + vector.add(String.format("%03d: %s", idx, name)); + System.out.println("instrument: " + name); + } + + this.module = module; + this.ibxm = ibxm; + } + } finally { + inputStream.close(); + } + } + + public static short twoBytesToShort(byte b1, byte b2) { + return (short) ((b1 << 8) | (b2 & 0xFF)); + } + + public synchronized void play(float volume) { + final AudioDevice audioDevice = Gdx.audio.newAudioDevice(SAMPLE_RATE, false); + audioDevice.setVolume(volume); + if (ibxm != null) { + playing = true; + playThread = new Thread(new Runnable() { + public void run() { + Gdx.app.log("IBXM", "thread start"); + int[] mixBuf = new int[ibxm.getMixBufferLength()]; + + short[] gBuf = new short[ibxm.getMixBufferLength()]; + + try { + + + long time = System.nanoTime(); + while (playing) { + long elapsed = System.nanoTime() - time; + time = System.nanoTime(); + + int count = getAudio(mixBuf); + + // Gdx.app.log("IBMX","playing "+count+ " time "+elapsed/1000000+" "); + //IBMX: playing 800time 12548322 + // 55855418 + + int outIdx = 0; + for (int mixIdx = 0; mixIdx < count * 2; mixIdx++) { + + + int ampl = mixBuf[mixIdx]; + if (ampl > 32767) { + ampl = 32767; + + } + if (ampl < -32768) { + ampl = -32768; + + } + + + outIdx++; + + int j = mixIdx * 2; + byte b1 = (byte) (ampl >> 8); + byte b2 = (byte) ampl; + gBuf[mixIdx] = twoBytesToShort(b1, b2); + } + long t = System.nanoTime(); + audioDevice.writeSamples(gBuf, 0, outIdx); + long t2 = System.nanoTime() - t; + // Gdx.app.log("IBMX","latency "+ audioDevice.getLatency()+" writing took "+t2/1000000 ); + + + } + // audioLine.drain(); + Gdx.app.log("IBXM", "thread done"); + } catch (Exception e) { + log(e.toString()); + } finally { + //if( audioLine != null && audioLine.isOpen() ) audioLine.close(); + if (audioDevice != null) audioDevice.dispose(); + } + } + }); + playThread.start(); + } + } + + public synchronized void stop() { + playing = false; + try { + if (playThread != null) playThread.join(); + } catch (InterruptedException e) { + } + + } + + private synchronized void seek(int pos) { + samplePos = ibxm.seek(pos); + } + + private synchronized void setInterpolation(int interpolation) { + this.interpolation = interpolation; + if (ibxm != null) ibxm.setInterpolation(interpolation); + } + + private synchronized void setReverb(int millis) { + reverbLen = ((SAMPLE_RATE * millis) >> 9) & -2; + reverbBuf = new int[reverbLen]; + reverbIdx = 0; + } + + private synchronized int getAudio(int[] mixBuf) { + int count = ibxm.getAudio(mixBuf); + samplePos += count; + return count; + } + +// private synchronized void saveWav( File wavFile, int time, int fade ) throws IOException { +// stop(); +// seek( 0 ); +// WavInputStream wavInputStream = new WavInputStream( ibxm, time, fade ); +// FileOutputStream fileOutputStream = null; +// try { +// fileOutputStream = new FileOutputStream( wavFile ); +// byte[] buf = new byte[ ibxm.getMixBufferLength() * 2 ]; +// int remain = wavInputStream.getBytesRemaining(); +// while( remain > 0 ) { +// int count = remain > buf.length ? buf.length : remain; +// count = wavInputStream.read( buf, 0, count ); +// fileOutputStream.write( buf, 0, count ); +// remain -= count; +// } +// } finally { +// if( fileOutputStream != null ) fileOutputStream.close(); +// seek( 0 ); +// } +// } +// +// private void reverb( int[] mixBuf, int count ) { +// /* Simple cross-delay with feedback. */ +// int mixIdx = 0, mixEnd = count << 1; +// while( mixIdx < mixEnd ) { +// mixBuf[ mixIdx ] = ( mixBuf[ mixIdx ] * 3 + reverbBuf[ reverbIdx + 1 ] ) >> 2; +// mixBuf[ mixIdx + 1 ] = ( mixBuf[ mixIdx + 1 ] * 3 + reverbBuf[ reverbIdx ] ) >> 2; +// reverbBuf[ reverbIdx ] = mixBuf[ mixIdx ]; +// reverbBuf[ reverbIdx + 1 ] = mixBuf[ mixIdx + 1 ]; +// reverbIdx += 2; +// if( reverbIdx >= reverbLen ) { +// reverbIdx = 0; +// } +// mixIdx += 2; +// } +// } + + +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Instrument.java b/src/uk/me/fantastic/retro/music/ibxm/Instrument.java new file mode 100755 index 0000000..71734bd --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Instrument.java @@ -0,0 +1,36 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Instrument { + public String name = ""; + public int numSamples = 1; + public int vibratoType = 0, vibratoSweep = 0, vibratoDepth = 0, vibratoRate = 0; + public int volumeFadeOut = 0; + public Envelope volumeEnvelope = new Envelope(); + public Envelope panningEnvelope = new Envelope(); + public int[] keyToSample = new int[97]; + public Sample[] samples = new Sample[]{new Sample()}; + + public void toStringBuffer(StringBuffer out) { + out.append("Name: " + name + '\n'); + if (numSamples > 0) { + out.append("Num Samples: " + numSamples + '\n'); + out.append("Vibrato Type: " + vibratoType + '\n'); + out.append("Vibrato Sweep: " + vibratoSweep + '\n'); + out.append("Vibrato Depth: " + vibratoDepth + '\n'); + out.append("Vibrato Rate: " + vibratoRate + '\n'); + out.append("Volume Fade Out: " + volumeFadeOut + '\n'); + out.append("Volume Envelope:\n"); + volumeEnvelope.toStringBuffer(out); + out.append("Panning Envelope:\n"); + panningEnvelope.toStringBuffer(out); + for (int samIdx = 0; samIdx < numSamples; samIdx++) { + out.append("Sample " + samIdx + ":\n"); + samples[samIdx].toStringBuffer(out); + } + out.append("Key To Sample: "); + for (int keyIdx = 1; keyIdx < 97; keyIdx++) + out.append(keyToSample[keyIdx] + ", "); + out.append('\n'); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Module.java b/src/uk/me/fantastic/retro/music/ibxm/Module.java new file mode 100755 index 0000000..204a6a0 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Module.java @@ -0,0 +1,425 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Module { + public String songName = "Blank"; + public int numChannels = 4, numInstruments = 1; + public int numPatterns = 1, sequenceLength = 1, restartPos = 0; + public int defaultGVol = 64, defaultSpeed = 6, defaultTempo = 125, c2Rate = Sample.C2_PAL, gain = 64; + public boolean linearPeriods = false, fastVolSlides = false; + public int[] defaultPanning = {51, 204, 204, 51}; + public int[] sequence = {0}; + public Pattern[] patterns = {new Pattern(4, 64)}; + public Instrument[] instruments = {new Instrument(), new Instrument()}; + + public Module() { + } + + public Module(java.io.InputStream inputStream) throws java.io.IOException { + this(new Data(inputStream)); + } + + public Module(Data moduleData) throws java.io.IOException { + if (moduleData.strLatin1(0, 17).equals("Extended Module: ")) { + loadXM(moduleData); + } else if (moduleData.strLatin1(44, 4).equals("SCRM")) { + loadS3M(moduleData); + } else { + loadMod(moduleData); + } + } + + public Module(byte[] moduleData) throws java.io.IOException { + this(new Data(moduleData)); + } + + private void loadMod(Data moduleData) throws java.io.IOException { + songName = moduleData.strLatin1(0, 20); + sequenceLength = moduleData.uByte(950) & 0x7F; + restartPos = moduleData.uByte(951) & 0x7F; + if (restartPos >= sequenceLength) restartPos = 0; + sequence = new int[128]; + for (int seqIdx = 0; seqIdx < 128; seqIdx++) { + int patIdx = moduleData.uByte(952 + seqIdx) & 0x7F; + sequence[seqIdx] = patIdx; + if (patIdx >= numPatterns) numPatterns = patIdx + 1; + } + switch (moduleData.ubeShort(1082)) { + case 0x4b2e: /* M.K. */ + case 0x4b21: /* M!K! */ + case 0x5434: /* FLT4 */ + numChannels = 4; + c2Rate = Sample.C2_PAL; + gain = 64; + break; + case 0x484e: /* xCHN */ + numChannels = moduleData.uByte(1080) - 48; + c2Rate = Sample.C2_NTSC; + gain = 32; + break; + case 0x4348: /* xxCH */ + numChannels = (moduleData.uByte(1080) - 48) * 10; + numChannels += moduleData.uByte(1081) - 48; + c2Rate = Sample.C2_NTSC; + gain = 32; + break; + default: + throw new IllegalArgumentException("MOD Format not recognised!"); + } + defaultGVol = 64; + defaultSpeed = 6; + defaultTempo = 125; + defaultPanning = new int[numChannels]; + for (int idx = 0; idx < numChannels; idx++) { + defaultPanning[idx] = 51; + if ((idx & 3) == 1 || (idx & 3) == 2) + defaultPanning[idx] = 204; + } + int moduleDataIdx = 1084; + patterns = new Pattern[numPatterns]; + for (int patIdx = 0; patIdx < numPatterns; patIdx++) { + Pattern pattern = patterns[patIdx] = new Pattern(numChannels, 64); + for (int patDataIdx = 0; patDataIdx < pattern.data.length; patDataIdx += 5) { + int period = (moduleData.uByte(moduleDataIdx) & 0xF) << 8; + period = (period | moduleData.uByte(moduleDataIdx + 1)) * 4; + if (period >= 112 && period <= 6848) { + int key = -12 * Channel.log2((period << Sample.FP_SHIFT) / 29021); + key = (key + (key & (Sample.FP_ONE >> 1))) >> Sample.FP_SHIFT; + pattern.data[patDataIdx] = (byte) key; + } + int ins = (moduleData.uByte(moduleDataIdx + 2) & 0xF0) >> 4; + ins = ins | moduleData.uByte(moduleDataIdx) & 0x10; + pattern.data[patDataIdx + 1] = (byte) ins; + int effect = moduleData.uByte(moduleDataIdx + 2) & 0x0F; + int param = moduleData.uByte(moduleDataIdx + 3); + if (param == 0 && (effect < 3 || effect == 0xA)) effect = 0; + if (param == 0 && (effect == 5 || effect == 6)) effect -= 2; + if (effect == 8 && numChannels == 4) effect = param = 0; + pattern.data[patDataIdx + 3] = (byte) effect; + pattern.data[patDataIdx + 4] = (byte) param; + moduleDataIdx += 4; + } + } + numInstruments = 31; + instruments = new Instrument[numInstruments + 1]; + instruments[0] = new Instrument(); + for (int instIdx = 1; instIdx <= numInstruments; instIdx++) { + Instrument instrument = instruments[instIdx] = new Instrument(); + Sample sample = instrument.samples[0]; + instrument.name = moduleData.strLatin1(instIdx * 30 - 10, 22); + int sampleLength = moduleData.ubeShort(instIdx * 30 + 12) * 2; + int fineTune = (moduleData.uByte(instIdx * 30 + 14) & 0xF) << 4; + sample.fineTune = (fineTune < 128) ? fineTune : fineTune - 256; + int volume = moduleData.uByte(instIdx * 30 + 15) & 0x7F; + sample.volume = (volume <= 64) ? volume : 64; + sample.panning = -1; + int loopStart = moduleData.ubeShort(instIdx * 30 + 16) * 2; + int loopLength = moduleData.ubeShort(instIdx * 30 + 18) * 2; + if (loopStart + loopLength > sampleLength) + loopLength = sampleLength - loopStart; + if (loopLength < 4) { + loopStart = sampleLength; + loopLength = 0; + } + sample.setSampleData(moduleData.samS8(moduleDataIdx, sampleLength), loopStart, loopLength, false); + moduleDataIdx += sampleLength; + } + } + + private void loadS3M(Data moduleData) throws java.io.IOException { + songName = moduleData.strCp850(0, 28); + sequenceLength = moduleData.uleShort(32); + numInstruments = moduleData.uleShort(34); + numPatterns = moduleData.uleShort(36); + int flags = moduleData.uleShort(38); + int version = moduleData.uleShort(40); + fastVolSlides = ((flags & 0x40) == 0x40) || version == 0x1300; + boolean signedSamples = moduleData.uleShort(42) == 1; + if (moduleData.uleInt(44) != 0x4d524353) + throw new IllegalArgumentException("Not an S3M file!"); + defaultGVol = moduleData.uByte(48); + defaultSpeed = moduleData.uByte(49); + defaultTempo = moduleData.uByte(50); + c2Rate = Sample.C2_NTSC; + gain = moduleData.uByte(51) & 0x7F; + boolean stereoMode = (moduleData.uByte(51) & 0x80) == 0x80; + boolean defaultPan = moduleData.uByte(53) == 0xFC; + int[] channelMap = new int[32]; + for (int chanIdx = 0; chanIdx < 32; chanIdx++) { + channelMap[chanIdx] = -1; + if (moduleData.uByte(64 + chanIdx) < 16) + channelMap[chanIdx] = numChannels++; + } + sequence = new int[sequenceLength]; + for (int seqIdx = 0; seqIdx < sequenceLength; seqIdx++) + sequence[seqIdx] = moduleData.uByte(96 + seqIdx); + int moduleDataIdx = 96 + sequenceLength; + instruments = new Instrument[numInstruments + 1]; + instruments[0] = new Instrument(); + for (int instIdx = 1; instIdx <= numInstruments; instIdx++) { + Instrument instrument = instruments[instIdx] = new Instrument(); + Sample sample = instrument.samples[0]; + int instOffset = moduleData.uleShort(moduleDataIdx) << 4; + moduleDataIdx += 2; + instrument.name = moduleData.strCp850(instOffset + 48, 28); + if (moduleData.uByte(instOffset) != 1) continue; + if (moduleData.uleShort(instOffset + 76) != 0x4353) continue; + int sampleOffset = moduleData.uByte(instOffset + 13) << 20; + sampleOffset += moduleData.uleShort(instOffset + 14) << 4; + int sampleLength = moduleData.uleInt(instOffset + 16); + int loopStart = moduleData.uleInt(instOffset + 20); + int loopLength = moduleData.uleInt(instOffset + 24) - loopStart; + sample.volume = moduleData.uByte(instOffset + 28); + sample.panning = -1; + boolean packed = moduleData.uByte(instOffset + 30) != 0; + boolean loopOn = (moduleData.uByte(instOffset + 31) & 0x1) == 0x1; + if (loopStart + loopLength > sampleLength) + loopLength = sampleLength - loopStart; + if (loopLength < 1 || !loopOn) { + loopStart = sampleLength; + loopLength = 0; + } + boolean stereo = (moduleData.uByte(instOffset + 31) & 0x2) == 0x2; + boolean sixteenBit = (moduleData.uByte(instOffset + 31) & 0x4) == 0x4; + if (packed) throw new IllegalArgumentException("Packed samples not supported!"); + int c2Rate = moduleData.uleInt(instOffset + 32); + int tune = (Channel.log2(c2Rate) - Channel.log2(this.c2Rate)) * 12; + sample.relNote = tune >> Sample.FP_SHIFT; + sample.fineTune = (tune & Sample.FP_MASK) >> (Sample.FP_SHIFT - 7); + if (sixteenBit) { + if (signedSamples) { + sample.setSampleData(moduleData.samS16(sampleOffset, sampleLength), loopStart, loopLength, false); + } else { + sample.setSampleData(moduleData.samU16(sampleOffset, sampleLength), loopStart, loopLength, false); + } + } else { + if (signedSamples) { + sample.setSampleData(moduleData.samS8(sampleOffset, sampleLength), loopStart, loopLength, false); + } else { + sample.setSampleData(moduleData.samU8(sampleOffset, sampleLength), loopStart, loopLength, false); + } + } + } + patterns = new Pattern[numPatterns]; + for (int patIdx = 0; patIdx < numPatterns; patIdx++) { + Pattern pattern = patterns[patIdx] = new Pattern(numChannels, 64); + int inOffset = (moduleData.uleShort(moduleDataIdx) << 4) + 2; + int rowIdx = 0; + while (rowIdx < 64) { + int token = moduleData.uByte(inOffset++); + if (token == 0) { + rowIdx++; + continue; + } + int noteKey = 0; + int noteIns = 0; + if ((token & 0x20) == 0x20) { /* Key + Instrument.*/ + noteKey = moduleData.uByte(inOffset++); + noteIns = moduleData.uByte(inOffset++); + if (noteKey < 0xFE) + noteKey = (noteKey >> 4) * 12 + (noteKey & 0xF) + 1; + if (noteKey == 0xFF) noteKey = 0; + } + int noteVol = 0; + if ((token & 0x40) == 0x40) { /* Volume Column.*/ + noteVol = (moduleData.uByte(inOffset++) & 0x7F) + 0x10; + if (noteVol > 0x50) noteVol = 0; + } + int noteEffect = 0; + int noteParam = 0; + if ((token & 0x80) == 0x80) { /* Effect + Param.*/ + noteEffect = moduleData.uByte(inOffset++); + noteParam = moduleData.uByte(inOffset++); + if (noteEffect < 1 || noteEffect >= 0x40) + noteEffect = noteParam = 0; + if (noteEffect > 0) noteEffect += 0x80; + } + int chanIdx = channelMap[token & 0x1F]; + if (chanIdx >= 0) { + int noteOffset = (rowIdx * numChannels + chanIdx) * 5; + pattern.data[noteOffset] = (byte) noteKey; + pattern.data[noteOffset + 1] = (byte) noteIns; + pattern.data[noteOffset + 2] = (byte) noteVol; + pattern.data[noteOffset + 3] = (byte) noteEffect; + pattern.data[noteOffset + 4] = (byte) noteParam; + } + } + moduleDataIdx += 2; + } + defaultPanning = new int[numChannels]; + for (int chanIdx = 0; chanIdx < 32; chanIdx++) { + if (channelMap[chanIdx] < 0) continue; + int panning = 7; + if (stereoMode) { + panning = 12; + if (moduleData.uByte(64 + chanIdx) < 8) panning = 3; + } + if (defaultPan) { + int panFlags = moduleData.uByte(moduleDataIdx + chanIdx); + if ((panFlags & 0x20) == 0x20) panning = panFlags & 0xF; + } + defaultPanning[channelMap[chanIdx]] = panning * 17; + } + } + + private void loadXM(Data moduleData) throws java.io.IOException { + if (moduleData.uleShort(58) != 0x0104) + throw new IllegalArgumentException("XM format version must be 0x0104!"); + songName = moduleData.strCp850(17, 20); + boolean deltaEnv = moduleData.strLatin1(38, 20).startsWith("DigiBooster Pro"); + int dataOffset = 60 + moduleData.uleInt(60); + sequenceLength = moduleData.uleShort(64); + restartPos = moduleData.uleShort(66); + numChannels = moduleData.uleShort(68); + numPatterns = moduleData.uleShort(70); + numInstruments = moduleData.uleShort(72); + linearPeriods = (moduleData.uleShort(74) & 0x1) > 0; + defaultGVol = 64; + defaultSpeed = moduleData.uleShort(76); + defaultTempo = moduleData.uleShort(78); + c2Rate = Sample.C2_NTSC; + gain = 64; + defaultPanning = new int[numChannels]; + for (int idx = 0; idx < numChannels; idx++) defaultPanning[idx] = 128; + sequence = new int[sequenceLength]; + for (int seqIdx = 0; seqIdx < sequenceLength; seqIdx++) { + int entry = moduleData.uByte(80 + seqIdx); + sequence[seqIdx] = entry < numPatterns ? entry : 0; + } + patterns = new Pattern[numPatterns]; + for (int patIdx = 0; patIdx < numPatterns; patIdx++) { + if (moduleData.uByte(dataOffset + 4) != 0) + throw new IllegalArgumentException("Unknown pattern packing type!"); + int numRows = moduleData.uleShort(dataOffset + 5); + int numNotes = numRows * numChannels; + Pattern pattern = patterns[patIdx] = new Pattern(numChannels, numRows); + int patternDataLength = moduleData.uleShort(dataOffset + 7); + dataOffset += moduleData.uleInt(dataOffset); + int nextOffset = dataOffset + patternDataLength; + if (patternDataLength > 0) { + int patternDataOffset = 0; + for (int note = 0; note < numNotes; note++) { + int flags = moduleData.uByte(dataOffset); + if ((flags & 0x80) == 0) flags = 0x1F; + else dataOffset++; + byte key = (flags & 0x01) > 0 ? moduleData.sByte(dataOffset++) : 0; + pattern.data[patternDataOffset++] = key; + byte ins = (flags & 0x02) > 0 ? moduleData.sByte(dataOffset++) : 0; + pattern.data[patternDataOffset++] = ins; + byte vol = (flags & 0x04) > 0 ? moduleData.sByte(dataOffset++) : 0; + pattern.data[patternDataOffset++] = vol; + byte fxc = (flags & 0x08) > 0 ? moduleData.sByte(dataOffset++) : 0; + byte fxp = (flags & 0x10) > 0 ? moduleData.sByte(dataOffset++) : 0; + if (fxc >= 0x40) fxc = fxp = 0; + pattern.data[patternDataOffset++] = fxc; + pattern.data[patternDataOffset++] = fxp; + } + } + dataOffset = nextOffset; + } + instruments = new Instrument[numInstruments + 1]; + instruments[0] = new Instrument(); + for (int insIdx = 1; insIdx <= numInstruments; insIdx++) { + Instrument instrument = instruments[insIdx] = new Instrument(); + instrument.name = moduleData.strCp850(dataOffset + 4, 22); + int numSamples = instrument.numSamples = moduleData.uleShort(dataOffset + 27); + if (numSamples > 0) { + instrument.samples = new Sample[numSamples]; + for (int keyIdx = 0; keyIdx < 96; keyIdx++) + instrument.keyToSample[keyIdx + 1] = moduleData.uByte(dataOffset + 33 + keyIdx); + Envelope volEnv = instrument.volumeEnvelope = new Envelope(); + volEnv.pointsTick = new int[12]; + volEnv.pointsAmpl = new int[12]; + int pointTick = 0; + for (int point = 0; point < 12; point++) { + int pointOffset = dataOffset + 129 + (point * 4); + pointTick = (deltaEnv ? pointTick : 0) + moduleData.uleShort(pointOffset); + volEnv.pointsTick[point] = pointTick; + volEnv.pointsAmpl[point] = moduleData.uleShort(pointOffset + 2); + } + Envelope panEnv = instrument.panningEnvelope = new Envelope(); + panEnv.pointsTick = new int[16]; + panEnv.pointsAmpl = new int[16]; + pointTick = 0; + for (int point = 0; point < 12; point++) { + int pointOffset = dataOffset + 177 + (point * 4); + pointTick = (deltaEnv ? pointTick : 0) + moduleData.uleShort(pointOffset); + panEnv.pointsTick[point] = pointTick; + panEnv.pointsAmpl[point] = moduleData.uleShort(pointOffset + 2); + } + volEnv.numPoints = moduleData.uByte(dataOffset + 225); + if (volEnv.numPoints > 12) volEnv.numPoints = 0; + panEnv.numPoints = moduleData.uByte(dataOffset + 226); + if (panEnv.numPoints > 12) panEnv.numPoints = 0; + volEnv.sustainTick = volEnv.pointsTick[moduleData.uByte(dataOffset + 227) & 0xF]; + volEnv.loopStartTick = volEnv.pointsTick[moduleData.uByte(dataOffset + 228) & 0xF]; + volEnv.loopEndTick = volEnv.pointsTick[moduleData.uByte(dataOffset + 229) & 0xF]; + panEnv.sustainTick = panEnv.pointsTick[moduleData.uByte(dataOffset + 230) & 0xF]; + panEnv.loopStartTick = panEnv.pointsTick[moduleData.uByte(dataOffset + 231) & 0xF]; + panEnv.loopEndTick = panEnv.pointsTick[moduleData.uByte(dataOffset + 232) & 0xF]; + volEnv.enabled = volEnv.numPoints > 0 && (moduleData.uByte(dataOffset + 233) & 0x1) > 0; + volEnv.sustain = (moduleData.uByte(dataOffset + 233) & 0x2) > 0; + volEnv.looped = (moduleData.uByte(dataOffset + 233) & 0x4) > 0; + panEnv.enabled = panEnv.numPoints > 0 && (moduleData.uByte(dataOffset + 234) & 0x1) > 0; + panEnv.sustain = (moduleData.uByte(dataOffset + 234) & 0x2) > 0; + panEnv.looped = (moduleData.uByte(dataOffset + 234) & 0x4) > 0; + instrument.vibratoType = moduleData.uByte(dataOffset + 235); + instrument.vibratoSweep = moduleData.uByte(dataOffset + 236); + instrument.vibratoDepth = moduleData.uByte(dataOffset + 237); + instrument.vibratoRate = moduleData.uByte(dataOffset + 238); + instrument.volumeFadeOut = moduleData.uleShort(dataOffset + 239); + } + dataOffset += moduleData.uleInt(dataOffset); + int sampleHeaderOffset = dataOffset; + dataOffset += numSamples * 40; + for (int samIdx = 0; samIdx < numSamples; samIdx++) { + Sample sample = instrument.samples[samIdx] = new Sample(); + int sampleDataBytes = moduleData.uleInt(sampleHeaderOffset); + int sampleLoopStart = moduleData.uleInt(sampleHeaderOffset + 4); + int sampleLoopLength = moduleData.uleInt(sampleHeaderOffset + 8); + sample.volume = moduleData.sByte(sampleHeaderOffset + 12); + sample.fineTune = moduleData.sByte(sampleHeaderOffset + 13); + boolean looped = (moduleData.uByte(sampleHeaderOffset + 14) & 0x3) > 0; + boolean pingPong = (moduleData.uByte(sampleHeaderOffset + 14) & 0x2) > 0; + boolean sixteenBit = (moduleData.uByte(sampleHeaderOffset + 14) & 0x10) > 0; + sample.panning = moduleData.uByte(sampleHeaderOffset + 15); + sample.relNote = moduleData.sByte(sampleHeaderOffset + 16); + sample.name = moduleData.strCp850(sampleHeaderOffset + 18, 22); + sampleHeaderOffset += 40; + if (!looped || (sampleLoopStart + sampleLoopLength) > sampleDataBytes) { + sampleLoopStart = sampleDataBytes; + sampleLoopLength = 0; + } + if (sixteenBit) { + sample.setSampleData(moduleData.samS16D(dataOffset, sampleDataBytes >> 1), sampleLoopStart >> 1, sampleLoopLength >> 1, pingPong); + } else { + sample.setSampleData(moduleData.samS8D(dataOffset, sampleDataBytes), sampleLoopStart, sampleLoopLength, pingPong); + } + dataOffset += sampleDataBytes; + } + } + } + + public void toStringBuffer(StringBuffer out) { + out.append("Song Name: " + songName + '\n' + + "Num Channels: " + numChannels + '\n' + + "Num Instruments: " + numInstruments + '\n' + + "Num Patterns: " + numPatterns + '\n' + + "Sequence Length: " + sequenceLength + '\n' + + "Restart Pos: " + restartPos + '\n' + + "Default Speed: " + defaultSpeed + '\n' + + "Default Tempo: " + defaultTempo + '\n' + + "Linear Periods: " + linearPeriods + '\n'); + out.append("Sequence: "); + for (int seqIdx = 0; seqIdx < sequence.length; seqIdx++) + out.append(sequence[seqIdx] + ", "); + out.append('\n'); + for (int patIdx = 0; patIdx < patterns.length; patIdx++) { + out.append("Pattern " + patIdx + ":\n"); + patterns[patIdx].toStringBuffer(out); + } + for (int insIdx = 1; insIdx < instruments.length; insIdx++) { + out.append("Instrument " + insIdx + ":\n"); + instruments[insIdx].toStringBuffer(out); + } + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Note.java b/src/uk/me/fantastic/retro/music/ibxm/Note.java new file mode 100755 index 0000000..d5ae769 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Note.java @@ -0,0 +1,30 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Note { + public int key, instrument, volume, effect, param; + + private static final String b36ToString = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String keyToString = "A-A#B-C-C#D-D#E-F-F#G-G#"; + + public String toString() { + return new String(toChars(new char[10])); + } + + public char[] toChars(char[] chars) { + keyToChars(key, chars); + chars[3] = (instrument > 0xF && instrument < 0xFF) ? b36ToString.charAt((instrument >> 4) & 0xF) : '-'; + chars[4] = (instrument > 0x0 && instrument < 0xFF) ? b36ToString.charAt(instrument & 0xF) : '-'; + chars[5] = (volume > 0xF && volume < 0xFF) ? b36ToString.charAt((volume >> 4) & 0xF) : '-'; + chars[6] = (volume > 0x0 && volume < 0xFF) ? b36ToString.charAt(volume & 0xF) : '-'; + chars[7] = ((effect > 0 || param > 0) && effect < 36) ? b36ToString.charAt(effect) : '-'; + chars[8] = (effect > 0 || param > 0) ? b36ToString.charAt((param >> 4) & 0xF) : '-'; + chars[9] = (effect > 0 || param > 0) ? b36ToString.charAt(param & 0xF) : '-'; + return chars; + } + + private static void keyToChars(int key, char[] out) { + out[0] = (key > 0 && key < 118) ? keyToString.charAt(((key + 2) % 12) * 2) : '-'; + out[1] = (key > 0 && key < 118) ? keyToString.charAt(((key + 2) % 12) * 2 + 1) : '-'; + out[2] = (key > 0 && key < 118) ? (char) ('0' + (key + 2) / 12) : '-'; + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Pattern.java b/src/uk/me/fantastic/retro/music/ibxm/Pattern.java new file mode 100755 index 0000000..95231fd --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Pattern.java @@ -0,0 +1,43 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Pattern { + public int numRows; + public byte[] data; + + public Pattern(int numChannels, int numRows) { + this.numRows = numRows; + data = new byte[numChannels * numRows * 5]; + } + + public Note getNote(int index, Note note) { + int offset = index * 5; + note.key = data[offset] & 0xFF; + note.instrument = data[offset + 1] & 0xFF; + note.volume = data[offset + 2] & 0xFF; + note.effect = data[offset + 3] & 0xFF; + note.param = data[offset + 4] & 0xFF; + return note; + } + + public void toStringBuffer(StringBuffer out) { + Note note = new Note(); + char[] chars = new char[10]; + int numChannels = data.length / (numRows * 5); + for (int row = 0; row < numRows; row++) { + for (int channel = 0; channel < numChannels; channel++) { + getNote(numChannels * row + channel, note); + note.toChars(chars); + out.append(chars); + out.append(' '); + } + out.append('\n'); + } + } + + public String toString() { + int numChannels = data.length / (numRows * 5); + StringBuffer stringBuffer = new StringBuffer(numRows * numChannels * 11 + numRows); + toStringBuffer(stringBuffer); + return stringBuffer.toString(); + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/Sample.java b/src/uk/me/fantastic/retro/music/ibxm/Sample.java new file mode 100755 index 0000000..d7bf00c --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/Sample.java @@ -0,0 +1,213 @@ +package uk.me.fantastic.retro.music.ibxm; + +public class Sample { + public static final int + FP_SHIFT = 15, + FP_ONE = 1 << FP_SHIFT, + FP_MASK = FP_ONE - 1; + + public static final int C2_PAL = 8287, C2_NTSC = 8363; + + public String name = ""; + public int volume = 0, panning = -1, relNote = 0, fineTune = 0; + private int loopStart = 0, loopLength = 0; + private short[] sampleData; + + /* Constants for the fixed-point sinc interpolator. */ + private static final int LOG2_FILTER_TAPS = 4; /* 16 taps. */ + private static final int FILTER_TAPS = 1 << LOG2_FILTER_TAPS; + private static final int DELAY = FILTER_TAPS / 2; + private static final int LOG2_TABLE_ACCURACY = 4; + private static final int TABLE_ACCURACY = 1 << LOG2_TABLE_ACCURACY; + private static final int TABLE_INTERP_SHIFT = FP_SHIFT - LOG2_TABLE_ACCURACY; + private static final int TABLE_INTERP_ONE = 1 << TABLE_INTERP_SHIFT; + private static final int TABLE_INTERP_MASK = TABLE_INTERP_ONE - 1; + private static final int LOG2_NUM_TABLES = LOG2_FILTER_TAPS - 1; + private static final int NUM_TABLES = 1 << LOG2_NUM_TABLES; + private static final short[][] SINC_TABLES = calculateSincTables(); + + private static short[][] calculateSincTables() { + short[][] sincTables = new short[NUM_TABLES][]; + for (int tableIdx = 0; tableIdx < NUM_TABLES; tableIdx++) { + sincTables[tableIdx] = calculateSincTable(1.0 / (tableIdx + 1)); + } + return sincTables; + } + + private static short[] calculateSincTable(double lowpass) { + short[] sincTable = new short[(TABLE_ACCURACY + 1) * FILTER_TAPS]; + double windDT = -2.0 * Math.PI / FILTER_TAPS; + double sincDT = -Math.PI; + int tableIdx = 0; + for (int tableY = 0; tableY <= TABLE_ACCURACY; tableY++) { + double fracT = tableY / (double) TABLE_ACCURACY; + double sincT = Math.PI * (FILTER_TAPS / 2 - 1 + fracT); + double windT = Math.PI + sincT * 2.0 / FILTER_TAPS; + for (int tableX = 0; tableX < FILTER_TAPS; tableX++) { + double sincY = lowpass; + if (sincT != 0) { + sincY = Math.sin(lowpass * sincT) / sincT; + } + /* Blackman-Harris window function.*/ + double windY = 0.35875; + windY -= 0.48829 * Math.cos(windT); + windY += 0.14128 * Math.cos(windT * 2); + windY -= 0.01168 * Math.cos(windT * 3); + sincTable[tableIdx++] = (short) Math.round(sincY * windY * 32767); + sincT += sincDT; + windT += windDT; + } + } + return sincTable; + } + + public void setSampleData(short[] sampleData, int loopStart, int loopLength, boolean pingPong) { + int sampleLength = sampleData.length; + // Fix loop if necessary. + if (loopStart < 0 || loopStart > sampleLength) + loopStart = sampleLength; + if (loopLength < 0 || (loopStart + loopLength) > sampleLength) + loopLength = sampleLength - loopStart; + sampleLength = loopStart + loopLength; + // Compensate for sinc-interpolator delay. + loopStart += DELAY; + // Allocate new sample. + int newSampleLength = DELAY + sampleLength + (pingPong ? loopLength : 0) + FILTER_TAPS; + short[] newSampleData = new short[newSampleLength]; + System.arraycopy(sampleData, 0, newSampleData, DELAY, sampleLength); + sampleData = newSampleData; + if (pingPong) { + // Calculate reversed loop. + int loopEnd = loopStart + loopLength; + for (int idx = 0; idx < loopLength; idx++) + sampleData[loopEnd + idx] = sampleData[loopEnd - idx - 1]; + loopLength *= 2; + } + // Extend loop for sinc interpolator. + for (int idx = loopStart + loopLength, end = idx + FILTER_TAPS; idx < end; idx++) + sampleData[idx] = sampleData[idx - loopLength]; + this.sampleData = sampleData; + this.loopStart = loopStart; + this.loopLength = loopLength; + } + + public void resampleNearest(int sampleIdx, int sampleFrac, int step, + int leftGain, int rightGain, int[] mixBuffer, int offset, int length) { + int loopLen = loopLength; + int loopEnd = loopStart + loopLen; + sampleIdx += DELAY; + if (sampleIdx >= loopEnd) + sampleIdx = normaliseSampleIdx(sampleIdx); + short[] data = sampleData; + int outIdx = offset << 1; + int outEnd = (offset + length) << 1; + while (outIdx < outEnd) { + if (sampleIdx >= loopEnd) { + if (loopLen < 2) break; + while (sampleIdx >= loopEnd) sampleIdx -= loopLen; + } + int y = data[sampleIdx]; + mixBuffer[outIdx++] += y * leftGain >> FP_SHIFT; + mixBuffer[outIdx++] += y * rightGain >> FP_SHIFT; + sampleFrac += step; + sampleIdx += sampleFrac >> FP_SHIFT; + sampleFrac &= FP_MASK; + } + } + + public void resampleLinear(int sampleIdx, int sampleFrac, int step, + int leftGain, int rightGain, int[] mixBuffer, int offset, int length) { + int loopLen = loopLength; + int loopEnd = loopStart + loopLen; + sampleIdx += DELAY; + if (sampleIdx >= loopEnd) + sampleIdx = normaliseSampleIdx(sampleIdx); + short[] data = sampleData; + int outIdx = offset << 1; + int outEnd = (offset + length) << 1; + while (outIdx < outEnd) { + if (sampleIdx >= loopEnd) { + if (loopLen < 2) break; + while (sampleIdx >= loopEnd) sampleIdx -= loopLen; + } + int c = data[sampleIdx]; + int m = data[sampleIdx + 1] - c; + int y = (m * sampleFrac >> FP_SHIFT) + c; + mixBuffer[outIdx++] += y * leftGain >> FP_SHIFT; + mixBuffer[outIdx++] += y * rightGain >> FP_SHIFT; + sampleFrac += step; + sampleIdx += sampleFrac >> FP_SHIFT; + sampleFrac &= FP_MASK; + } + } + + public void resampleSinc(int sampleIdx, int sampleFrac, int step, + int leftGain, int rightGain, int[] mixBuffer, int offset, int length) { + int tableIdx = 0; + if (step > FP_ONE) { + // Increase lowpass filter to avoid aliasing. + tableIdx = (step >> FP_SHIFT) - 1; + if (tableIdx >= NUM_TABLES) { + tableIdx = NUM_TABLES - 1; + } + } + short[] sincTable = SINC_TABLES[tableIdx]; + int loopLen = loopLength; + int loopEnd = loopStart + loopLen; + if (sampleIdx >= loopEnd) + sampleIdx = normaliseSampleIdx(sampleIdx); + short[] data = sampleData; + int outIdx = offset << 1; + int outEnd = (offset + length) << 1; + while (outIdx < outEnd) { + if (sampleIdx >= loopEnd) { + if (loopLen < 2) break; + while (sampleIdx >= loopEnd) sampleIdx -= loopLen; + } + int tableIdx1 = (sampleFrac >> TABLE_INTERP_SHIFT) << LOG2_FILTER_TAPS; + int tableIdx2 = tableIdx1 + FILTER_TAPS; + int a1 = 0, a2 = 0; + for (int tap = 0; tap < FILTER_TAPS; tap++) { + a1 += sincTable[tableIdx1 + tap] * data[sampleIdx + tap]; + a2 += sincTable[tableIdx2 + tap] * data[sampleIdx + tap]; + } + a1 >>= FP_SHIFT; + a2 >>= FP_SHIFT; + int y = a1 + ((a2 - a1) * (sampleFrac & TABLE_INTERP_MASK) >> TABLE_INTERP_SHIFT); + mixBuffer[outIdx++] += y * leftGain >> FP_SHIFT; + mixBuffer[outIdx++] += y * rightGain >> FP_SHIFT; + sampleFrac += step; + sampleIdx += sampleFrac >> FP_SHIFT; + sampleFrac &= FP_MASK; + } + } + + public int normaliseSampleIdx(int sampleIdx) { + int loopOffset = sampleIdx - loopStart; + if (loopOffset > 0) { + sampleIdx = loopStart; + if (loopLength > 1) sampleIdx += loopOffset % loopLength; + } + return sampleIdx; + } + + public boolean looped() { + return loopLength > 1; + } + + public void toStringBuffer(StringBuffer out) { + out.append("Name: " + name + '\n'); + out.append("Volume: " + volume + '\n'); + out.append("Panning: " + panning + '\n'); + out.append("Relative Note: " + relNote + '\n'); + out.append("Fine Tune: " + fineTune + '\n'); + out.append("Loop Start: " + loopStart + '\n'); + out.append("Loop Length: " + loopLength + '\n'); + /* + out.append( "Sample Data: " ); + for( int idx = 0; idx < sampleData.length; idx++ ) + out.append( sampleData[ idx ] + ", " ); + out.append( '\n' ); + */ + } +} diff --git a/src/uk/me/fantastic/retro/music/ibxm/WavInputStream.java b/src/uk/me/fantastic/retro/music/ibxm/WavInputStream.java new file mode 100755 index 0000000..f3bbcc5 --- /dev/null +++ b/src/uk/me/fantastic/retro/music/ibxm/WavInputStream.java @@ -0,0 +1,176 @@ +package uk.me.fantastic.retro.music.ibxm; + +import java.io.InputStream; + +/* + An InputStream that produces 16-bit WAV audio data from an IBXM instance. + J2ME: java.microedition.media.Manager.createPlayer( wavInputStream, "audio/x-wav" ).start(); +*/ +public class WavInputStream extends InputStream { + private static final byte[] header = { + 0x52, 0x49, 0x46, 0x46, /* "RIFF" */ + 0x00, 0x00, 0x00, 0x00, /* Audio data size + 36 */ + 0x57, 0x41, 0x56, 0x45, /* "WAVE" */ + 0x66, 0x6D, 0x74, 0x20, /* "fmt " */ + 0x10, 0x00, 0x00, 0x00, /* 16 (bytes to follow) */ + 0x01, 0x00, 0x02, 0x00, /* 1 (pcm), 2 (stereo) */ + 0x00, 0x00, 0x00, 0x00, /* Sample rate */ + 0x00, 0x00, 0x00, 0x00, /* Sample rate * 4 */ + 0x04, 0x00, 0x10, 0x00, /* 4 (bytes/frame), 16 (bits) */ + 0x64, 0x61, 0x74, 0x61, /* "data" */ + 0x00, 0x00, 0x00, 0x00, /* Audio data size */ + }; + + private IBXM ibxm; + private int[] mixBuf; + private byte[] outBuf; + private int outIdx, outLen, remain, fadeLen; + + public WavInputStream(IBXM ibxm) { + this(ibxm, ibxm.calculateSongDuration(), 0); + } + + /* + Duration is specified in samples at the sampling rate of the IBXM instance. + If fadeOutSeconds is greater than zero, a fade-out will be applied at the end of the stream. + */ + public WavInputStream(IBXM ibxm, int duration, int fadeOutSeconds) { + this.ibxm = ibxm; + mixBuf = new int[ibxm.getMixBufferLength()]; + outBuf = new byte[mixBuf.length * 2]; + int dataLen = duration * 4; + int samplingRate = ibxm.getSampleRate(); + System.arraycopy(header, 0, outBuf, 0, header.length); + writeInt32(outBuf, 4, dataLen + 36); + writeInt32(outBuf, 24, samplingRate); + writeInt32(outBuf, 28, samplingRate * 4); + writeInt32(outBuf, 40, dataLen); + outIdx = 0; + outLen = header.length; + remain = header.length + dataLen; + fadeLen = samplingRate * 4 * fadeOutSeconds; + } + + /* Get the number of bytes available before read() returns end-of-file. */ + public int getBytesRemaining() { + return remain; + } + + @Override + public int available() { + return outLen - outIdx; + } + + @Override + public int read() { + int out = -1; + if (remain > 0) { + out = outBuf[outIdx++]; + if (outIdx >= outLen) { + getAudio(); + } + remain--; + } + return out; + } + + @Override + public int read(byte[] buf, int off, int len) { + int count = -1; + if (remain > 0) { + count = remain; + if (count > len) { + count = len; + } + int outRem = outLen - outIdx; + if (count > outRem) { + count = outRem; + } + System.arraycopy(outBuf, outIdx, buf, off, count); + outIdx += count; + if (outIdx >= outLen) { + getAudio(); + } + remain -= count; + } + return count; + } + + private void getAudio() { + int mEnd = ibxm.getAudio(mixBuf) * 2; + int gain = 1024; + if (remain < fadeLen) { + gain = remain / (fadeLen >> 10); + gain = (gain * gain * gain) >> 20; + } + for (int mIdx = 0, oIdx = 0; mIdx < mEnd; mIdx++) { + int ampl = (mixBuf[mIdx] * gain) >> 10; + if (ampl > 32767) ampl = 32767; + if (ampl < -32768) ampl = -32768; + outBuf[oIdx++] = (byte) ampl; + outBuf[oIdx++] = (byte) (ampl >> 8); + } + outIdx = 0; + outLen = mEnd * 2; + } + + private static void writeInt32(byte[] buf, int idx, int value) { + buf[idx] = (byte) value; + buf[idx + 1] = (byte) (value >> 8); + buf[idx + 2] = (byte) (value >> 16); + buf[idx + 3] = (byte) (value >> 24); + } + + /* Simple Mod to Wav converter. */ + public static void main(String[] args) throws java.io.IOException { + java.io.File modFile = null, wavFile = null; + boolean fadeOut = false; + int argIdx = 0, interpolation = Channel.NEAREST; + while (argIdx < args.length) { + // Parse arguments. + String arg = args[argIdx++]; + if ("-linear".equals(arg)) { + interpolation = Channel.LINEAR; + } else if ("-sinc".equals(arg)) { + interpolation = Channel.SINC; + } else if ("-fade".equals(arg)) { + fadeOut = true; + } else if (modFile == null) { + modFile = new java.io.File(arg); + } else { + wavFile = new java.io.File(arg); + } + } + if (modFile == null || wavFile == null) { + System.err.println("Mod to Wav converter for IBXM " + IBXM.VERSION); + System.err.println("Usage: java " + WavInputStream.class.getName() + " [-linear] [-sinc] [-fade] modfile wavfile"); + } else { + if (wavFile.exists()) { + System.err.println("Output file already exists: " + wavFile.getName()); + } else { + // Write WAV file to output. + IBXM ibxm = new IBXM(new Module(new java.io.FileInputStream(modFile)), 48000); + ibxm.setInterpolation(interpolation); + int duration = ibxm.calculateSongDuration(); + if (fadeOut) { + // 16-second fade-out. + duration = duration + ibxm.getSampleRate() * 16; + } + WavInputStream in = new WavInputStream(ibxm, duration, fadeOut ? 16 : 0); + java.io.OutputStream out = new java.io.FileOutputStream(wavFile); + try { + byte[] buf = new byte[ibxm.getMixBufferLength() * 2]; + int remain = in.getBytesRemaining(); + while (remain > 0) { + int count = remain > buf.length ? buf.length : remain; + count = in.read(buf, 0, count); + out.write(buf, 0, count); + remain -= count; + } + } finally { + out.close(); + } + } + } + } +} diff --git a/src/uk/me/fantastic/retro/network/Client.kt b/src/uk/me/fantastic/retro/network/Client.kt new file mode 100644 index 0000000..adeabac --- /dev/null +++ b/src/uk/me/fantastic/retro/network/Client.kt @@ -0,0 +1,70 @@ +package uk.me.fantastic.retro.network + +import com.badlogic.gdx.graphics.Color +import com.esotericsoftware.kryonet.Client +import com.esotericsoftware.kryonet.Connection +import com.esotericsoftware.kryonet.Listener +import org.objenesis.strategy.StdInstantiatorStrategy +import uk.me.fantastic.retro.AbstractGameFactory +import uk.me.fantastic.retro.App.Companion.app +import uk.me.fantastic.retro.Player +import uk.me.fantastic.retro.Prefs +import uk.me.fantastic.retro.input.GamepadInput +import uk.me.fantastic.retro.log +import java.util.concurrent.ArrayBlockingQueue + +class Client : Listener() { + val BUFFER_SIZE = 1024 * 1024 * 10 + + val client = Client(BUFFER_SIZE, BUFFER_SIZE) + val queue = ArrayBlockingQueue(10) + + var gameToLoad: AbstractGameFactory? = null + + var player: Player? = null + + fun initialise() { + log("initialize networkclient") + + // Server.registerKryo(client.kryo) + } + + fun connect() { + val p = Player( + GamepadInput(app.mappedControllers.first()), + Prefs.StringPref.PLAYER1.getString(), + Color.valueOf(Prefs.MultiChoicePref.PLAYER1_COLOR.getString()), + Color.valueOf(Prefs.MultiChoicePref.PLAYER1_COLOR2.getString()) + ) + connect(p) + } + + fun connect(p: Player) { + player = p + client.kryo.isRegistrationRequired = false + client.kryo.instantiatorStrategy = StdInstantiatorStrategy() + + client.addListener(this) + + client.start() + log("networkclient started") + client.connect(5000, Prefs.StringPref.SERVER.getString(), 54555, 54777) + } + + override fun connected(connection: Connection) { + } + + override fun received(connection: Connection, obj: Any) { + when (obj) { + is AbstractGameFactory -> { + gameToLoad = obj + player?.let { + client.sendTCP(it) + } + } + else -> { + queue.offer(obj) + } + } + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/network/ClientGameSession.kt b/src/uk/me/fantastic/retro/network/ClientGameSession.kt new file mode 100644 index 0000000..f0eb78a --- /dev/null +++ b/src/uk/me/fantastic/retro/network/ClientGameSession.kt @@ -0,0 +1,54 @@ +package uk.me.fantastic.retro.network + +import uk.me.fantastic.retro.AbstractGameFactory +import uk.me.fantastic.retro.App.Companion.app +import uk.me.fantastic.retro.Player +import uk.me.fantastic.retro.input.NetworkInput +import uk.me.fantastic.retro.log +import uk.me.fantastic.retro.screens.GameSession + +class ClientGameSession(factory: AbstractGameFactory) : GameSession(factory) { + + override fun show() { + log("Gamesession $game show input $preSelectedInputDevice") + // val game = game + + if (game == null) { + throw Exception("Trying to show a GameSession but its Game has not been set yet") + } + game!!.show() + } + + override fun render(deltaTime: Float) { + val networkedGame = game + val client = app.client + if (networkedGame is isNetworked && client != null) { + + var obj: Any? = app.client?.queue?.poll() + + while (obj != null) { + when (obj) { + is SessionUpdate -> { + log("is SessionUpdate ${obj.players} ${obj.players.size}") + players = ArrayList() + players.addAll(obj.players) + } + else -> { + log("passing it on") + networkedGame.processIncomingMessage(obj) + } + } + obj = client.queue.poll() + } + + client.player?.let { + val n = NetworkInput() + n.copyFrom(it.input) + client.client.sendUDP(n) + } + } + super.render(deltaTime) + } + + class SessionUpdate(val players: Array) +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/network/ClientPlayer.kt b/src/uk/me/fantastic/retro/network/ClientPlayer.kt new file mode 100644 index 0000000..7cc874d --- /dev/null +++ b/src/uk/me/fantastic/retro/network/ClientPlayer.kt @@ -0,0 +1,17 @@ +package uk.me.fantastic.retro.network + +import com.badlogic.gdx.graphics.Color +import uk.me.fantastic.retro.Player +import uk.me.fantastic.retro.input.InputDevice + +/** + * specialized kind of Player used by networking TODO redo networking + */ +class ClientPlayer(input: InputDevice, name: String, color: Color, color2: Color, val localId: Int) : Player(input, + name, + color, color2) { + var remoteId: Int? = null + + // @Suppress("unused") + // constructor() : this(null, "", Color.WHITE, -1) +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/network/Server.kt b/src/uk/me/fantastic/retro/network/Server.kt new file mode 100644 index 0000000..97f4112 --- /dev/null +++ b/src/uk/me/fantastic/retro/network/Server.kt @@ -0,0 +1,97 @@ +package uk.me.fantastic.retro.network + +import com.esotericsoftware.kryonet.Connection +import com.esotericsoftware.kryonet.Listener +import com.esotericsoftware.kryonet.Server +import org.objenesis.strategy.StdInstantiatorStrategy +import uk.me.fantastic.retro.log +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.CopyOnWriteArrayList + +class Server : Listener() { + + val BUFFER_SIZE = 1024 * 1024 * 10 + val server = Server(BUFFER_SIZE, BUFFER_SIZE) + + val queue = ArrayBlockingQueue>(10) + + // private var connectionQueue = ArrayBlockingQueue(64) + // private var disConnectionQueue = ArrayBlockingQueue(64) + + private var clients = CopyOnWriteArrayList() + + fun initialise() { + // registerKryo(server.kryo) + server.kryo.isRegistrationRequired = false + server.kryo.instantiatorStrategy = StdInstantiatorStrategy() + + try { + server.bind(54555, 54777) + server.addListener(this) + + server.start() + log("server running") + } catch (e: java.net.BindException) { + log("couldnt start a server, port in use") + } + } + + override fun received(connection: Connection, obj: Any) { + queue.offer(Pair(connection, obj)) + } + + override fun connected(connection: Connection) { + clients.add(connection) + log("Server: Connected client $connection") + } + + override fun disconnected(connection: Connection) { + clients.remove(connection) + log("Server: Disconnected client $connection") + } + +// fun startGame(gf: AbstractGameFactory){ +// sendReliable(gf) +// } + + fun send(obj: Any) { + log("server send $obj") + server.sendToAllUDP(obj) +// clients.forEach { +// log(" to $it") +// it.sendUDP(obj) +// } + } + + fun sendReliable(obj: Any) { + log("sendreliable") + server.sendToAllTCP(obj) +// clients.forEach { +// log("sending $it $obj") +// it.sendTCP(obj) +// } + } + + companion object { +// internal fun registerKryo(kryo: Kryo) { +// kryo.register(JoinRequest::class.java) +// kryo.register(WorldUpdate::class.java) +// kryo.register(InputUpdate::class.java) +// kryo.register(CreatePlayerRequest::class.java) +// kryo.register(CreatePlayerResponse::class.java) +// kryo.register(NetworkInput::class.java) +// kryo.register(Vec::class.java) +// +// kryo.register(ByteArray::class.java) +// kryo.register(Text::class.java) +// kryo.register(Player::class.java) +// kryo.register(ClientPlayer::class.java) +// kryo.register(PlayersUpdate::class.java) +// kryo.register(ArrayList::class.java) +// kryo.register(com.badlogic.gdx.graphics.Color::class.java) +// kryo.register(Pair::class.java) +// +// kryo.isRegistrationRequired = true +// } + } +} diff --git a/src/uk/me/fantastic/retro/network/isNetworked.kt b/src/uk/me/fantastic/retro/network/isNetworked.kt new file mode 100644 index 0000000..fdf8920 --- /dev/null +++ b/src/uk/me/fantastic/retro/network/isNetworked.kt @@ -0,0 +1,5 @@ +package uk.me.fantastic.retro.network + +interface isNetworked { + fun processIncomingMessage(obj: Any) +} diff --git a/src/uk/me/fantastic/retro/screens/GameSession.kt b/src/uk/me/fantastic/retro/screens/GameSession.kt new file mode 100644 index 0000000..e38b20e --- /dev/null +++ b/src/uk/me/fantastic/retro/screens/GameSession.kt @@ -0,0 +1,449 @@ +package uk.me.fantastic.retro.screens + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Gdx.input +import com.badlogic.gdx.Input +import com.badlogic.gdx.ScreenAdapter +import com.badlogic.gdx.controllers.Controller +import com.badlogic.gdx.controllers.ControllerAdapter +import com.badlogic.gdx.graphics.Color +import com.esotericsoftware.kryonet.Connection +import uk.me.fantastic.retro.AbstractGameFactory +import uk.me.fantastic.retro.App.Companion.app +import uk.me.fantastic.retro.Game +import uk.me.fantastic.retro.Player +import uk.me.fantastic.retro.Prefs +import uk.me.fantastic.retro.input.GamepadInput +import uk.me.fantastic.retro.input.InputDevice +import uk.me.fantastic.retro.input.KeyboardMouseInput +import uk.me.fantastic.retro.input.MappedController +import uk.me.fantastic.retro.input.NetworkInput +import uk.me.fantastic.retro.log +import uk.me.fantastic.retro.menu.ActionMenuItem +import uk.me.fantastic.retro.menu.BackMenuItem +import uk.me.fantastic.retro.menu.BinPrefMenuItem +import uk.me.fantastic.retro.menu.Menu +import uk.me.fantastic.retro.menu.MultiPrefMenuItem +import uk.me.fantastic.retro.menu.SubMenuItem +import uk.me.fantastic.retro.network.ClientGameSession +import uk.me.fantastic.retro.network.ClientPlayer +import java.util.ArrayList + +/** + * Created by richard on 23/06/2016. + * A GDX screen (i.e. a render loop, used whenever a game is in progress) + * intended to provide continuity between multiple games. + * If you're only playing a single game this could be part of the game class, + * but for a series of games you pass a single GameSession from game to game + * to maintain the same players, scores, (network connections?) + */ +open class GameSession( + val factory: AbstractGameFactory + + // val preSelectedInputDevice: InputDevice? = null +) : ScreenAdapter() { + + var game: Game? = null + var preSelectedInputDevice: InputDevice? = null + + var level = 0 + + var nextGame: Game? = null // allows chaining several games together + var metaGame: Game? = null // allows one game to launch minigames that return control to the parent game when done + + var readyTimer = if (Prefs.BinPref.DEBUG.isEnabled()) 1f else 40f + + enum class GameState { + PLAY, GAMEOVER, MENU, GETREADY + } + + var state = GameState.GETREADY // fixme only used by UniGame, does it need to be in session? + + var players = ArrayList() + val clientPlayers = ArrayList() // when we a network client this is the players on the local machine + val connections: ArrayList = ArrayList() + + var buffering = true + + var KBinUse = false + + val server: String = Prefs.StringPref.SERVER.getString() + + enum class NetworkRole { NONE, CLIENT, SERVER } + + internal val usedControllers = ArrayList() + + private val remotePlayers = HashMap() + private val networkInputs = HashMap() + + private var namesUsed = 0 // FIXME use this + + override fun dispose() { + super.dispose() + game?.dispose() + } + + init { + + // if(client) setNetworkRoleToClient() + +// if (minigame == null) { +// // game = ButtonMasherGame3(this) +// game = UniGame(CharacterFactory() , session=this, networkRole = networkRole) +// } else { +// game = minigame +// } + + // val clazz = UniGame::class + // game = clazz.primaryConstructor?.call() + game = factory.create(this) + app.submitAnalytics("sessionStart:${factory.name}") + } + + fun createClient(connection: Connection) { + connections.add(connection) + log("created connection " + connection) + postMessage("${connection.remoteAddressTCP} connected") + } + + fun removeClient(client: Connection) { + + log("players: ${players.size}") + + players.removeIf { + if (it.input is NetworkInput) { + log("removeclient 1 $it ${it.input.clientId} ${client.id}") + if (it.input.clientId == client.id) { + log("removing: $it") + return@removeIf true + } + } + return@removeIf false + } + log("players: ${players.size}") + + connections.remove(client) + postMessage("${client.id} disconnected") + } + + var keyboardPlayer: Player? = null + + fun createKBPlayer() { + if (KBinUse && !Prefs.BinPref.DEBUG.isEnabled()) return + KBinUse = true + + val i = KeyboardMouseInput(this) + keyboardPlayer = createPlayer(i) + + // val ship = createCharacter(i, player) + } + + private fun createControllerPlayer(controller: MappedController) { + val gamepad = GamepadInput(controller) + createPlayer(gamepad) + } + +// var touchscreenInput: TouchscreenInput? = null +// +// fun createTouchscreenPlayer() { +// if (touchscreenInput == null) { +// val t = TouchscreenInput() +// touchscreenInput = t +// createPlayer(t) +// } +// } + + @SuppressWarnings + fun standardMenu(): Menu { + val inGameMenu: Menu = Menu("MENU") + val inGameOptions = Menu("OPTIONS") + + // inGameOptions.add(BinPrefMenuItem("Display mode: ", Prefs.BinPref.FULLSCREEN)) + inGameOptions.add(MultiPrefMenuItem("Shader: ", Prefs.MultiChoicePref.SHADER)) + inGameOptions.add(BinPrefMenuItem("Vsync: ", Prefs.BinPref.VSYNC)) + inGameOptions.add(MultiPrefMenuItem("FPS limit: ", Prefs.MultiChoicePref.LIMIT_FPS)) + inGameOptions.add(BinPrefMenuItem("Pixels: ", Prefs.BinPref.SMOOTH)) + inGameOptions.add(BinPrefMenuItem("Blur: ", Prefs.BinPref.BILINEAR)) + inGameOptions.add(BinPrefMenuItem("Scaling: ", Prefs.BinPref.STRETCH)) + inGameOptions.add(BinPrefMenuItem("Scanlines: ", Prefs.BinPref.SCANLINES)) + inGameOptions.add(BinPrefMenuItem("Show FPS: ", Prefs.BinPref.FPS)) + + inGameOptions.add(BackMenuItem("<<<<")) + + inGameMenu.add(SubMenuItem("Video Options", subMenu = inGameOptions)) + + val quitMenu = Menu("QUIT?") + + quitMenu.add(BackMenuItem("No")) + + quitMenu.add(ActionMenuItem("YES", action = { + this.nextGame = null + this.metaGame = null + this.quit() + })) + + inGameMenu.add(SubMenuItem("Quit", subMenu = quitMenu)) + + inGameMenu.add(ActionMenuItem("Continue", action = { + this.state = GameState.PLAY + })) + + return inGameMenu + } + + private fun createPlayer(input: InputDevice): Player { + + return createLocalPlayerOnServer(input) + } + + fun createNetworkPlayerOnServer(name: String): Int { + + val id = players.size + + log("create network player $id") + + val player = Player(input = NetworkInput(), name = name, color = Color.valueOf( + (nextPlayerColor())), color2 = Color.valueOf(nextPlayerColor2())) + + players.add(player) + + postMessage("${player.name} JOINED!") + + return id + } + + private fun createLocalPlayerOnServer(input: InputDevice): Player { + + val id = players.size + + log("create player $id") + + val namePref = nextPlayerName() + + val player = Player(input = input, name = namePref.getString(), color = Color.valueOf( + (nextPlayerColor())), color2 = Color.valueOf(nextPlayerColor2())) + + players.add(player) + + postMessage("${player.name} JOINED!") + + return player + } + + private fun createPlayerOnClient(input: InputDevice): Int { + val id = clientPlayers.size + + log("create local player $id") + + val namePref = when (id) { + 0 -> Prefs.StringPref.PLAYER1 + 1 -> Prefs.StringPref.PLAYER2 + 2 -> Prefs.StringPref.PLAYER3 + 3 -> Prefs.StringPref.PLAYER4 + else -> Prefs.StringPref.PLAYER_GUEST + } + val player = ClientPlayer(input = input, name = namePref.getString(), localId = id, color = Color.valueOf( + (nextPlayerColor())), color2 = Color.valueOf(nextPlayerColor2())) + clientPlayers.add(player) + + // postMessage("${player.name} JOINED!") + return id + } + + override fun show() { + log("Gamesession $game show input $preSelectedInputDevice") + // val game = game + + if (game == null) { + throw Exception("Trying to show a GameSession but its Game has not been set yet") + } + game!!.show() + app.server?.sendReliable(factory) + + for (c in app.mappedControllers) { + log("attaching listener to $c") + attachListenerToController(c) + } + preSelectedInputDevice?.let { + createPlayer(it) + } + +// if (Gdx.app.type == Application.ApplicationType.Android) { +// createTouchscreenPlayer() +// } + } + + private fun attachListenerToController(c: MappedController) { + preSelectedInputDevice?.let { + if (it is GamepadInput && it.controller == c) { + return // dont use controller if its already been used as the preselectedinputdevice + } + } + + c.listener = object : ControllerAdapter() { + override fun buttonDown(controller: Controller?, buttonIndex: Int): Boolean { + + log("controller $controller $buttonIndex") + + if (!usedControllers.contains(c)) { + createControllerPlayer(c) + usedControllers.add(c) + + // unusedControllers.removeValue(controller, true) + } + return true + } + } + + c.controller.addListener(c.listener) + } + + override fun resize(width: Int, height: Int) { + game?.resize(width, height) + } + + override fun render(deltaTime: Float) { + game?.renderAndClampFramerate() + + if (state == GameState.GETREADY) { + readyTimer -= deltaTime * 10 + if (readyTimer < 0f) { + state = GameState.PLAY + } + } + + if (state == GameState.PLAY || state == GameState.GETREADY) { + if (input.isKeyJustPressed(Input.Keys.BACK)) { + app.showTitleScreen() + } + if (input.isKeyJustPressed(Input.Keys.ESCAPE) || + app.statefulControllers.any { it.isStartButtonJustPressed } + ) { + state = GameState.MENU + app.clearEvents() + } + if (input.isKeyJustPressed(Input.Keys.SPACE)) { + createKBPlayer() + } + } + + val server = app.server + + var pair = server?.queue?.poll() + + while (pair != null) { + var (connection, obj) = pair + when (obj) { + is Player -> { + val input = NetworkInput(clientId = connection.id) + val p = Player(input, obj.name, obj.color, obj.color2) + players.add(p) + remotePlayers.put(connection.id, p) + networkInputs.put(connection.id, input) + } + is InputDevice -> { + val i = networkInputs[connection.id] + if (i != null) { + i.copyFrom(obj) + } + } + else -> { + log("unknown message received") + } + } + pair = server?.queue?.poll() + } + + val connections = app.server?.server?.connections?.size + + if (connections != null && connections > 0 && players.size > 0) { + log("sending sessionupdate") + val p: Array = Array(players.size, { players[it] }) + val update = ClientGameSession.SessionUpdate(p) + app.server?.send(update) + } + } + + override fun hide() { + game?.hide() + + Gdx.input.isCursorCatched = false + + app.mappedControllers + .filter { it.listener != null } + .forEach { it.controller.removeListener(it.listener) } + } + + // FIXME pre-create array of 16 players and pop the top one when player needed + + fun nextPlayerName(): Prefs.StringPref { + return when (players.size) { + 0 -> Prefs.StringPref.PLAYER1 + 1 -> Prefs.StringPref.PLAYER2 + 2 -> Prefs.StringPref.PLAYER3 + 3 -> Prefs.StringPref.PLAYER4 + else -> Prefs.StringPref.PLAYER_GUEST + } + } + + fun nextPlayerColor(): String { + return when (players.size) { + 0 -> Prefs.MultiChoicePref.PLAYER1_COLOR.getString() + 1 -> Prefs.MultiChoicePref.PLAYER2_COLOR.getString() + 2 -> Prefs.MultiChoicePref.PLAYER3_COLOR.getString() + 3 -> Prefs.MultiChoicePref.PLAYER4_COLOR.getString() + else -> Prefs.MultiChoicePref.PLAYERGUEST_COLOR.getString() + } + } + + fun nextPlayerColor2(): String { + return when (players.size) { + 0 -> Prefs.MultiChoicePref.PLAYER1_COLOR2.getString() + 1 -> Prefs.MultiChoicePref.PLAYER2_COLOR2.getString() + 2 -> Prefs.MultiChoicePref.PLAYER3_COLOR2.getString() + 3 -> Prefs.MultiChoicePref.PLAYER4_COLOR2.getString() + else -> Prefs.MultiChoicePref.PLAYERGUEST_COLOR2.getString() + } + } + + fun postMessage(s: String) { + game?.postMessage(s) + } + + fun quit() { + log("gamesession quit") + nextGame?.let { + advanceToNextGame(it) + } ?: metaGame?.let { + advanceToNextGame(it) + } ?: app.showTitleScreen() + } + + fun advanceToNextGame(gameToShow: Game) { + game?.hide() + game?.dispose() + game = gameToShow + game?.show() + } + + // search for Player (or multiple Players) with highest score + fun findWinners(): List { + var highScore = Integer.MIN_VALUE + val winners = ArrayList() + for (player in players) { + if (player.score == highScore) { + winners.add(player) + } else if (player.score > highScore) { + highScore = player.score + winners.clear() + winners.add(player) + } + } + return winners + + } + +// private fun setNetworkRoleToClient() { +// game.setNetworkRoleToClient() +// } +} diff --git a/src/uk/me/fantastic/retro/screens/SimpleTitleScreen.kt b/src/uk/me/fantastic/retro/screens/SimpleTitleScreen.kt new file mode 100644 index 0000000..37de7fa --- /dev/null +++ b/src/uk/me/fantastic/retro/screens/SimpleTitleScreen.kt @@ -0,0 +1,190 @@ +package uk.me.fantastic.retro.screens + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.ScreenAdapter +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.Batch +import com.badlogic.gdx.graphics.g2d.BitmapFont +import com.badlogic.gdx.graphics.g2d.GlyphLayout +import com.badlogic.gdx.math.Vector3 +import com.badlogic.gdx.utils.Align +import uk.me.fantastic.retro.AbstractGameFactory +import uk.me.fantastic.retro.App +import uk.me.fantastic.retro.App.Companion.app +import uk.me.fantastic.retro.FBORenderer +import uk.me.fantastic.retro.Prefs +import uk.me.fantastic.retro.Prefs.BinPref +import uk.me.fantastic.retro.log +import uk.me.fantastic.retro.menu.ActionMenuItem +import uk.me.fantastic.retro.menu.BackMenuItem +import uk.me.fantastic.retro.menu.BinPrefMenuItem +import uk.me.fantastic.retro.menu.Menu +import uk.me.fantastic.retro.menu.MenuController +import uk.me.fantastic.retro.menu.MultiPrefMenuItem +import uk.me.fantastic.retro.menu.NumPrefMenuItem +import uk.me.fantastic.retro.menu.SubMenuItem + +/** + * Created by richard on 23/06/2016. + * GDX screen, i.e. a render loop, used to render the titlescreen + */ +open class SimpleTitleScreen( + val WIDTH: Float = 160f, + val HEIGHT: Float = 120f, + val FONT: BitmapFont = BitmapFont(Gdx + .files.internal("small.fnt")), + val title: String = "My Game", + val factory: AbstractGameFactory, + val quitText: String = "quit", + val quitURL: String? = null +) : ScreenAdapter() { + + val FONT_ENGLISH = FONT + + val renderer = FBORenderer(WIDTH, HEIGHT, false) + + val quitMenu = Menu("quit?") + + val videoOptions = Menu("") + + val titleMenu: Menu = Menu("", quitAction = { + if (quitURL != null) { + Gdx.net.openURI(quitURL) + } + app.quit() + }) + + val optionsMenu: Menu = Menu("") + + val soundOptions: Menu = Menu("") + + internal var glyphLayout = GlyphLayout() + + val controller = MenuController(titleMenu, WIDTH, HEIGHT, y = 100f) + + var timer: Float = 0f + var count = 0 + + val sequence = arrayOf("RED", "PURPLE", "BLUE", "CYAN", "GREEN", "YELLOW") + + var flash = "" + + val header = "\n" + + title + + "\n" + + val footer = "V" + app.versionString + + /* not sure if this is OK or whether it should all be re-initialized in show() */ + init { + + FONT.data.markupEnabled = true + + FONT.region.texture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest) + + videoOptions.addAll(listOf( + MultiPrefMenuItem("graphics ", Prefs.MultiChoicePref.GRAPHICS), + BinPrefMenuItem("scaling ", BinPref.STRETCH), + BinPrefMenuItem("scanlines ", BinPref.SCANLINES), + BackMenuItem("BACK") + )) + + soundOptions.addAll(listOf( + BinPrefMenuItem("inGameMusic ", BinPref.MUSIC), + NumPrefMenuItem("musicVolume ", numPref = Prefs.NumPref.MUSIC_VOLUME), + NumPrefMenuItem("fxVolume ", numPref = Prefs.NumPref.FX_VOLUME), + BackMenuItem("BACK") + )) + + titleMenu.addAll(listOf( + // SubMenuItem("Start game", optionsMenu), + ActionMenuItem("start Game", { + + App.app.screen = GameSession(factory) + }), + + SubMenuItem("options", subMenu = optionsMenu) + )) + + // items.add(MenuItem("Connect to server")) + optionsMenu.add(SubMenuItem("video", subMenu = videoOptions)) + + optionsMenu.add(SubMenuItem("sound", subMenu = soundOptions)) + + optionsMenu.add(BackMenuItem("back")) + + titleMenu.add(BackMenuItem(quitText)) + + quitMenu.add(BackMenuItem("no")) + + quitMenu.add(ActionMenuItem("yes", action = { + if (quitURL != null) { + Gdx.net.openURI(quitURL) + } + app.quit() + })) + } + + override fun show() { + log("show titlescreen") + timer = 0f + app.clearEvents() + } + + override fun render(delta: Float) { + timer += delta + + val mouse = renderer.cam.unproject(Vector3(Gdx.input.x.toFloat(), Gdx.input.y.toFloat(), 0f)) + + controller.doMouseInput(mouse.x, mouse.y) + if (timer > 0.2f) { + controller.doInput() + } else { + app.clearEvents() + } + renderToFBO(renderer.beginFBO()) + + renderer.renderFBOtoScreen() + } + + private fun renderToFBO(batch: Batch) { + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) // clear the screen + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + batch.begin() + + flash = sequence[count++ % sequence.size] + + // fixme encapsulate menu inside controler + val text = header + controller.menus.peek().getText(flash) + + glyphLayout.setText(FONT, text, Color.WHITE, WIDTH, Align.center, true) + + // Prefs.BinPref.BILINEAR.filter(FONT.region) + + FONT.draw(batch, glyphLayout, 0f, HEIGHT) + FONT_ENGLISH.draw(batch, footer, 0f, 255f) + + batch.end() + } + + override fun resize(width: Int, height: Int) { + log("TitleScreen resize $width $height") + renderer.resize(width, height) + } + + override fun pause() { + } + + override fun resume() { + } + + override fun hide() { + // App.stopMusic() + } + + override fun dispose() { + } +} diff --git a/src/uk/me/fantastic/retro/utils/AndroidLogger.kt b/src/uk/me/fantastic/retro/utils/AndroidLogger.kt new file mode 100644 index 0000000..8fb0bd9 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/AndroidLogger.kt @@ -0,0 +1,31 @@ +package uk.me.fantastic.retro.utils + +import com.badlogic.gdx.Gdx +import io.sentry.Sentry +import io.sentry.event.BreadcrumbBuilder +import uk.me.fantastic.retro.Logger + +internal class AndroidLogger(val version: String, val dsn: String) : Logger { + override fun error(message: String) { + println(message) + Sentry.capture(message) + } + + override fun log(message: String) { + println("$message") + Sentry.getContext().recordBreadcrumb( + BreadcrumbBuilder().setMessage(message).build() + ) + } + + override fun log(caller: String, message: String) { + log(message) + } + + override fun initialize() { + System.setProperty("sentry.dist", Gdx.app.type.name) + System.setProperty("sentry.release", version) + println("sentry.release $version sentry.dist ${Gdx.app.type.name}") + Sentry.init(dsn) + } +} diff --git a/src/uk/me/fantastic/retro/utils/AnimatedTexture.kt b/src/uk/me/fantastic/retro/utils/AnimatedTexture.kt new file mode 100644 index 0000000..2a59b21 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/AnimatedTexture.kt @@ -0,0 +1,40 @@ +package uk.me.fantastic.retro.utils + +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.Animation +import com.badlogic.gdx.graphics.g2d.TextureRegion + +class AnimatedTexture( + val delay: Float, + vararg tex: TextureRegion, + val mode: PlayMode = PlayMode.LOOP +) : + Animation(delay, *tex) { + constructor(delay: Float, t: Array>, default: IntRange, mode: PlayMode = PlayMode.LOOP) : + this(delay, *(default.map { t[0][it] }.toTypedArray()), mode = mode) + + constructor (delay: Float, file: String, width: Int, height: Int, default: IntRange, mode: PlayMode = PlayMode.LOOP) : + this(delay, TextureRegion.split(Texture(file), width, height), default, mode) + + constructor(delay: Float, sheet: SpriteSheet, name: String, mode: PlayMode = PlayMode.LOOP) : + // sheet.getTextureRegion(name, frames) + this(delay, *(sheet.getFrames(name).toTypedArray()), mode = mode) + + init { + if (keyFrames.size < 1) throw ExceptionInInitializerError("cant have an Animation with no frames") + tex.forEach { it.texture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest) } + playMode = mode + } + + companion object { + // fixme i feel there should be more elegant solution + fun AnimatedTextureOrNull(delay: Float, vararg tex: TextureRegion): AnimatedTexture? { + var a: AnimatedTexture? = null + try { + a = AnimatedTexture(delay, *tex) + } catch (e: ExceptionInInitializerError) { + } + return a + } + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/utils/ComboOutputStream.kt b/src/uk/me/fantastic/retro/utils/ComboOutputStream.kt new file mode 100644 index 0000000..763307e --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/ComboOutputStream.kt @@ -0,0 +1,25 @@ +package uk.me.fantastic.retro.utils + +import java.io.OutputStream + +/** + * combines two outputsteams into one outputstream + */ +class ComboOutputStream(val stream1: OutputStream, val stream2: OutputStream) : OutputStream() { + + override fun write(b: Int) { + + stream2.write(b) + stream1.write(b) + } + + override fun close() { + stream1.close() + stream2.close() + } + + override fun flush() { + stream1.flush() + stream2.flush() + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/utils/DesktopCallback.kt b/src/uk/me/fantastic/retro/utils/DesktopCallback.kt new file mode 100644 index 0000000..71cd4a3 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/DesktopCallback.kt @@ -0,0 +1,44 @@ +package uk.me.fantastic.retro.utils + +import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration +import uk.me.fantastic.retro.Callback + +/** + * used to change FPS while app is running without restarting + * @suppress + */ +class DesktopCallback : Callback { + + val config = LwjglApplicationConfiguration() + + init { + config.vSyncEnabled = true + config.audioDeviceBufferSize = 1024 + config.audioDeviceBufferCount = 32 + config.audioDeviceSimultaneousSources = 32 + config.useGL30 = true + config.gles30ContextMajorVersion = 3 + config.gles30ContextMinorVersion = 3 + + // // config.useVsync(true) + // config.setIdleFPS(30) + // config. + // config.foregroundFPS = 30 + // config.backgroundFPS = 30 + // config.width = 1920 +// config.height = 1200 +// config.fullscreen = true + + // config.fullscreen= true +// config.width = 1800 +// config.height = 600 + } + + override fun setForegroundFPS(foregroundFPS: Int) { + config.foregroundFPS = foregroundFPS + } + + override fun setBackgroundFPS(backgroundFPS: Int) { + config.backgroundFPS = backgroundFPS + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/utils/RetroShader.kt b/src/uk/me/fantastic/retro/utils/RetroShader.kt new file mode 100644 index 0000000..ea65783 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/RetroShader.kt @@ -0,0 +1,56 @@ +package uk.me.fantastic.retro.utils + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.glutils.ShaderProgram +import com.badlogic.gdx.math.Vector2 +import uk.me.fantastic.retro.log + +/** Shader format of RetroArch project */ +class RetroShader(filename: String) { + + var shader: ShaderProgram? = null + + init { + // important since we aren't using some uniforms and attributes that SpriteBatch expects + ShaderProgram.pedantic = false + ShaderProgram.prependVertexCode = "#version 330\n#define VERTEX\n#define MVPMatrix u_projTrans\n" + + "#define VertexCoord a_position\n" + + "#define TexCoord a_texCoord0\n" + + "#define COLOR a_color\n" + + "#define Texture u_texture\n" + ShaderProgram.prependFragmentCode = "#version 330\n#define FRAGMENT\n#define MVPMatrix u_projTrans\n" + + "#define VertexCoord a_position\n" + + "#define TexCoord a_texCoord0\n" + + "#define COLOR a_color\n" + + "#define Texture u_texture\n" + + val file = Gdx.files.internal(filename) + if (file.exists()) { + attemptToLoadShader(file) + } + } + + private fun attemptToLoadShader(file: FileHandle?) { + val potentialShader = ShaderProgram(file, file) + if (potentialShader.log.isNotEmpty()) { + log(potentialShader.log) + } + if (potentialShader.isCompiled) { + this.shader = potentialShader + } else { + this.shader = null + uk.me.fantastic.retro.error("failed to compile shader") + } + } + + fun process(fboBatch: SpriteBatch, textureSize: Vector2, inputSize: Vector2, outputSize: Vector2) { + fboBatch.shader = shader + shader?.let { + it.setUniformf("TextureSize", textureSize) + it.setUniformf("InputSize", inputSize) + it.setUniformf("OutputSize", outputSize) + } + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/utils/SentryLogger.kt b/src/uk/me/fantastic/retro/utils/SentryLogger.kt new file mode 100644 index 0000000..23f9571 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/SentryLogger.kt @@ -0,0 +1,58 @@ +package uk.me.fantastic.retro.utils + +import com.badlogic.gdx.Gdx +import io.sentry.Sentry +import io.sentry.event.BreadcrumbBuilder +import uk.me.fantastic.retro.App +import uk.me.fantastic.retro.Logger +import uk.me.fantastic.retro.Prefs +import java.io.File +import java.io.PrintStream +import java.time.LocalDateTime + +class SentryLogger : Logger { + override fun error(message: String) { + println(message) + Sentry.capture(message) + } + + init { + val logFile = File(App.LOG_FILE_PATH) + val logStream = logFile.outputStream().buffered() + val outStream = PrintStream(ComboOutputStream(logStream, System.out), true) + System.setErr(outStream) + System.setOut(outStream) + } + + override fun initialize() { + + System.setProperty("sentry.dist", Gdx.app.type.name) + // val keyGen1 = KeyPairGenerator.getInstance("DIFFIEHELLMAN") + System.setProperty("sentry.release", App.app.versionString) + println("sentry.release ${App.app.versionString} sentry.dist ${Gdx.app.type.name}") + + if (Prefs.BinPref.CRASH_REPORTS.isEnabled()) Sentry.init() + +// Sentry.getContext().recordBreadcrumb( +// BreadcrumbBuilder().setMessage("User made an action").build() +// ) +// +// Sentry.getContext().user = UserBuilder().setEmail("hello@sentry.io").build() +// Sentry.getContext().addExtra("extra", "thing") +// Sentry.getContext().addTag("tagName", "tagValue") + + // Sentry.capture("This is a test2") + } + + override fun log(message: String) { + val caller = org.slf4j.helpers.Util.getCallingClass().simpleName + log(caller, message) + } + + override fun log(caller: String, message: String) { + println("[${LocalDateTime.now()}] [$caller] $message") + Sentry.getContext().recordBreadcrumb( + BreadcrumbBuilder().setMessage(message).setCategory(caller).build() + ) + } +} diff --git a/src/uk/me/fantastic/retro/utils/SimpleLogger.kt b/src/uk/me/fantastic/retro/utils/SimpleLogger.kt new file mode 100644 index 0000000..7de23d0 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/SimpleLogger.kt @@ -0,0 +1,33 @@ +package uk.me.fantastic.retro.utils + +import uk.me.fantastic.retro.App +import uk.me.fantastic.retro.Logger +import java.io.File +import java.io.PrintStream +import java.time.LocalDateTime + +class SimpleLogger : Logger { + override fun error(message: String) { + println(message) + } + + init { + val logFile = File(App.LOG_FILE_PATH) + val logStream = logFile.outputStream().buffered() + val outStream = PrintStream(ComboOutputStream(logStream, System.out), true) + System.setErr(outStream) + System.setOut(outStream) + } + + override fun initialize() { + } + + override fun log(message: String) { + val caller = org.slf4j.helpers.Util.getCallingClass().simpleName + log(caller, message) + } + + override fun log(caller: String, message: String) { + println("[${LocalDateTime.now()}] [$caller] $message") + } +} diff --git a/src/uk/me/fantastic/retro/utils/SpriteSheet.kt b/src/uk/me/fantastic/retro/utils/SpriteSheet.kt new file mode 100644 index 0000000..b9952da --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/SpriteSheet.kt @@ -0,0 +1,55 @@ +package uk.me.fantastic.retro.utils + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.TextureRegion +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import com.beust.klaxon.obj + +class SpriteSheet(val file: String) { + + private val frames: JsonObject + private val sheet: Texture + + init { + val json = Parser().parse(Gdx.files.internal(file).reader()) as JsonObject + val meta = json["meta"] as JsonObject + val fn = meta["image"] as String + sheet = Texture(fn) + frames = json["frames"] as JsonObject + } + + fun getTextureRegion(name: String, num: Int): TextureRegion { + return getTextureRegion("$name$num") + } + + fun getFrames(tag: String): List { + return frames.filter { it.key.startsWith(tag + "-") }.map { getTextureRegion(it.key) } + } + + fun getFrameDelays(tag: String): List { + return frames.filter { it.key.startsWith(tag + "-") }.map { getFrameDelay(it.key) } + } + + private fun getFrameDelay(r: String): Float { + val json = frames.obj(r) as JsonObject + val ms = json["duration"] as Int + + return ms.toFloat() / 1000f + } + + private fun getTextureRegion(r: String): TextureRegion { + val frame = frames.obj(r)?.obj("frame") as JsonObject + val x = frame["x"] as Int + val y = frame["y"] as Int + val w = frame["w"] as Int + val h = frame["h"] as Int + return TextureRegion(sheet, x, y, w, h) + } + + fun getAnim(name: String): AnimatedTexture { + val d = getFrameDelays(name).first() + return AnimatedTexture(d, this, name) + } +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/utils/Vec.kt b/src/uk/me/fantastic/retro/utils/Vec.kt new file mode 100644 index 0000000..fdbb3a7 --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/Vec.kt @@ -0,0 +1,20 @@ +package uk.me.fantastic.retro.utils + +/** + * 2d vector + */ +class Vec(val x: Float, val y: Float) { + constructor() : this(0f, 0f) + + fun xy(): Pair { + return Pair(x, y) + } + + fun normVector(): Vec { + + val vMagnitude = (x * x + y * y).sqrt() + return Vec(x / vMagnitude, y / vMagnitude) + } + + infix operator fun minus(p: Vec): Vec = Vec(x - p.x, y - p.y) +} \ No newline at end of file diff --git a/src/uk/me/fantastic/retro/utils/utils.kt b/src/uk/me/fantastic/retro/utils/utils.kt new file mode 100644 index 0000000..277eced --- /dev/null +++ b/src/uk/me/fantastic/retro/utils/utils.kt @@ -0,0 +1,37 @@ +// +/* + Copyright 2018 Richard Smith. + + RetroWar 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. + + RetroWar is distributed in the hope that 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 RetroWar. If not, see . +*/ +// +package uk.me.fantastic.retro.utils + +/** + * Global scope stuff that IntelliJ doesn't like being in the actual files where it is used + */ + +/** @suppress */ +fun Float.sqrt(): Float { + val f = this + var y = java.lang.Float.intBitsToFloat(0x5f375a86 - (java.lang.Float.floatToIntBits(f) shr 1)) // evil floating point bit level hacking -- Use 0x5f375a86 instead of 0x5f3759df, due to slight accuracy increase. (Credit to Chris Lomont) + y *= (1.5f - 0.5f * f * y * y) // Newton step, repeating increases accuracy + return f * y +} + +/** @suppress */ +fun Float.sqrta(): Float { + val f = this + return f * java.lang.Float.intBitsToFloat(0x5f375a86 - (java.lang.Float.floatToIntBits(f) shr 1)) // evil floating point bit level hacking -- Use 0x5f375a86 instead of 0x5f3759df, due to slight accuracy increase. (Credit to Chris Lomont) +}