[
  {
    "path": ".gitattributes",
    "content": "* text=auto\n"
  },
  {
    "path": ".github/workflows/maven.yml",
    "content": "# This workflow will build a Java project with Maven\n# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven\n\nname: Java CI with Maven\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Set up JDK 1.8\n      uses: actions/setup-java@v1\n      with:\n        java-version: 1.8\n    - name: Build with Maven\n      run: mvn -B package --file pom.xml\n"
  },
  {
    "path": ".gitignore",
    "content": "# Maven\ntarget\n\n# IntelliJ IDEA\n.idea\n.settings\n*.iml\n\n*.gb\n*.sav\n*.wav\n*.raw\n./*.lsdprj\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n### Added\n - ROM Upgrade Tool: \"Select ROM file...\" button allows upgrading to arbitrary ROM files instead of only downloading latest stable or experimental from littlesounddj.com.\n\n## [1.13.3] - 2024-09-20\n### Fixed\n - Kit Editor: tiny window size on Linux.\n\n## [1.13.1] - 2023-01-27\n### Fixed\n - Palette Editor: Updated color correction to match Sameboy 0.15.x.\n\n## [1.13.0] - 2022-04-11\n### Fixed\n - Filename filtering in non-Windows file dialogs broke in 1.8.0.\n\n### Added\n - Palette Editor: Color space combo box.\n - Palette Editor: \"Reality\" color space, which looks close to real Game Boy Color.\n\n## [1.12.0] - 2021-10-01\n### Added\n - Kit Editor: \"Invert Polarity for GBA\" preference.\n\n### Changed\n - Kit Editor: Update dither per sample.\n - Kit Editor: Moved \"half-speed\" preference to menu.\n - ROM files may now use .gbc extension.\n\n## [1.11.6] - 2021-08-18\n### Fixed\n - Kit Editor: Dither preference.\n - Kit Editor: Allow filenames less than 3 characters in length.\n\n### Changed\n - Kit Editor: Moved dither preference to main window.\n \n### Added\n - Kit Editor: \"Duplicate sample\" and \"Copy\" in context menu\n - Kit Editor: Edit menu for \"Trim all samples to equal length\" and \"Paste\"\n\n## [1.11.5] - 2021-06-16\n### Changed\n - Kit Editor: Tweaked resampler lowpass-filter settings to preserve more treble.\n\n### Added\n - Kit Editor: Preferences menu for low-pass filter and dither.\n\n## [1.11.4] - 2021-06-01\n### Fixed\n - Palette Editor: Color picker could not pick black (0,0,0) due to UI scaling bug.\n\n### Added\n - Palette Editor: Show selected color RGB value in color picker.\n\n## [1.11.3] - 2021-04-25\n### Changed\n - Kit Editor: reverted 1.11.2 changes due to cymbal dithering problems.\n\n## [1.11.2] - 2021-04-23\n### Changed\n - Kit Editor: tweaked DC level to reduce DMG noise. works best with\n   lsdj 9.2.2 and above.\n\n## [1.11.1] - 2021-04-18\n### Changed\n - Kit Editor: added internal versioning field to kits, that tells if they were\n   created using lsdpatcher 1.11.1 or above. this allows adding old kits to\n   lsdj 9.2.0+ without loss of quality.\n\n## [1.11.0] - 2021-04-15\n### Fixed\n - Kit Editor: sample preview playback had inverted polarity.\n - Song Manager: remember last used .lsdprj path.\n - Clearing kits would not free up space for adding .lsdprj songs.\n - Minor UI issues.\n\n### Changed\n - Kit Editor: when creating kits, wave frames are now rotated right, so that\n   the sample to be played back last is written first.\n   this compensates for the Game Boy wave refresh bug that plays back samples\n   in wrong order after changing wave.\n   best used with Little Sound Dj 9.2.0 and above.\n\n## [1.10.5] - 2021-03-05\n### Fixed\n - Kit Editor: inversed sample polarity. broken since always.\n - Kit Editor: garbled sample names when renaming an uninitialized kit.\n - Kit Editor: slow switching to high numbered kits.\n - ROM upgrade tool would not find new versions ending with A-Z.\n - Disabled ROM upgrade if there are unsaved changes.\n\n### Added\n - Kit Editor: F1/F2 shortcut for previous/next bank.\n - Kit Editor: Sample pad tooltip.\n\n### Removed\n - Kit Editor: wave blending, which didn't quite work due to timing issues.\n\n## [1.10.4] - 2021-02-22\n### Fixed\n - Kit Editor: removed sample prelisten low-pass filter.\n\n## [1.10.3] - 2021-02-12\n### Fixed\n - Kit Editor: various volume and trimming errors.\n - Font Editor: avoid duplicate font names.\n - Font Editor: .png file extension got included in font name when loading a font PNG.\n - Error handling when palettes cannot be parsed.\n - .sav file not found for ROM images ending with \".gb.gb\".\n - ROM upgrade did not preserve graphics characters.\n\n### Changed\n - Subwindows are now modal.\n\n### Added\n - Kit Editor: bank switching buttons.\n\n## [1.10.2] - 2020-11-22\n### Fixed\n - Kit Editor: when replacing samples, trim sample end to fit.\n\n## [1.10.1] - 2020-11-10\n### Fixed\n - New version check at startup.\n\n## [1.10.0] - 2020-11-10\n### Fixed\n - Kit Editor: sample duration right alignment.\n - Kit Editor: made text fields handle \"enter\" key.\n - Kit Editor: stop listening to keypresses when window is closed.\n - Kit Editor: make focus return to main window when bank is changed.\n - Various window resize issues.\n\n### Added\n - Kit Editor: pitch spinner, for changing sample pitch by semitone.\n - Kit Editor: trim spinner, for reducing sample length.\n - New version check at startup.\n\n### Changed\n - Kit Editor: now removes DC offset before resampling.\n - Kit Editor: changed \"Reload samples\" button to \"Reload sample\".\n - Kit Editor: when adding a too big sample, trim sample end to fit.\n - Kit Editor: disabled \"Add sample\" button when kit is full.\n\n## [1.9.0] - 2020-10-31\n### Fixed\n - Kit Editor: dramatically improved resampling using libresample4j.\n - Kit Editor: refresh sample view when the sample is reloaded.\n - Kit Editor: update \"seconds free\" after volume change.\n - Kit Editor: pad kit banks with \"rst\" instead of \"nop\" instructions, for crash detection.\n\n### Added\n - Kit Editor: print sample duration in sample view.\n\n## [1.8.1] - 2020-10-25\n### Fixed\n - Kit Editor: loading kits with sample volumes stored in settings file.\n - Kit Editor: reduced wave blending noise for emulators that do not have the Game Boy wave refresh bug.\n - Palette Editor: force palette names to upper case.\n\n## [1.8.0] - 2020-10-24\n### Fixed\n - Palette Editor: avoid duplicate palette names when loading a palette.\n - Palette Editor: dragging color picker sliders is now more responsive.\n - Palette Editor: improved color picker visibility.\n - Kit Editor: update of \"bytes used\" field.\n - Font Editor: when loading font from .png, set font name from the file name.\n - Some file dialogs would not remember the last used directory.\n - Saving a ROM when no SAV has been loaded.\n - Switching .sav would not take effect until loading a ROM.\n\n### Added\n - Kit Editor: MPC-like UI with pads. Play by clicking or keys 1234QWERASDFZXC. Right-click pad to rename, replace or delete sample.\n - Kit Editor: \"Reload samples\", \"Save ROM\", \"Clear kit\" buttons.\n - Kit Editor: automatic silence trimming.\n - Kit Editor: when saving kits, remember source sample files + volumes.\n - Font Editor: support for editing graphics characters.\n\n### Changed\n - Kit Editor: \"Add Sample\" now automatically resamples, normalizes and dithers the sample. No need to prepare samples using sox anymore.\n - Kit Editor: switched to TPDF dither for improved sound.\n - Kit Editor: when adding samples, blend wave frames to reduce impact of the [Game Boy wave refresh bug](https://www.devrs.com/gb/files/gbsnd3.gif).\n - Kit Editor: volume control now adjusts sample volume instead of pre-listening volume.\n - Kit Editor: improved sound playback quality.\n - Kit Editor: click sample view to play.\n - Kit Editor: half-speed setting now also affects \"Add sample\".\n - Kit Editor: renamed \"Export kit\" to \"Save kit\".\n - Kit Editor: show unused space in seconds instead of bytes.\n - Palette Editor: improved mid-tone generation.\n - Various file dialog improvements.\n - Improved command line feedback.\n\n### Removed\n - Font Editor: removed saving of .lsdfnt files, as well as loading/saving multiple fonts in one go.\n - Kit Editor: \"Play sample on click\" toggle.\n - Kit Editor: \"Export all samples\" button.\n\n## [1.7.0] - 2020-10-06\n### Fixed\n - Kit Editor: sample export broke in 1.6.0.\n - Song Editor: incorrect broken-song warnings. thx michael dufault!\n\n### Added\n - Palette Editor: color picker!\n - Palette Editor: click in lsdj screens to select color.\n - Palette Editor: \"swap color\" and \"clone color\" buttons.\n - Palette Editor: \"raw\" button, which displays colors as-is.\n\n### Changed\n - Palette Editor: switched color correction from Gambatte to Sameboy.\n - Palette Editor: updated screenshots to LSDj v8.9.0.\n - Palette Editor: create brighter mid-tones if the background is brighter than the foreground.\n - Each file extension now has its own last used load/save file path.\n\n### Removed\n - Palette Editor: color spinners.\n\n## [1.6.0] - 2020-10-02\n### Fixed\n - Kit sample playback got stuck at times.\n - Old LSDj ROMs (like, v3) would not open.\n\n### Added\n - Startup dialog to choose ROM, SAV and sub-tool.\n - Added song manager from LSDManager project.\n - Song manager warning on corrupted songs.\n - Song manager now saves LSDj Project files (.lsdprj) which contains both song and sample kits.\n - Upgrade ROM button, which downloads the latest ROM images from https://www.littlesounddj.com. The upgrade preserves custom kits, fonts and palettes.\n - Palette editor randomize button.\n\n### Changed\n - Palette editor layout.\n\n## [1.5.0] - 2020-09-13\n### Changed\n - Merged LSDPatcher Redux v1.4.6. Full list of changes at [LSDPatcher Redux release page](https://github.com/Eiyeron/lsdpatch/releases).\n\n## [1.4.2] - 2020-08-19\n### Added\n - LSDj v8.8.3 support.\n\n## [1.4.1] - 2020-07-04\n### Added\n - LSDj v8.7.4 support.\n\n## [1.4.0] - 2020-07-02\n### Added\n - Palette editor \"Desaturate preview\" toggle.\n - Palette editor copy/paste.\n\n## [1.3.0] - 2020-06-27\n### Added\n - Allow variable number of palettes. Some LSDj versions have 6 palettes, others 7.\n\n## [1.2.0] - 2020-03-05\n### Changed\n - Java 8 now required.\n - Brighter background shade for DMG fonts.\n\n## [1.1.6] - 2017-10-19\n### Fixed\n - Recalculate ROM checksum on save.\n\n## [1.1.5] - 2017-10-14\n### Added\n - \"Import kits from ROM\" button.\n\n## [1.1.4] - 2017-05-07\n### Changed\n - Kit selector is now hexadecimal.\n - Brought back dot in kit list.\n\n## [1.1.3] - 2017-01-26\n### Changed\n - Made palette editor a bit bigger.\n\n### Fixed\n - Shaded and inverted tiles would not always be generated by font editor.\n\n## [1.1.2] - 2017-01-23\n### Added\n - Load and save palettes.\n\n### Fixed\n - When loading a ROM, palettes would be added twice.\n - Errors related to combo box in font editor.\n\n## [1.1.1] - 2017-01-23\n### Added\n - Font renaming.\n - Font editor grid.\n - Include font name in .lsdfnt\n\n### Fixed\n - Out-of-bounds drawing in font editor.\n\n## [1.1.0] - 2017-01-23\n### Added\n - Font editor.\n\n## [1.0.2] - 2017-01-20\n### Changed\n - Made preview screens in palette editor bigger.\n\n## [1.0.1] - 2017-01-20\n### Fixed\n - Wrong behavior when loading invalid ROM images.\n\n### Changed\n - Lowered required JRE version to 1.6.\n\n## [1.0.0] - 2017-01-20\n### Added\n - Palette editor, entered by menu option Palette->Edit Palette.\n\n## [0.19] - 2011-08-20\n### Fixed\n - Loading a long sample threw a confusing, empty error message. Thanks to Clay Morrow for reporting.\n\n\n[unreleased]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.3..HEAD\n[1.13.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.2..v1.13.3\n[1.13.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.1..v1.13.2\n[1.13.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.0..v1.13.1\n[1.13.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.12.0..v1.13.0\n[1.12.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.6..v1.12.0\n[1.11.6]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.5..v1.11.6\n[1.11.5]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.4..v1.11.5\n[1.11.4]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.3..v1.11.4\n[1.11.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.2..v1.11.3\n[1.11.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.1..v1.11.2\n[1.11.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.0..v1.11.1\n[1.11.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.5..v1.11.0\n[1.10.5]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.4..v1.10.5\n[1.10.4]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.3..v1.10.4\n[1.10.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.2..v1.10.3\n[1.10.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.1..v1.10.2\n[1.10.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.0..v1.10.1\n[1.10.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.9.0..v1.10.0\n[1.9.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.8.1..v1.9.0\n[1.8.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.8.0..v1.8.1\n[1.8.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.7.0..v1.8.0\n[1.7.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.6.0..v1.7.0\n[1.6.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.5.0..v1.6.0\n[1.5.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.4.2..v1.5.0\n[1.4.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.4.1..v1.4.2\n[1.4.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.4.0..v1.4.1\n[1.4.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.3.0..v1.4.0\n[1.3.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.2.0..v1.3.0\n[1.2.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.6..v1.2.0\n[1.1.6]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.5..v1.1.6\n[1.1.5]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.4..v1.1.5\n[1.1.4]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.3..v1.1.4\n[1.1.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.2..v1.1.3\n[1.1.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.1..v1.1.2\n[1.1.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.0..v1.1.1\n[1.1.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.0.2..v1.1.0\n[1.0.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.0.1..v1.0.2\n[1.0.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.0.0..v1.0.1\n[1.0.0]: https://github.com/jkotlinski/lsdpatch/compare/v0.19...v1.0.0\n[0.19]: https://github.com/jkotlinski/lsdpatch/releases/tag/v0.19\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (C) 2001 by Johan Kotlinski, 2017 by Florian Dormont\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n===============================================================================\n\nFiledrop.java, Copyright (C) 2007 by Robert Harder. No rights reserved.\n\n===============================================================================\n\nStretchIcon.java, Copyright (C) 2016 by Darryl Burke. No rights reserved.\n\n===============================================================================\n\nColor correction routine from Sameboy:\n\nMIT License\n\nCopyright (c) 2015-2020 Lior Halphon\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n===============================================================================\n\nlibresample4j\nCopyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved.\n\nlibresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3,\nwhich is in turn based on Julius Smith's Resample 1.7 library.\n     http://www-ccrma.stanford.edu/~jos/resample/\n\nLicense: LGPL -- see the file LICENSE.txt for more information\n\n--\n\nThis product includes software derived from the work of\nJulius Smith and Dominic Mazzoni\n(http://ccrma-www.stanford.edu/~jos/resample/Free_Resampling_Software.html)\n\nlibresample 0.1.3\nCopyright 2003 Dominic Mazzoni <dominic@minorninth.com>.\n\nResample 1.7\nCopyright 1994-2002 Julius O. Smith III <jos@ccrma.stanford.edu>,\nall rights reserved.\n"
  },
  {
    "path": "README.md",
    "content": "# LSDPatcher\n\nA tool for modifying songs, samples, fonts and palettes on [Little Sound Dj][lsdj] (LSDj) ROM images and save files. Requires\n[Java][java] 8+. If you have problems running the .jar on Windows, try [Jarfix][jarfix].\n\n[Download][releases] | [Fonts][lsdfnts] | [Palettes][lsdpals]\n\n## Building\n\nBuild using [Maven](https://maven.apache.org/): `mvn package`\n\n![Java CI with Maven](https://github.com/jkotlinski/lsdpatch/workflows/Java%20CI%20with%20Maven/badge.svg)\n\n[lsdj]: https://www.littlesounddj.com/\n[sox]: http://sox.sourceforge.net/\n[releases]: https://github.com/jkotlinski/lsdpatch/releases\n[wiki]: https://github.com/jkotlinski/lsdpatch/wiki/Documentation\n[jarfix]: http://johann.loefflmann.net/en/software/jarfix/index.html\n[java]: http://www.java.com/\n[lsdfnts]: https://github.com/psgcabal/lsdfonts\n[lsdpals]: https://github.com/psgcabal/lsdpals\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.littlesounddj</groupId>\n    <artifactId>lsdpatch</artifactId>\n    <version>1.13.3</version>\n    <packaging>jar</packaging>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.miglayout</groupId>\n            <artifactId>miglayout</artifactId>\n            <version>3.7.4</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>5.7.0</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>3.0.0-M5</version>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.8.1</version>\n                <configuration>\n                    <compilerArgument>-Xlint:deprecation</compilerArgument>\n                    <compilerArgument>-XDignore.symbol.file</compilerArgument>\n                    <source>1.8</source>\n                    <target>1.8</target>\n                    <encoding>UTF-8</encoding>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-assembly-plugin</artifactId>\n                <groupId>org.apache.maven.plugins</groupId>\n                <version>3.1.0</version>\n                <executions>\n                    <execution>\n                        <id>make-executable-jar-with-dependencies</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>single</goal>\n                        </goals>\n                        <configuration>\n                            <appendAssemblyId>false</appendAssemblyId>\n                            <archive>\n                                <manifest>\n                                    <addDefaultImplementationEntries>true</addDefaultImplementationEntries>\n                                    <addClasspath>true</addClasspath>\n                                    <mainClass>lsdpatch.LSDPatcher</mainClass>\n                                </manifest>\n                            </archive>\n                            <descriptorRefs>\n                                <descriptorRef>jar-with-dependencies</descriptorRef>\n                            </descriptorRefs>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n\n    </build>\n</project>\n"
  },
  {
    "path": "src/main/java/Document/Document.java",
    "content": "package Document;\n\nimport utils.EditorPreferences;\nimport utils.RomUtilities;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.util.Arrays;\nimport java.util.LinkedList;\nimport java.util.List;\n\npublic class Document {\n    private boolean romDirty;\n    private byte[] romImage;\n    private File romFile;\n\n    private boolean savDirty;\n    private LSDSavFile savFile = new LSDSavFile();\n\n    private final List<IDocumentListener> documentListeners = new LinkedList<>();\n\n    public void subscribe(IDocumentListener documentListener) {\n        documentListeners.add(documentListener);\n    }\n\n    public File romFile() {\n        return romFile;\n    }\n\n    private void publishDocumentDirty() {\n        for (IDocumentListener documentListener : documentListeners) {\n            documentListener.onDocumentDirty(isDirty());\n        }\n    }\n\n    private void setRomDirty(boolean dirty) {\n        romDirty = dirty;\n        publishDocumentDirty();\n    }\n\n    private void setSavDirty(boolean dirty) {\n        savDirty = dirty;\n        publishDocumentDirty();\n    }\n\n    public byte[] romImage() {\n        return romImage == null ? null : romImage.clone();\n    }\n\n    public void setRomImage(byte[] romImage) {\n        if (Arrays.equals(romImage, this.romImage)) {\n            return;\n        }\n        this.romImage = romImage;\n        setRomDirty(true);\n    }\n\n    public void loadRomImage(String romPath) throws IOException {\n        romFile = new File(romPath);\n        romImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n        setRomDirty(false);\n        try {\n            RandomAccessFile f = new RandomAccessFile(romFile, \"r\");\n            f.readFully(romImage);\n            f.close();\n            EditorPreferences.setLastPath(\"gb\", romPath);\n        } catch (IOException ioe) {\n            romImage = null;\n            throw ioe;\n        }\n    }\n\n    public void loadSavFile(String savPath) throws IOException {\n        setSavDirty(false);\n        try {\n            savFile = new LSDSavFile();\n            savFile.loadFromSav(savPath);\n            EditorPreferences.setLastPath(\"sav\", savPath);\n        } catch (IOException e) {\n            savFile = null;\n            throw e;\n        }\n    }\n\n    public LSDSavFile savFile() {\n        if (savFile == null) {\n            return null;\n        }\n        try {\n            return savFile.clone();\n        } catch (CloneNotSupportedException e) {\n            return null;\n        }\n    }\n\n    public void setSavFile(LSDSavFile savFile) {\n        if (savFile == null) {\n            this.savFile = null;\n            setSavDirty(false);\n            return;\n        }\n        if (this.savFile != null && savFile.equals(this.savFile)) {\n            return;\n        }\n        this.savFile = savFile;\n        setSavDirty(true);\n    }\n\n    public boolean isSavDirty() {\n        return savDirty;\n    }\n\n    public boolean isRomDirty() {\n        return romDirty;\n    }\n\n    public boolean isDirty() {\n        return romDirty || savDirty;\n    }\n\n    public void setRomFile(File file) {\n        romFile = file;\n    }\n\n    public void clearSavDirty() {\n        setSavDirty(false);\n    }\n\n    public void clearRomDirty() {\n        setRomDirty(false);\n    }\n}\n"
  },
  {
    "path": "src/main/java/Document/IDocumentListener.java",
    "content": "package Document;\n\npublic interface IDocumentListener {\n    void onDocumentDirty(boolean dirty);\n}\n"
  },
  {
    "path": "src/main/java/Document/LSDSavFile.java",
    "content": "package Document;\n\nimport utils.RomUtilities;\n\nimport java.io.*;\nimport java.util.*;\nimport javax.swing.*;\n\npublic class LSDSavFile implements Cloneable {\n    final int blockSize = 0x200;\n    final int bankSize = 0x8000;\n    final int bankCount = 4;\n    final int savFileSize = bankSize * bankCount;\n    final int songCount = 0x20;\n    final int fileNameLength = 8;\n\n    final int fileNameStartPtr = 0x8000;\n    final int fileVersionStartPtr = 0x8100;\n    final int blockAllocTableStartPtr = 0x8141;\n    final int blockStartPtr = 0x8200;\n    final int activeFileSlot = 0x8140;\n    final char emptySlotValue = (char) 0xff;\n\n    boolean is64kb = false;\n    boolean is64kbHasBeenSet = false;\n\n    byte[] workRam;\n\n    public LSDSavFile() {\n        workRam = new byte[savFileSize];\n    }\n\n    public LSDSavFile clone() throws CloneNotSupportedException {\n        LSDSavFile copy = (LSDSavFile)super.clone();\n        copy.is64kb = is64kb;\n        copy.is64kbHasBeenSet = is64kbHasBeenSet;\n        copy.workRam = workRam.clone();\n        return copy;\n    }\n\n    public boolean equals(LSDSavFile rhs) {\n        return Arrays.equals(workRam, rhs.workRam);\n    }\n\n    private boolean isSixtyFourKbRam() {\n        if (is64kbHasBeenSet) return is64kb;\n\n        for (int i = 0; i < 0x10000; ++i) {\n            if (workRam[i] != workRam[0x10000 + i]) {\n                is64kb = false;\n                is64kbHasBeenSet = true;\n                return false;\n            }\n        }\n        is64kb = true;\n        is64kbHasBeenSet = true;\n        return true;\n    }\n\n    public int totalBlockCount() {\n        // FAT takes one block.\n        return isSixtyFourKbRam() ? 0xbf - 0x80 : 0xbf;\n    }\n\n    public void saveAs(String filePath) throws IOException {\n        try (FileOutputStream fileOutputStream = new FileOutputStream(filePath)) {\n            if (isSixtyFourKbRam()) {\n                System.arraycopy(workRam, 0, workRam, 65536, 0x10000);\n            }\n            fileOutputStream.write(workRam);\n        }\n    }\n\n    public void clearSong(int index) {\n        int ramPtr = blockAllocTableStartPtr;\n        int block = 0;\n\n        while (block < totalBlockCount()) {\n            int tableValue = workRam[ramPtr];\n            if (index == tableValue) {\n                workRam[ramPtr] = (byte) emptySlotValue;\n            }\n            ramPtr++;\n            block++;\n        }\n\n        clearFileName(index);\n        clearFileVersion(index);\n\n        if (index == getActiveFileSlot()) {\n            clearActiveFileSlot();\n        }\n    }\n\n    public int getBlocksUsed(int slot) {\n        int ramPtr = blockAllocTableStartPtr;\n        int block = 0;\n        int blockCount = 0;\n\n        while (block++ < totalBlockCount()) {\n            if (slot == workRam[ramPtr++]) {\n                blockCount++;\n            }\n        }\n        return blockCount;\n    }\n\n    private void clearFileName(int index) {\n        workRam[fileNameStartPtr + fileNameLength * index] = (byte) 0;\n    }\n\n    private void clearFileVersion(int index) {\n        workRam[fileVersionStartPtr + index] = (byte) 0;\n    }\n\n    public int usedBlockCount() {\n        return totalBlockCount() - freeBlockCount();\n    }\n\n    private byte getNewSongId() {\n        for (byte slot = 0; slot < songCount; slot++) {\n            if (0 == getBlocksUsed(slot)) {\n                return slot;\n            }\n        }\n        return -1;\n    }\n\n    private int getBlockIdOfFirstFreeBlock() {\n        int blockAllocTableStartPtr = this.blockAllocTableStartPtr;\n        int block = 0;\n\n        while (block < totalBlockCount()) {\n            int tableValue = workRam[blockAllocTableStartPtr++];\n            if (tableValue < 0 || tableValue > 0x1f) {\n                return block;\n            }\n            block++;\n        }\n        return -1;\n    }\n\n    /*\n    public void debug_dump_fat()\n    {\n        int l_ram_ptr = g_block_alloc_table_start_ptr;\n        int l_block = 0;\n\n        while (l_block < getTotalBlockCount())\n        {\n            int l_table_value = m_work_ram[l_ram_ptr++];\n            System.out.print(l_table_value + \" \" );\n            l_block++;\n        }\n        System.out.println();\n    }\n    */\n\n    public int freeBlockCount() {\n        int ramPtr = blockAllocTableStartPtr;\n        int block = 0;\n        int freeBlockCount = 0;\n\n        while (block < totalBlockCount()) {\n            int tableValue = workRam[ramPtr++];\n            if (tableValue < 0 || tableValue > 0x1f) {\n                freeBlockCount++;\n            }\n            block++;\n        }\n        return freeBlockCount;\n    }\n\n    public void loadFromSav(String filePath) throws IOException {\n        RandomAccessFile savFile = new RandomAccessFile(filePath, \"r\");\n        savFile.readFully(workRam);\n        savFile.close();\n\n        is64kbHasBeenSet = false;\n    }\n\n    public void populateSongList(JList<String> songList) {\n        String[] songStringList = new String[songCount];\n        songList.removeAll();\n\n        for (int song = 0; song < songCount; song++) {\n            int blocksUsed = getBlocksUsed(song);\n            String songString = song + 1 + \". \";\n\n            if (blocksUsed > 0) {\n                songString += getFileName(song);\n                songString += \".\" + version(song);\n                songString += \" \" + blocksUsed;\n                if (!isValid(song)) {\n                    songString += \" \\u26a0\"; // warning sign\n                }\n            }\n\n            songStringList[song] = songString;\n        }\n\n        songList.setListData(songStringList);\n    }\n\n    private static int convertLsdCharToAscii(int ch) {\n        if (ch >= 65 && ch <= (65 + 25)) {\n            //char\n            return 'A' + ch - 65;\n        }\n        if (ch >= 48 && ch < 58) {\n            //decimal number\n            return '0' + ch - 48;\n        }\n        return 0 == ch ? 0 : ' ';\n    }\n\n    public String getFileName(int slot) {\n        StringBuilder sb = new StringBuilder();\n        int ramPtr = fileNameStartPtr + fileNameLength * slot;\n        boolean endOfFileName = false;\n        for (int fileNamePos = 0;\n             fileNamePos < 8;\n             fileNamePos++) {\n            if (!endOfFileName) {\n                char ch = (char) convertLsdCharToAscii((char)\n                        workRam[ramPtr]);\n                if (0 == ch) {\n                    endOfFileName = true;\n                } else {\n                    sb.append(ch);\n                }\n            }\n            ramPtr++;\n        }\n        return sb.toString();\n    }\n\n    public String version(int slot) {\n        int ramPtr = fileVersionStartPtr + slot;\n        String version = Integer.toHexString(workRam[ramPtr]);\n        return version.substring(Math.max(version.length() - 2, 0)).toUpperCase();\n    }\n\n    public void exportSongToFile(int songId, String filePath, byte[] romImage) {\n        assert (songId >= 0 && songId < 0x20);\n\n        RandomAccessFile file;\n        try {\n            file = new RandomAccessFile(filePath, \"rw\");\n\n            int fileNamePtr = fileNameStartPtr + songId * fileNameLength;\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr++]);\n            file.writeByte(workRam[fileNamePtr]);\n\n            int fileVersionPtr = fileVersionStartPtr + songId;\n            file.writeByte(workRam[fileVersionPtr]);\n\n            writeSongBlocks(songId, file);\n            writeKits(romImage, songId, file);\n\n            file.close();\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(null,\n                    e.getMessage(),\n                    \"Song export failed!\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n    }\n\n    private void writeKits(byte[] romImage, int songId, RandomAccessFile file) throws IOException {\n        TreeSet<Integer> kitsToWrite = usedKits(songId);\n        while (true) {\n            Integer kit = kitsToWrite.pollFirst();\n            if (kit == null) {\n                break;\n            }\n            // because legacy, kits are in banks 8-26, 32-63.\n            kit += 8;\n            if (kit > 26) {\n                kit += 5;\n            }\n            int kitOffset = kit * 0x4000;\n            for (int i = 0; i < 0x4000; ++i) {\n                file.writeByte(romImage[kitOffset + i]);\n            }\n        }\n    }\n\n    TreeSet<Integer> usedKits(int songId) {\n        byte[] unpackedSong = unpackSong(songId);\n        assert (unpackedSong != null);\n        assert (unpackedSong.length == 0x8000);\n\n        TreeSet<Integer> kits = new TreeSet<>();\n        for (int instr = 0; instr < 0x40; ++instr) {\n            int instrPtr = 0x3080 + instr * 0x10;\n            if (unpackedSong[instrPtr] != 2) {\n                continue; // Not kit instrument.\n            }\n            kits.add(unpackedSong[instrPtr + 2] & 0x3f);\n            kits.add(unpackedSong[instrPtr + 9] & 0x3f);\n        }\n        return kits;\n    }\n\n    void writeSongBlocks(int songId, RandomAccessFile file) throws IOException {\n        int blockId = 0;\n        int blockAllocTablePtr = blockAllocTableStartPtr;\n\n        while (blockId < totalBlockCount()) {\n            if (songId == workRam[blockAllocTablePtr++]) {\n                int blockPtr = blockStartPtr + blockId * blockSize;\n                for (int byteIndex = 0; byteIndex < blockSize; byteIndex++) {\n                    file.writeByte(workRam[blockPtr++]);\n                }\n            }\n            blockId++;\n        }\n    }\n\n    /**\n     * Decodes a song. Returns 32 kB with decoded song data, or null on failure.\n     */\n    private byte[] unpackSong(int songId) {\n        byte[] dstBuffer = new byte[0x8000];\n        int dstPos = 0;\n\n        int blockId = 0;\n        int blockAllocTablePtr = blockAllocTableStartPtr;\n\n        while (blockId < totalBlockCount()) {\n            if (songId == workRam[blockAllocTablePtr++]) {\n                break;\n            }\n            blockId++;\n        }\n\n        int srcPtr = blockStartPtr + blockSize * blockId;\n\n        try {\n            while (true) {\n                switch (workRam[srcPtr]) {\n                    case (byte) 0xc0:\n                        srcPtr++;\n                        if (workRam[srcPtr] == (byte) 0xc0) {\n                            srcPtr++;\n                            dstBuffer[dstPos++] = (byte) 0xc0;\n                        } else {\n                            // rle\n                            byte b = workRam[srcPtr++];\n                            byte count = workRam[srcPtr++];\n                            while (count-- != 0) {\n                                dstBuffer[dstPos++] = b;\n                            }\n                        }\n                        break;\n\n                    case (byte) 0xe0:\n                        byte count;\n                        srcPtr++;\n                        switch (workRam[srcPtr]) {\n                            case (byte) 0xe0: // e0\n                                srcPtr++;\n                                dstBuffer[dstPos++] = (byte) 0xe0;\n                                break;\n\n                            case (byte) 0xff: // done!\n                                return dstPos == 0x8000 ? dstBuffer : null;\n\n                            case (byte) 0xf0: //wave\n                                srcPtr++;\n                                count = workRam[srcPtr++];\n                                while (count-- != 0) {\n                                    dstBuffer[dstPos++] = (byte) 0x8e;\n                                    dstBuffer[dstPos++] = (byte) 0xcd;\n                                    dstBuffer[dstPos++] = (byte) 0xcc;\n                                    dstBuffer[dstPos++] = (byte) 0xbb;\n                                    dstBuffer[dstPos++] = (byte) 0xaa;\n                                    dstBuffer[dstPos++] = (byte) 0xa9;\n                                    dstBuffer[dstPos++] = (byte) 0x99;\n                                    dstBuffer[dstPos++] = (byte) 0x88;\n                                    dstBuffer[dstPos++] = (byte) 0x87;\n                                    dstBuffer[dstPos++] = (byte) 0x76;\n                                    dstBuffer[dstPos++] = (byte) 0x66;\n                                    dstBuffer[dstPos++] = (byte) 0x55;\n                                    dstBuffer[dstPos++] = (byte) 0x54;\n                                    dstBuffer[dstPos++] = (byte) 0x43;\n                                    dstBuffer[dstPos++] = (byte) 0x32;\n                                    dstBuffer[dstPos++] = (byte) 0x31;\n                                }\n                                break;\n\n                            case (byte) 0xf1: //instr\n                                srcPtr++;\n                                count = workRam[srcPtr++];\n                                while (count-- != 0) {\n                                    dstBuffer[dstPos++] = (byte) 0xa8;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = (byte) 0xff;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 3;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = (byte) 0xd0;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = (byte) 0xf3;\n                                    dstBuffer[dstPos++] = 0;\n                                    dstBuffer[dstPos++] = 0;\n                                }\n                                break;\n\n                            default: // block switch\n                                int block = workRam[srcPtr] & 0xff;\n                                srcPtr = 0x8000 + blockSize * block;\n                                break;\n                        }\n                        break;\n\n                    default:\n                        dstBuffer[dstPos++] = workRam[srcPtr++];\n                }\n            }\n        } catch (ArrayIndexOutOfBoundsException e) {\n            return null;\n        }\n    }\n\n    public boolean isValid(int songId) {\n        return unpackSong(songId) != null;\n    }\n\n    static class AddSongException extends Exception {\n        AddSongException(String message) {\n            super(message);\n        }\n    }\n\n    public void addSongFromFile(String filePath, byte[] romImage) throws Exception {\n        final byte songId = getNewSongId();\n        if (songId == -1) {\n            throw new AddSongException(\"Out of song slots!\");\n        }\n\n        try (FileInputStream fileInputStream = new FileInputStream(filePath)) {\n            writeFileNameAndVersion(fileInputStream, songId);\n            copySongToWorkRam(fileInputStream, songId);\n            patchKits(fileInputStream, songId, romImage);\n        } catch (Exception e) {\n            clearSong(songId);\n            throw e;\n        }\n    }\n\n    private void writeFileNameAndVersion(FileInputStream fileInputStream, byte songId) throws IOException {\n        byte[] fileName = new byte[8];\n        int read = fileInputStream.read(fileName);\n        assert(read == fileName.length);\n        byte fileVersion = (byte)fileInputStream.read();\n\n        int fileNamePtr = fileNameStartPtr + songId * fileNameLength;\n        for (int i = 0; i < 8; ++i) {\n            workRam[fileNamePtr++] = fileName[i];\n        }\n\n        int fileVersionPtr = fileVersionStartPtr + songId;\n        workRam[fileVersionPtr] = fileVersion;\n    }\n\n    private void patchKits(FileInputStream fileInputStream,\n                           byte songId,\n                           byte[] romImage) throws IOException, AddSongException {\n        ArrayList<byte[]> lsdSngKits = new ArrayList<>();\n        while (true) {\n            byte[] kit = new byte[0x4000];\n            if (fileInputStream.read(kit) != kit.length) {\n                break;\n            }\n            lsdSngKits.add(kit);\n        }\n\n        if (lsdSngKits.size() == 0) {\n            return;\n        }\n\n        // Check if kits are already in ROM. If so, they should be reused.\n        int[] newKits = new int[lsdSngKits.size()];\n        for (int romKit = 0; romKit < romImage.length / 0x4000; ++romKit) {\n            for (int kit = 0; kit < lsdSngKits.size(); ++kit) {\n                boolean kitsAreEqual = true;\n                for (int i = 0; i < 0x4000; ++i) {\n                    if (lsdSngKits.get(kit)[i] != romImage[romKit * 0x4000 + i]) {\n                        kitsAreEqual = false;\n                        break;\n                    }\n                }\n                if (kitsAreEqual) {\n                    newKits[kit] = romKit;\n                }\n            }\n        }\n\n        addMissingKits(romImage, lsdSngKits, newKits);\n        adjustInstruments(songId, newKits);\n    }\n\n    private List<Integer> instrumentKitLocations(int songId) {\n        int songPos = 0;\n        int blockId = 0;\n        int blockAllocTablePtr = blockAllocTableStartPtr;\n        List<Integer> instrumentKitLocations = new LinkedList<>();\n\n        while (blockId < totalBlockCount()) {\n            if (songId == workRam[blockAllocTablePtr++]) {\n                break;\n            }\n            blockId++;\n        }\n\n        int srcPtr = blockStartPtr + blockSize * blockId;\n        boolean[] isKit = new boolean[64];\n\n        try {\n            while (true) {\n                switch (workRam[srcPtr]) {\n                    case (byte) 0xc0:\n                        srcPtr++;\n                        if (workRam[srcPtr] == (byte) 0xc0) {\n                            srcPtr++;\n                            songPos++;\n                        } else {\n                            srcPtr++;\n                            byte count = workRam[srcPtr++];\n                            while (count-- != 0) {\n                                songPos++;\n                            }\n                        }\n                        break;\n\n                    case (byte) 0xe0:\n                        byte count;\n                        srcPtr++;\n                        switch (workRam[srcPtr]) {\n                            case (byte) 0xe0: // e0\n                                srcPtr++;\n                                songPos++;\n                                break;\n\n                            case (byte) 0xff: // done!\n                                assert(songPos == 0x8000);\n                                return instrumentKitLocations;\n\n                            case (byte) 0xf0: //wave\n                            case (byte) 0xf1: //instr\n                                srcPtr++;\n                                count = workRam[srcPtr++];\n                                while (count-- != 0) {\n                                    songPos += 16;\n                                }\n                                break;\n\n                            default: // block switch\n                                int block = workRam[srcPtr] & 0xff;\n                                srcPtr = 0x8000 + blockSize * block;\n                                break;\n                        }\n                        break;\n\n                    default:\n                        // Regular byte write.\n                        boolean isInstrumentWrite = songPos >= 0x3080 && songPos < 0x3480;\n                        if (isInstrumentWrite) {\n                            int instr = (songPos - 0x3080) / 0x10;\n                            switch (songPos % 16) {\n                                case 0:\n                                    if (workRam[srcPtr] == 2) {\n                                        isKit[instr] = true;\n                                    }\n                                    break;\n                                case 2:\n                                case 9:\n                                    if (isKit[instr]) {\n                                        instrumentKitLocations.add(srcPtr);\n                                    }\n                                    break;\n                            }\n                        }\n                        ++songPos;\n                        ++srcPtr;\n                }\n            }\n        } catch (ArrayIndexOutOfBoundsException e) {\n            return null;\n        }\n    }\n\n    private void adjustInstruments(int songId, int[] newKits) {\n        List<Integer> instrumentKitLocations = instrumentKitLocations(songId);\n        assert(instrumentKitLocations != null);\n\n        TreeSet<Integer> lsdSngKits = new TreeSet<>();\n        for (Integer instrumentKitLocation : instrumentKitLocations) {\n            int kitId = workRam[instrumentKitLocation] & 0x3f;\n            lsdSngKits.add(kitId);\n        }\n\n        HashMap<Integer, Integer> kitMap = new HashMap<>();\n        for (int newKit : newKits) {\n            Integer oldKit = lsdSngKits.pollFirst();\n            if (newKit > 26) {\n                newKit -= 5;\n            }\n            newKit -= 8;\n            kitMap.put(oldKit, newKit);\n        }\n\n        for (Integer instrumentKitLocation : instrumentKitLocations) {\n            int value = workRam[instrumentKitLocation];\n            int newValue = (value & ~0x3f) | kitMap.get(value & 0x3f);\n            workRam[instrumentKitLocation] = (byte)newValue;\n        }\n    }\n\n    private void addMissingKits(byte[] romImage, ArrayList<byte[]> lsdSngKits, int[] newKits) throws AddSongException {\n        for (int kit = 0; kit < newKits.length; ++kit) {\n            if (newKits[kit] != 0) {\n                continue;\n            }\n            int newKit = findFreeKit(romImage);\n            if (newKit == -1) {\n                throw new AddSongException(\"Not enough space for kits! Remove some and try again!\");\n            }\n            newKits[kit] = newKit;\n            // Copy kit.\n            // TODO: this might be a good place to swizzle old kits for improved sound quality. See sbc.java\n            System.arraycopy(lsdSngKits.get(kit), 0, romImage, newKit * 0x4000, 0x4000);\n        }\n    }\n\n    private int findFreeKit(byte[] romImage) {\n        for (int bank = 0; bank < romImage.length / RomUtilities.BANK_SIZE; ++bank) {\n            int offset = bank * RomUtilities.BANK_SIZE;\n            if (romImage[offset] == -1 && romImage[offset + 1] == -1) {\n                return bank;\n            }\n        }\n        return -1;\n    }\n\n    private void copySongToWorkRam(FileInputStream fileInputStream, byte songId) throws IOException, AddSongException {\n        int nextBlockIdPtr = 0;\n        while (true) {\n            int blockId = getBlockIdOfFirstFreeBlock();\n            if (blockId == -1) {\n                throw new AddSongException(\"Out of blocks!\");\n            }\n\n            if (0 != nextBlockIdPtr) {\n                //add one to compensate for unused FAT block\n                workRam[nextBlockIdPtr] = (byte) (blockId + 1);\n            }\n            workRam[blockAllocTableStartPtr + blockId] = songId;\n            int blockPtr = blockStartPtr + blockId * blockSize;\n            for (int i = 0; i < blockSize; ++i) {\n                workRam[blockPtr++] = (byte)fileInputStream.read();\n            }\n            nextBlockIdPtr = getNextBlockIdPtr(blockId);\n            if (nextBlockIdPtr == -1) {\n                return;\n            }\n        }\n    }\n\n    private void clearActiveFileSlot() {\n        workRam[activeFileSlot] = (byte) 0xff;\n    }\n\n    private byte getActiveFileSlot() {\n        return workRam[activeFileSlot];\n    }\n\n    /* Returns address of next block id pointer (E0 XX), if one exists in block.\n     * If there is none, return -1.\n     */\n    private int getNextBlockIdPtr(int block) throws AddSongException {\n        int ramPtr = blockStartPtr + blockSize * block;\n        int byteCounter = 0;\n\n        while (byteCounter < blockSize) {\n            if (workRam[ramPtr] == (byte) 0xc0) {\n                ramPtr++;\n                byteCounter++;\n                if (workRam[ramPtr] != (byte) 0xc0) {\n                    //rle\n                    ramPtr++;\n                    byteCounter++;\n                }\n            } else if (workRam[ramPtr] == (byte) 0xe0) {\n                switch (workRam[ramPtr + 1]) {\n                    case (byte) 0xe0:\n                        ramPtr++;\n                        byteCounter++;\n                        break;\n                    case (byte) 0xff:\n                        return -1;\n                    case (byte) 0xf0: //wave\n                    case (byte) 0xf1: //instr\n                        ramPtr += 2;\n                        byteCounter += 2;\n                        break;\n                    default:\n                        return ramPtr + 1;\n                }\n            }\n            ramPtr++;\n            byteCounter++;\n        }\n        // If the pointer to next block is missing, and this is not the last\n        // block of a song, the song is most likely corrupted.\n        throw new AddSongException(\"Song corrupted!\");\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/laszlosystems/libresample4j/FilterKit.java",
    "content": "/******************************************************************************\n *\n * libresample4j\n * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved.\n *\n * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3,\n * which is in turn based on Julius Smith's Resample 1.7 library.\n *      http://www-ccrma.stanford.edu/~jos/resample/\n *\n * License: LGPL -- see the file LICENSE.txt for more information\n *\n *****************************************************************************/\npackage com.laszlosystems.libresample4j;\n\n/**\n * This file provides Kaiser-windowed low-pass filter support,\n * including a function to create the filter coefficients, and\n * two functions to apply the filter at a particular point.\n * \n * <pre>\n * reference: \"Digital Filters, 2nd edition\"\n *            R.W. Hamming, pp. 178-179\n *\n * Izero() computes the 0th order modified bessel function of the first kind.\n *    (Needed to compute Kaiser window).\n *\n * LpFilter() computes the coeffs of a Kaiser-windowed low pass filter with\n *    the following characteristics:\n *\n *       c[]  = array in which to store computed coeffs\n *       frq  = roll-off frequency of filter\n *       N    = Half the window length in number of coeffs\n *       Beta = parameter of Kaiser window\n *       Num  = number of coeffs before 1/frq\n *\n * Beta trades the rejection of the lowpass filter against the transition\n *    width from passband to stopband.  Larger Beta means a slower\n *    transition and greater stopband rejection.  See Rabiner and Gold\n *    (Theory and Application of DSP) under Kaiser windows for more about\n *    Beta.  The following table from Rabiner and Gold gives some feel\n *    for the effect of Beta:\n *\n * All ripples in dB, width of transition band = D*N where N = window length\n *\n *               BETA    D       PB RIP   SB RIP\n *               2.120   1.50  +-0.27      -30\n *               3.384   2.23    0.0864    -40\n *               4.538   2.93    0.0274    -50\n *               5.658   3.62    0.00868   -60\n *               6.764   4.32    0.00275   -70\n *               7.865   5.0     0.000868  -80\n *               8.960   5.7     0.000275  -90\n *               10.056  6.4     0.000087  -100\n * </pre>\n */\npublic class FilterKit {\n\n    // Max error acceptable in Izero\n    private static final double IzeroEPSILON = 1E-21;\n\n    private static double Izero(double x) {\n        double sum, u, halfx, temp;\n        int n;\n\n        sum = u = n = 1;\n        halfx = x / 2.0;\n        do {\n            temp = halfx / (double) n;\n            n += 1;\n            temp *= temp;\n            u *= temp;\n            sum += u;\n        } while (u >= IzeroEPSILON * sum);\n        return (sum);\n    }\n\n    public static void lrsLpFilter(double c[], int N, double frq, double Beta, int Num) {\n        double IBeta, temp, temp1, inm1;\n        int i;\n\n        // Calculate ideal lowpass filter impulse response coefficients:\n        c[0] = 2.0 * frq;\n        for (i = 1; i < N; i++) {\n            temp = Math.PI * (double) i / (double) Num;\n            c[i] = Math.sin(2.0 * temp * frq) / temp; // Analog sinc function,\n            // cutoff = frq\n        }\n\n        /*\n         * Calculate and Apply Kaiser window to ideal lowpass filter. Note: last\n         * window value is IBeta which is NOT zero. You're supposed to really\n         * truncate the window here, not ramp it to zero. This helps reduce the\n         * first sidelobe.\n         */\n        IBeta = 1.0 / Izero(Beta);\n        inm1 = 1.0 / ((double) (N - 1));\n        for (i = 1; i < N; i++) {\n            temp = (double) i * inm1;\n            temp1 = 1.0 - temp * temp;\n            temp1 = (temp1 < 0 ? 0 : temp1); /*\n                                              * make sure it's not negative\n                                              * since we're taking the square\n                                              * root - this happens on Pentium\n                                              * 4's due to tiny roundoff errors\n                                              */\n            c[i] *= Izero(Beta * Math.sqrt(temp1)) * IBeta;\n        }\n    }\n\n    /**\n     * \n     * @param Imp impulse response\n     * @param ImpD impulse response deltas\n     * @param Nwing length of one wing of filter\n     * @param Interp Interpolate coefs using deltas?\n     * @param Xp_array Current sample array\n     * @param Xp_index Current sample index\n     * @param Ph Phase\n     * @param Inc increment (1 for right wing or -1 for left)\n     * @return\n     */\n    public static float lrsFilterUp(float Imp[], float ImpD[], int Nwing, boolean Interp, float[] Xp_array, int Xp_index, double Ph,\n            int Inc) {\n        double a = 0;\n        float v, t;\n\n        Ph *= Resampler.Npc; // Npc is number of values per 1/delta in impulse\n        // response\n\n        v = 0.0f; // The output value\n\n        float[] Hp_array = Imp;\n        int Hp_index = (int) Ph;\n\n        float[] End_array = Imp;\n        int End_index = Nwing;\n\n        float[] Hdp_array = ImpD;\n        int Hdp_index = (int) Ph;\n\n        if (Interp) {\n            // Hdp = &ImpD[(int)Ph];\n            a = Ph - Math.floor(Ph); /* fractional part of Phase */\n        }\n\n        if (Inc == 1) // If doing right wing...\n        { // ...drop extra coeff, so when Ph is\n            End_index--; // 0.5, we don't do too many mult's\n            if (Ph == 0) // If the phase is zero...\n            { // ...then we've already skipped the\n                Hp_index += Resampler.Npc; // first sample, so we must also\n                Hdp_index += Resampler.Npc; // skip ahead in Imp[] and ImpD[]\n            }\n        }\n\n        if (Interp)\n            while (Hp_index < End_index) {\n                t = Hp_array[Hp_index]; /* Get filter coeff */\n                t += Hdp_array[Hdp_index] * a; /* t is now interp'd filter coeff */\n                Hdp_index += Resampler.Npc; /* Filter coeff differences step */\n                t *= Xp_array[Xp_index]; /* Mult coeff by input sample */\n                v += t; /* The filter output */\n                Hp_index += Resampler.Npc; /* Filter coeff step */\n                Xp_index += Inc; /* Input signal step. NO CHECK ON BOUNDS */\n            }\n        else\n            while (Hp_index < End_index) {\n                t = Hp_array[Hp_index]; /* Get filter coeff */\n                t *= Xp_array[Xp_index]; /* Mult coeff by input sample */\n                v += t; /* The filter output */\n                Hp_index += Resampler.Npc; /* Filter coeff step */\n                Xp_index += Inc; /* Input signal step. NO CHECK ON BOUNDS */\n            }\n\n        return v;\n    }\n\n    /**\n     * \n     * @param Imp impulse response\n     * @param ImpD impulse response deltas\n     * @param Nwing length of one wing of filter\n     * @param Interp Interpolate coefs using deltas?\n     * @param Xp_array Current sample array\n     * @param Xp_index Current sample index\n     * @param Ph Phase\n     * @param Inc increment (1 for right wing or -1 for left)\n     * @param dhb filter sampling period\n     * @return\n     */\n    public static float lrsFilterUD(float Imp[], float ImpD[], int Nwing, boolean Interp, float[] Xp_array, int Xp_index, double Ph,\n            int Inc, double dhb) {\n        float a;\n        float v, t;\n        double Ho;\n\n        v = 0.0f; // The output value\n        Ho = Ph * dhb;\n\n        float[] End_array = Imp;\n        int End_index = Nwing;\n\n        if (Inc == 1) // If doing right wing...\n        { // ...drop extra coeff, so when Ph is\n            End_index--; // 0.5, we don't do too many mult's\n            if (Ph == 0) // If the phase is zero...\n                Ho += dhb; // ...then we've already skipped the\n        } // first sample, so we must also\n        // skip ahead in Imp[] and ImpD[]\n\n        float[] Hp_array = Imp;\n        int Hp_index;\n\n        if (Interp) {\n            float[] Hdp_array = ImpD;\n            int Hdp_index;\n\n            while ((Hp_index = (int) Ho) < End_index) {\n                t = Hp_array[Hp_index]; // Get IR sample\n                Hdp_index = (int) Ho; // get interp bits from diff table\n                a = (float) (Ho - Math.floor(Ho)); // a is logically between 0\n                                                   // and 1\n                t += Hdp_array[Hdp_index] * a; // t is now interp'd filter coeff\n                t *= Xp_array[Xp_index]; // Mult coeff by input sample\n                v += t; // The filter output\n                Ho += dhb; // IR step\n                Xp_index += Inc; // Input signal step. NO CHECK ON BOUNDS\n            }\n        } else {\n            while ((Hp_index = (int) Ho) < End_index) {\n                t = Hp_array[Hp_index]; // Get IR sample\n                t *= Xp_array[Xp_index]; // Mult coeff by input sample\n                v += t; // The filter output\n                Ho += dhb; // IR step\n                Xp_index += Inc; // Input signal step. NO CHECK ON BOUNDS\n            }\n        }\n\n        return v;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/laszlosystems/libresample4j/Resampler.java",
    "content": "/******************************************************************************\n *\n * libresample4j\n * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved.\n *\n * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3,\n * which is in turn based on Julius Smith's Resample 1.7 library.\n *      http://www-ccrma.stanford.edu/~jos/resample/\n *\n * License: LGPL -- see the file LICENSE.txt for more information\n *\n *****************************************************************************/\npackage com.laszlosystems.libresample4j;\n\nimport java.nio.FloatBuffer;\n\npublic class Resampler {\n\n    public static class Result {\n        public final int inputSamplesConsumed;\n        public final int outputSamplesGenerated;\n\n        public Result(int inputSamplesConsumed, int outputSamplesGenerated) {\n            this.inputSamplesConsumed = inputSamplesConsumed;\n            this.outputSamplesGenerated = outputSamplesGenerated;\n        }\n    }\n\n    // number of values per 1/delta in impulse response\n    protected static final int Npc = 4096;\n\n    private final float[] Imp;\n    private final float[] ImpD;\n    private final float LpScl;\n    private final int Nmult;\n    private final int Nwing;\n    private final double minFactor;\n    private final double maxFactor;\n    private final int XSize;\n    private final float[] X;\n    private int Xp; // Current \"now\"-sample pointer for input\n    private int Xread; // Position to put new samples\n    private final int Xoff;\n    private final float[] Y;\n    private int Yp;\n    private double Time;\n\n    public static double RollOff = 0.99; // johan\n    public static double Beta = 6;\n\n    /**\n     * Clone an existing resampling session. Faster than creating one from scratch.\n     *\n     * @param other\n     */\n    public Resampler(Resampler other) {\n        this.Imp = other.Imp.clone();\n        this.ImpD = other.ImpD.clone();\n        this.LpScl = other.LpScl;\n        this.Nmult = other.Nmult;\n        this.Nwing = other.Nwing;\n        this.minFactor = other.minFactor;\n        this.maxFactor = other.maxFactor;\n        this.XSize = other.XSize;\n        this.X = other.X.clone();\n        this.Xp = other.Xp;\n        this.Xread = other.Xread;\n        this.Xoff = other.Xoff;\n        this.Y = other.Y.clone();\n        this.Yp = other.Yp;\n        this.Time = other.Time;\n    }\n\n    /**\n     * Create a new resampling session.\n     *\n     * @param highQuality true for better quality, slower processing time\n     * @param minFactor   lower bound on resampling factor for this session\n     * @param maxFactor   upper bound on resampling factor for this session\n     * @throws IllegalArgumentException if minFactor or maxFactor is not\n     *                                  positive, or if maxFactor is less than minFactor\n     */\n    public Resampler(boolean highQuality, double minFactor, double maxFactor) {\n        if (minFactor <= 0.0 || maxFactor <= 0.0) {\n            throw new IllegalArgumentException(\"minFactor and maxFactor must be positive\");\n        }\n        if (maxFactor < minFactor) {\n            throw new IllegalArgumentException(\"minFactor must be <= maxFactor\");\n        }\n\n        this.minFactor = minFactor;\n        this.maxFactor = maxFactor;\n        this.Nmult = highQuality ? 35 : 11;\n        this.LpScl = 1.0f;\n        this.Nwing = Npc * (this.Nmult - 1) / 2; // # of filter coeffs in right wing\n\n        double[] Imp64 = new double[this.Nwing];\n\n        FilterKit.lrsLpFilter(Imp64, this.Nwing, 0.5 * RollOff, Beta, Npc);\n        this.Imp = new float[this.Nwing];\n        this.ImpD = new float[this.Nwing];\n\n        for (int i = 0; i < this.Nwing; i++) {\n            this.Imp[i] = (float) Imp64[i];\n        }\n\n        // Storing deltas in ImpD makes linear interpolation\n        // of the filter coefficients faster\n        for (int i = 0; i < this.Nwing - 1; i++) {\n            this.ImpD[i] = this.Imp[i + 1] - this.Imp[i];\n        }\n\n        // Last coeff. not interpolated\n        this.ImpD[this.Nwing - 1] = -this.Imp[this.Nwing - 1];\n\n        // Calc reach of LP filter wing (plus some creeping room)\n        int Xoff_min = (int) (((this.Nmult + 1) / 2.0) * Math.max(1.0, 1.0 / minFactor) + 10);\n        int Xoff_max = (int) (((this.Nmult + 1) / 2.0) * Math.max(1.0, 1.0 / maxFactor) + 10);\n        this.Xoff = Math.max(Xoff_min, Xoff_max);\n\n        // Make the inBuffer size at least 4096, but larger if necessary\n        // in order to store the minimum reach of the LP filter and then some.\n        // Then allocate the buffer an extra Xoff larger so that\n        // we can zero-pad up to Xoff zeros at the end when we reach the\n        // end of the input samples.\n        this.XSize = Math.max(2 * this.Xoff + 10, 4096);\n        this.X = new float[this.XSize + this.Xoff];\n        this.Xp = this.Xoff;\n        this.Xread = this.Xoff;\n\n        // Make the outBuffer long enough to hold the entire processed\n        // output of one inBuffer\n        int YSize = (int) (((double) this.XSize) * maxFactor + 2.0);\n        this.Y = new float[YSize];\n        this.Yp = 0;\n\n        this.Time = (double) this.Xoff; // Current-time pointer for converter\n    }\n\n    public int getFilterWidth() {\n        return this.Xoff;\n    }\n\n    /**\n     * Process a batch of samples. There is no guarantee that the input buffer will be drained.\n     *\n     * @param factor    factor at which to resample this batch\n     * @param buffers   sample buffer for producing input and consuming output\n     * @param lastBatch true if this is known to be the last batch of samples\n     * @return true iff resampling is complete (ie. no input samples consumed and no output samples produced)\n     */\n    public boolean process(double factor, SampleBuffers buffers, boolean lastBatch) {\n        if (factor < this.minFactor || factor > this.maxFactor) {\n            throw new IllegalArgumentException(\"factor \" + factor + \" is not between minFactor=\" + minFactor\n                    + \" and maxFactor=\" + maxFactor);\n        }\n\n        int outBufferLen = buffers.getOutputBufferLength();\n        int inBufferLen = buffers.getInputBufferLength();\n\n        float[] Imp = this.Imp;\n        float[] ImpD = this.ImpD;\n        float LpScl = this.LpScl;\n        int Nwing = this.Nwing;\n        boolean interpFilt = false; // TRUE means interpolate filter coeffs\n\n        int inBufferUsed = 0;\n        int outSampleCount = 0;\n\n        // Start by copying any samples still in the Y buffer to the output\n        // buffer\n        if ((this.Yp != 0) && (outBufferLen - outSampleCount) > 0) {\n            int len = Math.min(outBufferLen - outSampleCount, this.Yp);\n\n            buffers.consumeOutput(this.Y, 0, len);\n            //for (int i = 0; i < len; i++) {\n            //    outBuffer[outBufferOffset + outSampleCount + i] = this.Y[i];\n            //}\n\n            outSampleCount += len;\n            for (int i = 0; i < this.Yp - len; i++) {\n                this.Y[i] = this.Y[i + len];\n            }\n            this.Yp -= len;\n        }\n\n        // If there are still output samples left, return now - we need\n        // the full output buffer available to us...\n        if (this.Yp != 0) {\n            return inBufferUsed == 0 && outSampleCount == 0;\n        }\n\n        // Account for increased filter gain when using factors less than 1\n        if (factor < 1) {\n            LpScl = (float) (LpScl * factor);\n        }\n\n        while (true) {\n\n            // This is the maximum number of samples we can process\n            // per loop iteration\n\n            /*\n             * #ifdef DEBUG\n             * printf(\"XSize: %d Xoff: %d Xread: %d Xp: %d lastFlag: %d\\n\",\n             * this.XSize, this.Xoff, this.Xread, this.Xp, lastFlag); #endif\n             */\n\n            // Copy as many samples as we can from the input buffer into X\n            int len = this.XSize - this.Xread;\n\n            if (len >= inBufferLen - inBufferUsed) {\n                len = inBufferLen - inBufferUsed;\n            }\n\n            buffers.produceInput(this.X, this.Xread, len);\n            //for (int i = 0; i < len; i++) {\n            //    this.X[this.Xread + i] = inBuffer[inBufferOffset + inBufferUsed + i];\n            //}\n\n            inBufferUsed += len;\n            this.Xread += len;\n\n            int Nx;\n            if (lastBatch && (inBufferUsed == inBufferLen)) {\n                // If these are the last samples, zero-pad the\n                // end of the input buffer and make sure we process\n                // all the way to the end\n                Nx = this.Xread - this.Xoff;\n                for (int i = 0; i < this.Xoff; i++) {\n                    this.X[this.Xread + i] = 0;\n                }\n            } else {\n                Nx = this.Xread - 2 * this.Xoff;\n            }\n\n            /*\n             * #ifdef DEBUG fprintf(stderr, \"new len=%d Nx=%d\\n\", len, Nx);\n             * #endif\n             */\n\n            if (Nx <= 0) {\n                break;\n            }\n\n            // Resample stuff in input buffer\n            int Nout;\n            if (factor >= 1) { // SrcUp() is faster if we can use it */\n                Nout = lrsSrcUp(this.X, this.Y, factor, /* &this.Time, */Nx, Nwing, LpScl, Imp, ImpD, interpFilt);\n            } else {\n                Nout = lrsSrcUD(this.X, this.Y, factor, /* &this.Time, */Nx, Nwing, LpScl, Imp, ImpD, interpFilt);\n            }\n\n            /*\n             * #ifdef DEBUG\n             * printf(\"Nout: %d\\n\", Nout);\n             * #endif\n             */\n\n            this.Time -= Nx; // Move converter Nx samples back in time\n            this.Xp += Nx; // Advance by number of samples processed\n\n            // Calc time accumulation in Time\n            int Ncreep = (int) (this.Time) - this.Xoff;\n            if (Ncreep != 0) {\n                this.Time -= Ncreep; // Remove time accumulation\n                this.Xp += Ncreep; // and add it to read pointer\n            }\n\n            // Copy part of input signal that must be re-used\n            int Nreuse = this.Xread - (this.Xp - this.Xoff);\n\n            for (int i = 0; i < Nreuse; i++) {\n                this.X[i] = this.X[i + (this.Xp - this.Xoff)];\n            }\n\n            /*\n            #ifdef DEBUG\n            printf(\"New Xread=%d\\n\", Nreuse);\n            #endif */\n\n            this.Xread = Nreuse; // Pos in input buff to read new data into\n            this.Xp = this.Xoff;\n\n            this.Yp = Nout;\n\n            // Copy as many samples as possible to the output buffer\n            if (this.Yp != 0 && (outBufferLen - outSampleCount) > 0) {\n                len = Math.min(outBufferLen - outSampleCount, this.Yp);\n\n                buffers.consumeOutput(this.Y, 0, len);\n                //for (int i = 0; i < len; i++) {\n                //    outBuffer[outBufferOffset + outSampleCount + i] = this.Y[i];\n                //}\n\n                outSampleCount += len;\n                for (int i = 0; i < this.Yp - len; i++) {\n                    this.Y[i] = this.Y[i + len];\n                }\n                this.Yp -= len;\n            }\n\n            // If there are still output samples left, return now,\n            //   since we need the full output buffer available\n            if (this.Yp != 0) {\n                break;\n            }\n        }\n\n        return inBufferUsed == 0 && outSampleCount == 0;\n    }\n\n    /**\n     * Process a batch of samples. Convenience method for when the input and output are both floats.\n     *\n     * @param factor       factor at which to resample this batch\n     * @param inputBuffer  contains input samples in the range -1.0 to 1.0\n     * @param outputBuffer output samples will be deposited here\n     * @param lastBatch    true if this is known to be the last batch of samples\n     * @return true iff resampling is complete (ie. no input samples consumed and no output samples produced)\n     */\n    public boolean process(double factor, final FloatBuffer inputBuffer, boolean lastBatch, final FloatBuffer outputBuffer) {\n        SampleBuffers sampleBuffers = new SampleBuffers() {\n            public int getInputBufferLength() {\n                return inputBuffer.remaining();\n            }\n\n            public int getOutputBufferLength() {\n                return outputBuffer.remaining();\n            }\n\n            public void produceInput(float[] array, int offset, int length) {\n                inputBuffer.get(array, offset, length);\n            }\n\n            public void consumeOutput(float[] array, int offset, int length) {\n                outputBuffer.put(array, offset, length);\n            }\n        };\n        return process(factor, sampleBuffers, lastBatch);\n    }\n\n    /**\n     * Process a batch of samples. Alternative interface if you prefer to work with arrays.\n     *\n     * @param factor         resampling rate for this batch\n     * @param inBuffer       array containing input samples in the range -1.0 to 1.0\n     * @param inBufferOffset offset into inBuffer at which to start processing\n     * @param inBufferLen    number of valid elements in the inputBuffer\n     * @param lastBatch      pass true if this is the last batch of samples\n     * @param outBuffer      array to hold the resampled data\n     * @return the number of samples consumed and generated\n     */\n    public Result process(double factor, float[] inBuffer, int inBufferOffset, int inBufferLen, boolean lastBatch, float[] outBuffer, int outBufferOffset, int outBufferLen) {\n        FloatBuffer inputBuffer = FloatBuffer.wrap(inBuffer, inBufferOffset, inBufferLen);\n        FloatBuffer outputBuffer = FloatBuffer.wrap(outBuffer, outBufferOffset, outBufferLen);\n\n        process(factor, inputBuffer, lastBatch, outputBuffer);\n\n        return new Result(inputBuffer.position() - inBufferOffset, outputBuffer.position() - outBufferOffset);\n    }\n\n\n\n    /*\n     * Sampling rate up-conversion only subroutine; Slightly faster than\n     * down-conversion;\n     */\n    private int lrsSrcUp(float X[], float Y[], double factor, int Nx, int Nwing, float LpScl, float Imp[],\n                         float ImpD[], boolean Interp) {\n\n        float[] Xp_array = X;\n        int Xp_index;\n\n        float[] Yp_array = Y;\n        int Yp_index = 0;\n\n        float v;\n\n        double CurrentTime = this.Time;\n        double dt; // Step through input signal\n        double endTime; // When Time reaches EndTime, return to user\n\n        dt = 1.0 / factor; // Output sampling period\n\n        endTime = CurrentTime + Nx;\n        while (CurrentTime < endTime) {\n            double LeftPhase = CurrentTime - Math.floor(CurrentTime);\n            double RightPhase = 1.0 - LeftPhase;\n\n            Xp_index = (int) CurrentTime; // Ptr to current input sample\n            // Perform left-wing inner product\n            v = FilterKit.lrsFilterUp(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index++, LeftPhase, -1);\n            // Perform right-wing inner product\n            v += FilterKit.lrsFilterUp(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index, RightPhase, 1);\n\n            v *= LpScl; // Normalize for unity filter gain\n\n            Yp_array[Yp_index++] = v; // Deposit output\n            CurrentTime += dt; // Move to next sample by time increment\n        }\n\n        this.Time = CurrentTime;\n        return Yp_index; // Return the number of output samples\n    }\n\n    private int lrsSrcUD(float X[], float Y[], double factor, int Nx, int Nwing, float LpScl, float Imp[],\n                         float ImpD[], boolean Interp) {\n\n        float[] Xp_array = X;\n        int Xp_index;\n\n        float[] Yp_array = Y;\n        int Yp_index = 0;\n\n        float v;\n\n        double CurrentTime = this.Time;\n        double dh; // Step through filter impulse response\n        double dt; // Step through input signal\n        double endTime; // When Time reaches EndTime, return to user\n\n        dt = 1.0 / factor; // Output sampling period\n\n        dh = Math.min(Npc, factor * Npc); // Filter sampling period\n\n        endTime = CurrentTime + Nx;\n        while (CurrentTime < endTime) {\n            double LeftPhase = CurrentTime - Math.floor(CurrentTime);\n            double RightPhase = 1.0 - LeftPhase;\n\n            Xp_index = (int) CurrentTime; // Ptr to current input sample\n            // Perform left-wing inner product\n            v = FilterKit.lrsFilterUD(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index++, LeftPhase, -1, dh);\n            // Perform right-wing inner product\n            v += FilterKit.lrsFilterUD(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index, RightPhase, 1, dh);\n\n            v *= LpScl; // Normalize for unity filter gain\n\n            Yp_array[Yp_index++] = v; // Deposit output\n\n            CurrentTime += dt; // Move to next sample by time increment\n        }\n\n        this.Time = CurrentTime;\n        return Yp_index; // Return the number of output samples\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/laszlosystems/libresample4j/SampleBuffers.java",
    "content": "/******************************************************************************\n *\n * libresample4j\n * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved.\n *\n * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3,\n * which is in turn based on Julius Smith's Resample 1.7 library.\n *      http://www-ccrma.stanford.edu/~jos/resample/\n *\n * License: LGPL -- see the file LICENSE.txt for more information\n *\n *****************************************************************************/\npackage com.laszlosystems.libresample4j;\n\n/**\n * Callback for producing and consuming samples. Enables on-the-fly conversion between sample types\n * (signed 16-bit integers to floats, for example) and/or writing directly to an output stream.\n */\npublic interface SampleBuffers {\n    /**\n     * @return number of input samples available\n     */\n\n    int getInputBufferLength();\n\n    /**\n     * @return number of samples the output buffer has room for\n     */\n    int getOutputBufferLength();\n\n    /**\n     * Copy <code>length</code> samples from the input buffer to the given array, starting at the given offset.\n     * Samples should be in the range -1.0f to 1.0f.\n     *\n     * @param array  array to hold samples from the input buffer\n     * @param offset start writing samples here\n     * @param length write this many samples\n     */\n    void produceInput(float[] array, int offset, int length);\n\n    /**\n     * Copy <code>length</code> samples from the given array to the output buffer, starting at the given offset.\n     *\n     * @param array  array to read from\n     * @param offset start reading samples here\n     * @param length read this many samples\n     */\n    void consumeOutput(float[] array, int offset, int length);\n}\n"
  },
  {
    "path": "src/main/java/fontEditor/.gitignore",
    "content": "/ChangeEventListener.class\n/FontEditor$1.class\n/FontEditor$2.class\n/FontEditor$3.class\n/FontEditor$4.class\n/FontEditor$5.class\n/FontEditor$6.class\n/FontEditor$7.class\n/FontEditor$8.class\n/FontEditor.class\n/FontEditorColorSelector.class\n/FontMap$TileSelectListener.class\n/FontMap.class\n/TileEditor$TileChangedListener.class\n/TileEditor.class\n/FontEditor$9.class\n/ChangeEventMouseSide.class\n/ChangeEventListener$ChangeEventMouseSide.class\n/FontEditorColorSelector$FontEditorColorListener.class\n"
  },
  {
    "path": "src/main/java/fontEditor/ChangeEventListener.java",
    "content": "package fontEditor;\n\nabstract class ChangeEventListener {\n    public enum ChangeEventMouseSide {\n        LEFT,\n        RIGHT\n    }\n\n    public abstract void onChange(int color, ChangeEventMouseSide side);\n}\n\n"
  },
  {
    "path": "src/main/java/fontEditor/FontEditor.java",
    "content": "package fontEditor;\n\nimport java.awt.Dimension;\nimport java.awt.GridBagConstraints;\nimport java.awt.GridBagLayout;\nimport java.awt.event.ActionListener;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\n\nimport javax.imageio.ImageIO;\nimport javax.swing.*;\n\nimport Document.Document;\nimport structures.LSDJFont;\nimport utils.FileDialogLauncher;\nimport utils.FontIO;\nimport utils.RomUtilities;\n\npublic class FontEditor extends JFrame implements FontMap.TileSelectListener, TileEditor.TileChangedListener {\n\n    private static final long serialVersionUID = 5296681614787155252L;\n\n    private final JCheckBox displayGfxCharacters = new JCheckBox();\n    private final FontMap fontMap;\n    private final TileEditor tileEditor;\n\n    private final JComboBox<String> fontSelector;\n\n    private byte[] romImage = null;\n    private int fontOffset = -1;\n    private int selectedFontOffset = -1;\n    private int previousSelectedFont = -1;\n\n    public FontEditor(JFrame parent, Document document) {\n        parent.setEnabled(false);\n\n        setTitle(\"Font Editor\");\n        setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);\n        setBounds(100, 100, 800, 600);\n        setResizable(true);\n        GridBagConstraints constraints = new GridBagConstraints();\n\n        JMenuBar menuBar = new JMenuBar();\n        setJMenuBar(menuBar);\n\n        createFileMenu(menuBar);\n\n        createEditMenu(menuBar);\n\n        GridBagLayout layout = new GridBagLayout();\n        JPanel contentPane = new JPanel();\n        contentPane.setLayout(layout);\n        setContentPane(contentPane);\n\n        tileEditor = new TileEditor();\n        tileEditor.setMinimumSize(new Dimension(240, 240));\n        tileEditor.setPreferredSize(new Dimension(240, 240));\n        tileEditor.setTileChangedListener(this);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.gridx = 3;\n        constraints.gridy = 0;\n        constraints.weightx = 1;\n        constraints.weighty = 1;\n        constraints.gridheight = 6;\n        contentPane.add(tileEditor, constraints);\n\n        fontSelector = new JComboBox<>();\n        fontSelector.setEditable(true);\n        // TODO is there a way to remove the action listener implementation from this class?\n        fontSelector.addItemListener(this::fontSelectorItemChanged);\n        fontSelector.addActionListener(this::fontSelectorAction);\n        constraints.fill = GridBagConstraints.HORIZONTAL;\n        constraints.gridx = 0;\n        constraints.gridy = 1;\n        constraints.weightx = 0;\n        constraints.weighty = 0;\n        constraints.gridheight = 1;\n        constraints.gridwidth = 3;\n        contentPane.add(fontSelector, constraints);\n\n\n        JPanel colorPanel = new JPanel();\n        colorPanel.setLayout(new BoxLayout(colorPanel, BoxLayout.X_AXIS));\n        FontEditorColorSelector colorSelector = new FontEditorColorSelector(colorPanel);\n        colorSelector.addChangeEventListener(new ChangeEventListener() {\n            @Override\n            public void onChange(int color, ChangeEventMouseSide side) {\n                if (side == ChangeEventMouseSide.LEFT)\n                    setColor(color);\n                else\n                    setRightColor(color);\n            }\n        });\n\n        setColor(1);\n        constraints.fill = GridBagConstraints.HORIZONTAL;\n        constraints.gridx = 0;\n        constraints.gridy = 2;\n        constraints.gridwidth = 3;\n        constraints.gridheight = 1;\n        contentPane.add(colorPanel, constraints);\n\n        JPanel shiftButtonPanel = new JPanel();\n        shiftButtonPanel.setLayout(new BoxLayout(shiftButtonPanel, BoxLayout.X_AXIS));\n\n        addImageButtonToPanel(shiftButtonPanel, \"/shift_up.png\", \"Rotate up\", e -> tileEditor.shiftUp(tileEditor.getTile()));\n        addImageButtonToPanel(shiftButtonPanel, \"/shift_down.png\", \"Rotate down\", e -> tileEditor.shiftDown(tileEditor.getTile()));\n        addImageButtonToPanel(shiftButtonPanel, \"/shift_left.png\", \"Rotate left\", e -> tileEditor.shiftLeft(tileEditor.getTile()));\n        addImageButtonToPanel(shiftButtonPanel, \"/shift_right.png\", \"Rotate right\", e -> tileEditor.shiftRight(tileEditor.getTile()));\n\n        constraints.gridx = 0;\n        constraints.gridy = 3;\n        constraints.gridwidth = 3;\n        constraints.gridheight = 1;\n        constraints.fill = GridBagConstraints.NONE;\n        contentPane.add(shiftButtonPanel, constraints);\n\n        displayGfxCharacters.setText(\"Show graphics characters\");\n        displayGfxCharacters.setToolTipText(\"Changes made to graphics characters will apply to all fonts.\");\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.gridx = 0;\n        constraints.gridy = 4;\n        constraints.weightx = 0;\n        constraints.weighty = 0;\n        constraints.gridwidth = 3;\n        constraints.gridheight = 1;\n        contentPane.add(displayGfxCharacters, constraints);\n\n        fontMap = new FontMap();\n        fontMap.setMinimumSize(new Dimension(128, 16 * 8 * 2));\n        fontMap.setPreferredSize(new Dimension(128, 16 * 8 * 2));\n        fontMap.setTileSelectListener(this);\n        constraints.fill = GridBagConstraints.BOTH;\n        constraints.gridx = 0;\n        constraints.gridy = 5;\n        constraints.ipadx = 0;\n        constraints.ipady = 0;\n        constraints.weightx = 0;\n        constraints.weighty = 1;\n        constraints.gridwidth = 3;\n        constraints.gridheight = 1;\n        contentPane.add(fontMap, constraints);\n\n        setMinimumSize(layout.preferredLayoutSize(contentPane));\n        pack();\n\n        setRomImage(document.romImage());\n\n        displayGfxCharacters.addActionListener(e -> {\n            fontMap.setShowGfxCharacters(displayGfxCharacters.isSelected());\n            if (!displayGfxCharacters.isSelected()) {\n                tileSelected(0);\n            }\n        });\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                super.windowClosing(e);\n                document.setRomImage(fontMap.romImage());\n                parent.setEnabled(true);\n            }\n        });\n    }\n\n    private void addImageButtonToPanel(JPanel panel, String imagePath, String altText, ActionListener event) {\n        BufferedImage buttonImage = loadImage(imagePath);\n        JButton button = new JButton();\n        setUpButtonIconOrText(button, buttonImage, altText);\n        button.addActionListener(event);\n        panel.add(button);\n    }\n\n    private void addMenuEntry(JMenu destination, String name, int key, ActionListener event) {\n        JMenuItem newMenuEntry = new JMenuItem(name);\n        newMenuEntry.setMnemonic(key);\n        newMenuEntry.addActionListener(event);\n        destination.add(newMenuEntry);\n    }\n\n    private void createFileMenu(JMenuBar menuBar) {\n        JMenu fileMenu = new JMenu(\"File\");\n        fileMenu.setMnemonic(KeyEvent.VK_F);\n        menuBar.add(fileMenu);\n\n        addMenuEntry(fileMenu, \"Load font...\", KeyEvent.VK_L, e -> loadFont());\n        addMenuEntry(fileMenu, \"Save font...\", KeyEvent.VK_S, e -> saveFont());\n    }\n\n    private void createEditMenu(JMenuBar menuBar) {\n        JMenu editMenu = new JMenu(\"Edit\");\n        editMenu.setMnemonic(KeyEvent.VK_E);\n        menuBar.add(editMenu);\n\n        addMenuEntry(editMenu, \"Copy Tile\", KeyEvent.VK_C, e -> tileEditor.copyTile());\n        addMenuEntry(editMenu, \"Paste Tile\", KeyEvent.VK_V, e -> tileEditor.pasteTile());\n    }\n\n    private BufferedImage loadImage(String iconPath) {\n        try {\n            return javax.imageio.ImageIO.read(getClass().getResource(iconPath));\n        } catch (IOException e1) {\n            e1.printStackTrace();\n        }\n        return null;\n    }\n\n    private void setUpButtonIconOrText(JButton button, BufferedImage image, String altText) {\n        if (image != null)\n            button.setIcon(new ImageIcon(image));\n        else\n            button.setText(altText);\n    }\n\n    private String getFontName(int i) {\n        return RomUtilities.getFontName(romImage, i);\n    }\n\n    private void populateFontSelector() {\n        fontSelector.removeAllItems();\n\n        for (int i = 0; i < LSDJFont.FONT_COUNT; ++i) {\n            char[] name = getFontName(i).toCharArray();\n\n            // Avoids duplicate names by appending incrementing number to duplicates.\n            for (int j = 0; j < i; ++j) {\n                if (!getFontName(j).equals(new String(name))) {\n                    continue;\n                }\n                char lastChar = name[name.length - 1];\n                if (Character.isDigit(lastChar)) {\n                    ++lastChar;\n                } else {\n                    lastChar = '1';\n                }\n                name[name.length - 1] = lastChar;\n                RomUtilities.setFontName(romImage, i, new String(name));\n            }\n\n            fontSelector.addItem(new String(name));\n        }\n    }\n\n    public void setRomImage(byte[] romImage) {\n        this.romImage = romImage;\n        fontMap.setRomImage(romImage);\n        fontMap.setGfxCharOffset(RomUtilities.findGfxFontOffset(romImage));\n        tileEditor.setRomImage(romImage);\n        tileEditor.setGfxDataOffset(RomUtilities.findGfxFontOffset(romImage));\n\n        fontOffset = RomUtilities.findFontOffset(romImage);\n        if (fontOffset == -1) {\n            System.err.println(\"Could not find font offset!\");\n        }\n        int nameOffset = RomUtilities.findFontNameOffset(romImage);\n        if (nameOffset == -1) {\n            System.err.println(\"Could not find font name offset!\");\n        }\n        populateFontSelector();\n    }\n\n    private void fontSelectorItemChanged(java.awt.event.ItemEvent e) {\n        if (e.getStateChange() == java.awt.event.ItemEvent.SELECTED) {\n            if (e.getItemSelectable() == fontSelector) {\n                // Font changed.\n                int index = fontSelector.getSelectedIndex();\n                if (index != -1) {\n                    previousSelectedFont = index;\n                    index = (index + 1) % 3; // Adjusts for fonts being defined in wrong order.\n                    selectedFontOffset = fontOffset + index * LSDJFont.FONT_SIZE + LSDJFont.FONT_HEADER_SIZE;\n                    fontMap.setFontOffset(selectedFontOffset);\n                    tileEditor.setFontOffset(selectedFontOffset);\n                }\n            }\n        }\n    }\n\n    private void setColor(int color) {\n        assert color >= 1 && color <= 3;\n        tileEditor.setColor(color);\n    }\n\n    private void setRightColor(int color) {\n        assert color >= 1 && color <= 3;\n        tileEditor.setRightColor(color);\n    }\n\n    public void tileSelected(int tile) {\n        tileEditor.setTile(tile);\n    }\n\n    public void tileChanged() {\n        fontMap.repaint();\n    }\n\n    private void fontSelectorAction(java.awt.event.ActionEvent e) {\n        switch (e.getActionCommand()) {\n            case \"comboBoxChanged\":\n                if (fontSelector.getSelectedIndex() != -1) {\n                    previousSelectedFont = fontSelector.getSelectedIndex();\n                }\n                break;\n            case \"comboBoxEdited\":\n                String selectedItem = (String) fontSelector.getSelectedItem();\n                if (fontSelector.getSelectedIndex() == -1 && selectedItem != null) {\n                    int index = previousSelectedFont;\n                    RomUtilities.setFontName(romImage, index, selectedItem);\n                    populateFontSelector();\n                    fontSelector.setSelectedIndex(index);\n                    fontSelector.setSelectedIndex(index);\n                } else {\n                    previousSelectedFont = fontSelector.getSelectedIndex();\n                }\n                break;\n        }\n    }\n\n    private void loadFont() {\n        try {\n            File f = FileDialogLauncher.load(this, \"Open Font\", new String[]{ \"png\", \"lsdfnt\" });\n            if (f == null) {\n                return;\n            }\n\n            if (f.getName().endsWith(\"png\")) {\n                importBitmap(f);\n                String fontName = f.getName().replaceFirst(\".png$\", \"\").toUpperCase();\n                RomUtilities.setFontName(romImage, fontSelector.getSelectedIndex(), fontName);\n            } else {\n                String fontName = FontIO.loadFnt(f, romImage, selectedFontOffset);\n                tileEditor.generateShadedAndInvertedTiles();\n                RomUtilities.setFontName(romImage, fontSelector.getSelectedIndex(), fontName);\n                tileEditor.tileChanged();\n                tileChanged();\n            }\n            // Refresh the name list.\n            int previousIndex = fontSelector.getSelectedIndex();\n            populateFontSelector();\n            fontSelector.setSelectedIndex(previousIndex);\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(this, \"Couldn't open fnt file.\\n\" + e.getMessage());\n            e.printStackTrace();\n        }\n    }\n\n    private void importBitmap(File bitmap) {\n        try {\n            BufferedImage image = ImageIO.read(bitmap);\n            if (image.getWidth() != 64 && image.getHeight() != 72) {\n                JOptionPane.showMessageDialog(this,\n                        \"Make sure your picture has the right dimensions (64 * 72 pixels).\");\n                return;\n            }\n            tileEditor.readImage(bitmap.getName(), image);\n            tileEditor.tileChanged();\n            tileChanged();\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(this, \"Couldn't load the given picture.\\n\" + e.getMessage());\n            e.printStackTrace();\n        }\n    }\n\n    private void saveFont() {\n        File f = FileDialogLauncher.save(this, \"Export Font\", \"png\");\n        if (f == null) {\n            return;\n        }\n        BufferedImage image = tileEditor.createImage(displayGfxCharacters.isSelected());\n        try {\n            ImageIO.write(image, \"PNG\", f);\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(this, \"Couldn't export the font map.\\n\" + e.getMessage());\n            e.printStackTrace();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fontEditor/FontEditorColorSelector.java",
    "content": "package fontEditor;\n\nimport java.awt.Color;\nimport java.awt.Dimension;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.util.ArrayList;\n\nimport javax.swing.JPanel;\nimport javax.swing.SwingUtilities;\n\nimport fontEditor.ChangeEventListener.ChangeEventMouseSide;\n\nclass FontEditorColorSelector {\n\n    private final JPanel foregroundColorIndicator;\n    private final JPanel backgroundColorIndicator;\n\n    private final ArrayList<ChangeEventListener> listeners;\n\n    private static class FontEditorColorListener implements MouseListener {\n        final FontEditorColorSelector selector;\n        final int color;\n\n        FontEditorColorListener(FontEditorColorSelector selector, int color) {\n            this.selector = selector;\n            this.color = color;\n        }\n\n        @Override\n        public void mouseClicked(MouseEvent e) {\n        }\n\n        @Override\n        public void mouseEntered(MouseEvent e) {\n        }\n\n        @Override\n        public void mouseExited(MouseEvent e) {\n        }\n\n        @Override\n        public void mousePressed(MouseEvent e) {\n            if (SwingUtilities.isRightMouseButton(e))\n                selector.sendEvent(color, ChangeEventMouseSide.RIGHT);\n            if (SwingUtilities.isLeftMouseButton(e))\n                selector.sendEvent(color, ChangeEventMouseSide.LEFT);\n\n        }\n\n        @Override\n        public void mouseReleased(MouseEvent e) {\n        }\n    }\n\n    public FontEditorColorSelector(JPanel buttonPanel) {\n        JPanel darkButton = new JPanel();\n        darkButton.setBackground(Color.BLACK);\n        darkButton.setForeground(Color.BLACK);\n        darkButton.addMouseListener(new FontEditorColorListener(this, 3));\n\n        JPanel mediumButton = new JPanel();\n        mediumButton.setBackground(Color.LIGHT_GRAY);\n        mediumButton.addMouseListener(new FontEditorColorListener(this, 2));\n\n        JPanel lightButton = new JPanel();\n        lightButton.setBackground(Color.WHITE);\n        lightButton.addMouseListener(new FontEditorColorListener(this, 1));\n\n        listeners = new ArrayList<>();\n\n        JPanel indicatorContainer = new JPanel();\n        indicatorContainer.setLayout(null);\n\n        foregroundColorIndicator = new JPanel();\n        foregroundColorIndicator.setBackground(Color.WHITE);\n        foregroundColorIndicator.setForeground(Color.WHITE);\n        foregroundColorIndicator.setPreferredSize(new Dimension(24, 24));\n        foregroundColorIndicator.setBounds(8, 8, 24, 24);\n        indicatorContainer.add(foregroundColorIndicator);\n\n        backgroundColorIndicator = new JPanel();\n        backgroundColorIndicator.setBackground(Color.BLACK);\n        backgroundColorIndicator.setForeground(Color.BLACK);\n        backgroundColorIndicator.setPreferredSize(new Dimension(24, 24));\n        backgroundColorIndicator.setBounds(0, 0, 24, 24);\n        indicatorContainer.add(backgroundColorIndicator);\n\n\n        buttonPanel.add(indicatorContainer);\n        buttonPanel.add(darkButton);\n        buttonPanel.add(mediumButton);\n        buttonPanel.add(lightButton);\n\n        buttonPanel.setPreferredSize(new Dimension(200, 32));\n\n    }\n\n    private void sendEvent(int color, ChangeEventMouseSide side) {\n        Color buttonColor = Color.RED;\n        switch (color) {\n            case 1:\n                buttonColor = Color.WHITE;\n                break;\n            case 2:\n                buttonColor = Color.LIGHT_GRAY;\n                break;\n            case 3:\n                buttonColor = Color.BLACK;\n                break;\n        }\n\n        for (ChangeEventListener listener : listeners) {\n            listener.onChange(color, side);\n        }\n        if (side == ChangeEventMouseSide.LEFT)\n            foregroundColorIndicator.setBackground(buttonColor);\n        else\n            backgroundColorIndicator.setBackground(buttonColor);\n    }\n\n    void addChangeEventListener(ChangeEventListener changeEventListener) {\n        listeners.add(changeEventListener);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/fontEditor/FontMap.java",
    "content": "package fontEditor;\n\nimport java.awt.*;\n\nimport javax.swing.JPanel;\n\nimport structures.LSDJFont;\n\npublic class FontMap extends JPanel implements java.awt.event.MouseListener {\n    private static final long serialVersionUID = -7745908775698863845L;\n    private byte[] romImage = null;\n    private int fontOffset = -1;\n    private int gfxCharOffset = -1;\n    private int tileZoom = 1;\n    private int displayTileSize = 8;\n    private boolean showGfxCharacters = false;\n\n    public interface TileSelectListener {\n        void tileSelected(int tile);\n    }\n\n    private TileSelectListener tileSelectedListener = null;\n\n    FontMap() {\n        addMouseListener(this);\n    }\n\n    void setShowGfxCharacters(boolean show) {\n        showGfxCharacters = show;\n        repaint();\n    }\n\n    void setTileSelectListener(TileSelectListener l) {\n        tileSelectedListener = l;\n    }\n\n    int getCurrentUnscaledMapHeight () {\n        return showGfxCharacters ? LSDJFont.GFX_FONT_MAP_HEIGHT : LSDJFont.FONT_MAP_HEIGHT;\n    }\n\n    int getCurrentTileNumber () {\n        return showGfxCharacters ? LSDJFont.GFX_TILE_COUNT + LSDJFont.TILE_COUNT : LSDJFont.TILE_COUNT;\n    }\n\n    public void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        int currentHeight = getCurrentUnscaledMapHeight();\n\n        int widthScale = getWidth() / LSDJFont.FONT_MAP_WIDTH;\n        int heightScale = getHeight() / currentHeight;\n        tileZoom = Math.min(widthScale, heightScale);\n        tileZoom = Math.max(tileZoom, 1);\n        int offsetX = (getWidth() - LSDJFont.FONT_MAP_WIDTH * tileZoom) / 2;\n        int offsetY = (getHeight() - currentHeight * tileZoom) / 2;\n        setPreferredSize(new Dimension(LSDJFont.FONT_MAP_WIDTH * tileZoom, currentHeight * tileZoom));\n\n        for (int tile = 0; tile < tileCount(); ++tile) {\n            paintTile(g, tile, offsetX, offsetY);\n        }\n    }\n\n    private int tileCount() {\n        return showGfxCharacters\n                ? LSDJFont.GFX_TILE_COUNT + LSDJFont.TILE_COUNT\n                : LSDJFont.TILE_COUNT;\n    }\n\n    private void switchColor(Graphics g, int c) {\n        switch (c & 3) {\n            case 0:\n                g.setColor(Color.white);\n                break;\n            case 1:\n                g.setColor(Color.lightGray);\n                break;\n            case 2:\n                g.setColor(Color.darkGray);  // Not used.\n                break;\n            case 3:\n                g.setColor(Color.black);\n                break;\n        }\n    }\n\n    private int getColor(int tile, int x, int y) {\n        int tileOffset;\n        if (tile < LSDJFont.TILE_COUNT) {\n            tileOffset = fontOffset;\n        } else {\n            tileOffset = gfxCharOffset;\n            tile -= LSDJFont.TILE_COUNT;\n        }\n        tileOffset += tile * 16 + y * 2;\n        int xMask = 7 - x;\n        int value = (romImage[tileOffset] >> xMask) & 1;\n        value |= ((romImage[tileOffset + 1] >> xMask) & 1) << 1;\n        return value;\n    }\n\n    private void paintTile(Graphics g, int tile, int offsetX, int offsetY) {\n        displayTileSize = 8 * tileZoom;\n        int x = (tile % 8) * displayTileSize;\n        int y = (tile / 8) * displayTileSize;\n\n        for (int row = 0; row < 8; ++row) {\n            for (int column = 0; column < 8; ++column) {\n                switchColor(g, getColor(tile, column, row));\n                g.fillRect(offsetX + x + column * tileZoom, offsetY + y + row * tileZoom, tileZoom, tileZoom);\n            }\n        }\n        if (tileZoom > 1) {\n            g.setColor(new Color(0.f,0.f,0.4f,0.6f));\n            g.drawRect(offsetX + x, offsetY + y, tileZoom*8, tileZoom*8);\n        }\n    }\n\n    void setRomImage(byte[] romImage) {\n        this.romImage = romImage;\n    }\n\n    public byte[] romImage() {\n        return romImage;\n    }\n\n    void setGfxCharOffset(int gfxCharOffset) {\n        this.gfxCharOffset = gfxCharOffset;\n    }\n\n    void setFontOffset(int fontOffset) {\n        this.fontOffset = fontOffset;\n        repaint();\n    }\n\n    public void mouseEntered(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseExited(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseReleased(java.awt.event.MouseEvent e) {\n    }\n\n    public void mousePressed(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseClicked(java.awt.event.MouseEvent e) {\n        int offsetX = (getWidth() - LSDJFont.FONT_MAP_WIDTH * tileZoom) / 2;\n        int offsetY = (getHeight() - getCurrentUnscaledMapHeight() * tileZoom) / 2;\n\n        int realX = e.getX() - offsetX;\n        int realY = e.getY() - offsetY;\n\n        if (realX < 0 || realY < 0  || realX > LSDJFont.FONT_MAP_WIDTH * tileZoom || realY > getCurrentUnscaledMapHeight() * tileZoom)\n            return;\n\n        int tile = (realY / displayTileSize) * LSDJFont.FONT_NUM_TILES_X +\n                realX / displayTileSize;\n        if (tile < 0 || tile >= getCurrentTileNumber())\n            return;\n\n        if (tileSelectedListener != null) {\n            tileSelectedListener.tileSelected(tile);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fontEditor/TileEditor.java",
    "content": "package fontEditor;\n\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\n\nimport javax.swing.JPanel;\nimport javax.swing.SwingUtilities;\n\nimport structures.LSDJFont;\n\nclass TileEditor extends JPanel implements java.awt.event.MouseListener, java.awt.event.MouseMotionListener {\n\n    private static final long serialVersionUID = 4048727729255703626L;\n\n    public interface TileChangedListener {\n        void tileChanged();\n    }\n\n    private final LSDJFont font;\n    private int selectedTile = 0;\n    private int color = 3;\n    private int rightColor = 3;\n\n    private int[][] clipboard = null;\n\n    private TileChangedListener tileChangedListener;\n\n    TileEditor() {\n        font = new LSDJFont();\n        addMouseListener(this);\n        addMouseMotionListener(this);\n    }\n\n    void setRomImage(byte[] romImage) {\n        font.setRomImage(romImage);\n    }\n\n    void setFontOffset(int offset) {\n        font.setDataOffset(offset);\n        repaint();\n    }\n\n    void setGfxDataOffset(int offset) {\n        font.setGfxDataOffset(offset);\n        repaint();\n    }\n\n    void setTile(int tile) {\n        selectedTile = tile;\n        repaint();\n    }\n\n    int getTile() {\n        return selectedTile;\n    }\n\n    void shiftUp(int tile) {\n        font.rotateTileUp(tile);\n        tileChanged();\n    }\n\n    void shiftDown(int tile) {\n        font.rotateTileDown(tile);\n        tileChanged();\n    }\n\n    void shiftRight(int tile) {\n        font.rotateTileRight(tile);\n        tileChanged();\n    }\n\n    void shiftLeft(int tile) {\n        font.rotateTileLeft(tile);\n        tileChanged();\n    }\n\n    private int getColor(int tile, int x, int y) {\n        return font.getTilePixel(tile, x, y);\n    }\n\n    private void switchColor(Graphics g, int c) {\n        switch (c & 3) {\n            case 0:\n                g.setColor(Color.white);\n                break;\n            case 1:\n                g.setColor(Color.lightGray);\n                break;\n            case 2:\n                g.setColor(Color.darkGray); // Not used.\n                break;\n            case 3:\n                g.setColor(Color.black);\n                break;\n        }\n    }\n\n    private int getMinimumDimension() {\n        return getWidth() < getHeight() ? getWidth() : getHeight();\n    }\n\n    private void paintGrid(Graphics g) {\n        g.setColor(java.awt.Color.gray);\n        int minimumDimension = getMinimumDimension();\n        int offsetX = (getWidth() - minimumDimension) / 2;\n        int offsetY = (getHeight() - minimumDimension) / 2;\n        int dx = minimumDimension / 8;\n        int minimumDimensionSquare = (minimumDimension / 8) * 8;\n        for (int x = dx + offsetX; x < minimumDimensionSquare + offsetX; x += dx) {\n            g.drawLine(x, offsetY, x, minimumDimensionSquare + offsetY);\n        }\n\n        int dy = minimumDimension / 8;\n        for (int y = dy + offsetY; y < minimumDimensionSquare + offsetY; y += dy) {\n            g.drawLine(offsetX, y, offsetX + minimumDimensionSquare, y);\n        }\n    }\n\n    public void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        int minimumDimension = getMinimumDimension();\n        int offsetX = (getWidth() - minimumDimension) / 2;\n        int offsetY = (getHeight() - minimumDimension) / 2;\n        for (int x = 0; x < 8; ++x) {\n            for (int y = 0; y < 8; ++y) {\n                int color = getColor(selectedTile, x, y);\n                switchColor(g, color);\n                int pixelWidth = minimumDimension / 8;\n                int pixelHeight = minimumDimension / 8;\n                g.fillRect(offsetX + x * pixelWidth, offsetY + y * pixelHeight, pixelWidth, pixelHeight);\n            }\n        }\n\n        paintGrid(g);\n    }\n\n    private void doMousePaint(java.awt.event.MouseEvent e) {\n        int minimumDimension = getMinimumDimension();\n        int offsetX = (getWidth() - minimumDimension) / 2;\n        int offsetY = (getHeight() - minimumDimension) / 2;\n\n        int x = ((e.getX() - offsetX) * 8) / minimumDimension;\n        int y = ((e.getY() - offsetY) * 8) / minimumDimension;\n        if (x < 0 || x >= 8 || y < 0 || y >= 8)\n            return;\n        if (SwingUtilities.isLeftMouseButton(e))\n            setColor(x, y, color);\n        else if (SwingUtilities.isRightMouseButton(e))\n            setColor(x, y, rightColor);\n        tileChanged();\n    }\n\n    private void setColor(int x, int y, int color) {\n        font.setTilePixel(selectedTile, x, y, color);\n    }\n\n    public void mouseEntered(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseExited(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseReleased(java.awt.event.MouseEvent e) {\n    }\n\n    public void mousePressed(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseClicked(java.awt.event.MouseEvent e) {\n        doMousePaint(e);\n    }\n\n    public void mouseMoved(java.awt.event.MouseEvent e) {\n    }\n\n    public void mouseDragged(java.awt.event.MouseEvent e) {\n        doMousePaint(e);\n    }\n\n    void setColor(int color) {\n        assert color >= 1 && color <= 3;\n        this.color = color;\n    }\n\n    void setRightColor(int color) {\n        assert color >= 1 && color <= 3;\n        this.rightColor = color;\n    }\n\n    void setTileChangedListener(TileChangedListener l) {\n        tileChangedListener = l;\n    }\n\n    void copyTile() {\n        if (clipboard == null) {\n            clipboard = new int[8][8];\n        }\n        for (int x = 0; x < 8; ++x) {\n            for (int y = 0; y < 8; ++y) {\n                clipboard[x][y] = getColor(selectedTile, x, y);\n            }\n        }\n    }\n\n    void generateShadedAndInvertedTiles() {\n        font.generateShadedAndInvertedTiles();\n    }\n\n    void pasteTile() {\n        if (clipboard == null) {\n            return;\n        }\n        for (int x = 0; x < 8; ++x) {\n            for (int y = 0; y < 8; ++y) {\n                int c = clipboard[x][y];\n                if (c < 3) {\n                    ++c; // Adjusts from Game Boy Color to editor color.\n                }\n                setColor(x, y, c);\n            }\n        }\n        tileChanged();\n    }\n\n    void tileChanged() {\n        repaint();\n        generateShadedAndInvertedTiles();\n        tileChangedListener.tileChanged();\n    }\n\n    void readImage(String name, BufferedImage image) {\n        font.loadImageData(name, image);\n\n    }\n\n    BufferedImage createImage(boolean includeGfxCharacters) {\n        return font.saveDataToImage(includeGfxCharacters);\n    }\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/KitEditor.java",
    "content": "package kitEditor;\n\nimport Document.Document;\nimport com.laszlosystems.libresample4j.Resampler;\nimport net.miginfocom.swing.MigLayout;\nimport utils.*;\n\nimport javax.sound.sampled.UnsupportedAudioFileException;\nimport javax.swing.*;\nimport javax.swing.event.MenuEvent;\nimport javax.swing.event.MenuListener;\nimport java.awt.*;\nimport java.awt.event.*;\nimport java.io.*;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Locale;\n\npublic class KitEditor extends JFrame implements SamplePicker.Listener {\n    private final Document document;\n\n    public interface Listener {\n        void saveRom();\n    }\n    Listener listener;\n\n    private static final long serialVersionUID = -3993608561466542956L;\n    private JPanel contentPane;\n    private final JComboBox<String> bankBox = new JComboBox<>();\n    private final SamplePicker samplePicker = new SamplePicker();\n    private final String SETTINGS_FILE_EXTENSION = \".settings\";\n\n    private static final int MAX_SAMPLES = 15;\n    private static final int MAX_SAMPLE_SPACE = 0x3fa0;\n\n    private final java.awt.event.ActionListener bankBoxListener = e -> bankBox_actionPerformed();\n\n    private static final byte KIT_VERSION_1 = 1;\n\n    private byte[] romImage;\n\n    private final Sample[][] samples = new Sample[RomUtilities.BANK_COUNT][MAX_SAMPLES];\n    \n    private final Sample[] clipboard = new Sample[MAX_SAMPLES];\n\n    private final JButton previousBankButton = new JButton(\"<\");\n    private final JButton nextBankButton = new JButton(\">\");\n\n    private final JButton loadKitButton = new JButton();\n    private final JButton saveKitButton = new JButton();\n    private final JButton saveRomButton = new JButton();\n    private final JButton exportSampleButton = new JButton();\n    private final JButton clearKitButton = new JButton(\"Clear kit\");\n    private final JButton renameKitButton = new JButton();\n    private final JTextField kitNameTextField = new JTextField();\n    private final JButton reloadSampleButton = new JButton(\"Reload sample\");\n    private final JButton addSampleButton = new JButton(\"Add sample\");\n    private final JLabel kitSizeLabel = new JLabel();\n    private final SampleView sampleView = new SampleView();\n    private final JSpinner volumeSpinner = new JSpinner();\n    private final JSpinner pitchSpinner = new JSpinner();\n    private final JSpinner trimSpinner = new JSpinner();\n    private final JCheckBoxMenuItem halfSpeed = new JCheckBoxMenuItem(\"Half-speed\");\n    private final JCheckBox dither = new JCheckBox(\"Dither\", true);\n    private final JMenuItem useGameBoyAdvancePolarity = new JCheckBoxMenuItem(\"Invert Polarity for GBA\");\n\n    public KitEditor(JFrame parent, Document document, Listener listener) {\n        parent.setEnabled(false);\n\n        romImage = document.romImage();\n        this.listener = listener;\n        this.document = document;\n        enableEvents(AWTEvent.WINDOW_EVENT_MASK);\n        jbInit();\n        addMenu();\n        setListeners();\n        setTitle(\"Kit Editor\");\n        createSamplesFromRom();\n        updateRomView();\n\n        saveRomButton.addActionListener(e -> {\n            document.setRomImage(romImage);\n            listener.saveRom();\n            romImage = document.romImage();\n            updateButtonStates();\n        });\n\n        KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();\n        PadKeyHandler padKeyHandler = new PadKeyHandler();\n        keyboardFocusManager.addKeyEventPostProcessor(padKeyHandler);\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                super.windowClosing(e);\n                keyboardFocusManager.removeKeyEventPostProcessor(padKeyHandler);\n                document.setRomImage(romImage);\n                parent.setEnabled(true);\n            }\n        });\n\n        add(contentPane);\n\n        pack();\n\n        samplePicker.grabFocus();\n\n        setResizable(false);\n        setVisible(true);\n    }\n\n    private void addEnterHandler(JSpinner spinner) {\n        ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField().addKeyListener(new KeyAdapter() {\n            @Override\n            public void keyPressed(KeyEvent e) {\n                if (e.getKeyCode() == KeyEvent.VK_ENTER) {\n                    requestFocus();\n                }\n            }\n        });\n    }\n\n    private void setListeners() {\n        bankBox.addActionListener(bankBoxListener);\n        samplePicker.addListSelectionListener(this);\n        volumeSpinner.addChangeListener(e -> onSpinnerChanged());\n        addEnterHandler(volumeSpinner);\n        pitchSpinner.addChangeListener(e -> onSpinnerChanged());\n        addEnterHandler(pitchSpinner);\n        trimSpinner.addChangeListener(e -> onSpinnerChanged());\n        halfSpeed.addActionListener(e -> onHalfSpeedChanged());\n        dither.addActionListener(e -> onSpinnerChanged());\n\n        Action previousBankAction = new AbstractAction(\"previous bank\") {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                bankBox.setSelectedIndex(bankBox.getSelectedIndex() - 1);\n            }\n        };\n        previousBankAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(\"F1\"));\n        previousBankButton.addActionListener(previousBankAction);\n        previousBankButton.getActionMap().put(\"previousBankAction\", previousBankAction);\n        previousBankButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(\n                (KeyStroke) previousBankAction.getValue(Action.ACCELERATOR_KEY), \"previousBankAction\");\n        previousBankButton.setToolTipText(\"F1\");\n\n        Action nextBankAction = new AbstractAction(\"next bank\") {\n            @Override\n            public void actionPerformed(ActionEvent e) {\n                bankBox.setSelectedIndex(bankBox.getSelectedIndex() + 1);\n            }\n        };\n        nextBankAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(\"F2\"));\n        nextBankButton.addActionListener(nextBankAction);\n        nextBankButton.getActionMap().put(\"nextBankAction\", nextBankAction);\n        nextBankButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(\n                (KeyStroke) nextBankAction.getValue(Action.ACCELERATOR_KEY), \"nextBankAction\");\n        nextBankButton.setToolTipText(\"F2\");\n\n        loadKitButton.addActionListener(e -> loadKit());\n        saveKitButton.addActionListener(e -> saveKit());\n        clearKitButton.addActionListener(e -> createKit());\n        renameKitButton.addActionListener(e -> renameKit(kitNameTextField.getText()));\n        kitNameTextField.addActionListener(e -> {\n            renameKit(kitNameTextField.getText());\n            requestFocus();\n        });\n\n        exportSampleButton.addActionListener(e -> exportSample());\n        addSampleButton.addActionListener(e -> addSample());\n        reloadSampleButton.addActionListener(e -> reloadSample());\n        reloadSampleButton.setEnabled(false);\n    }\n\n    private void onHalfSpeedChanged() {\n        // Update trim accordingly.\n        for (Sample s : samples[selectedBank]) {\n            if (s != null) {\n                if (halfSpeed.isSelected()) {\n                    s.setTrim((int)Math.ceil(s.getTrim() / 2.0)); // Round upwards.\n                } else {\n                    s.setTrim(s.getTrim() * 2);\n                }\n            }\n        }\n        reloadAllSamples();\n    }\n    \n    private void reloadAllSamples() {\n        int index = samplePicker.getSelectedIndex();\n        try {\n            for (Sample s : samples[selectedBank]) {\n                if (s != null) {\n                    s.reload(halfSpeed.isSelected());\n                }\n            }\n        } catch (Exception e) {\n            showFileErrorMessage(e);\n            e.printStackTrace();\n        }\n        compileKit();\n        updateRomView();\n        samplePicker.setSelectedIndex(index);\n    }\n\n    private void reloadSample() {\n        int index = samplePicker.getSelectedIndex();\n        Sample sample = samples[selectedBank][index];\n        if (sample != null) {\n            try {\n                sample.reload(halfSpeed.isSelected());\n            } catch (Exception e) {\n                showFileErrorMessage(e);\n                e.printStackTrace();\n            }\n        }\n        compileKit();\n        updateRomView();\n        playSample();\n        samplePicker.setSelectedIndex(index);\n    }\n\n    static boolean handlingSpinnerChange = false;\n    private void onSpinnerChanged() {\n        if (handlingSpinnerChange) {\n            return;\n        }\n        int index = samplePicker.getSelectedIndex();\n        if (index < 0) {\n            return;\n        }\n        Sample sample = samples[selectedBank][index];\n        if (sample == null || !sample.canAdjustVolume()) {\n            return;\n        }\n        handlingSpinnerChange = true;\n        sample.setVolumeDb((int)volumeSpinner.getValue());\n        if ((int)pitchSpinner.getValue() != sample.getPitchSemitones()) {\n            sample.setPitchSemitones((int) pitchSpinner.getValue());\n            try {\n                sample.reload(halfSpeed.isSelected());\n            } catch (UnsupportedAudioFileException | IOException e) {\n                e.printStackTrace();\n                showFileErrorMessage(e);\n            }\n        }\n        sample.setTrim((int)trimSpinner.getValue());\n        sample.setDither(dither.isSelected());\n        sample.processSamples();\n        compileKit();\n        if (bytesFree() < 0) {\n            // Sample did not fit, likely due to increased volume. Trim to fit.\n            int fixedTrim = sample.getTrim() - bytesFree() / 16;\n            assert fixedTrim >= 0;\n            trimSpinner.setValue(fixedTrim);\n            sample.setTrim(fixedTrim);\n            sample.processSamples();\n            compileKit();\n        }\n        // Makes sure trim is in valid range.\n        int maxTrim = maxTrim(sample);\n        if ((int)trimSpinner.getValue() > maxTrim) {\n            trimSpinner.setValue(maxTrim);\n            sample.setTrim(maxTrim);\n            sample.processSamples();\n            compileKit();\n        }\n        samplePicker.setSelectedIndex(index);\n        dither.setSelected(sample.getDither());\n        Sound.stopAll();\n        playSample();\n        handlingSpinnerChange = false;\n        updateKitSizeLabel();\n    }\n\n    private double ask(String message, double value) {\n        try {\n            value = Double.parseDouble(JOptionPane.showInputDialog(message, value));\n        } catch (NullPointerException | NumberFormatException ignored) {\n        }\n        return value;\n    }\n\n    private void addMenu() {\n        JMenuBar menuBar = new JMenuBar();\n        JMenu preferences = new JMenu(\"Preferences\");\n        preferences.add(halfSpeed);\n        useGameBoyAdvancePolarity.addActionListener(e -> reloadAllSamples());\n        preferences.add(useGameBoyAdvancePolarity);\n        JMenuItem lpFilter = new JMenuItem(\"Low-Pass Filter...\");\n        lpFilter.addActionListener(e -> {\n            Resampler.Beta = ask(\"Kaiser Window Beta\", Resampler.Beta);\n            Resampler.RollOff = ask( \"Kaiser Window Roll-Off (0-1, 1=Nyquist)\", Resampler.RollOff);\n        });\n        preferences.add(lpFilter);\n\n        JMenu edit = new JMenu(\"Edit\");\n        JMenuItem pasteSampleMenuItem = new JMenuItem(\"Paste\");\n        pasteSampleMenuItem.addActionListener(e -> pasteSample());\n        JMenuItem trimAll = new JMenuItem(\"Trim all samples to fit\");\n        trimAll.addActionListener(e -> trimAllSamples());\n\n        edit.add(pasteSampleMenuItem);\n        edit.add(trimAll);\n\n        edit.addMenuListener(new MenuListener() {\n            @Override\n            public void menuSelected(MenuEvent e) {\n                trimAll.setEnabled(firstFreeSampleSlot() != 0 &&\n                        firstFreeSampleSlot() != 1);\n                pasteSampleMenuItem.setEnabled(firstFreeSampleSlot() >= 0 &&\n                        firstFreeSampleSlot() < 15 && clipboard[0] != null);\n            }\n\n            @Override\n            public void menuDeselected(MenuEvent e) { }\n\n            @Override\n            public void menuCanceled(MenuEvent e) { }\n        });\n\n        menuBar.add(preferences);        \n        menuBar.add(edit);\n        \n        setJMenuBar(menuBar);\n    }\n\n    private void jbInit() {\n        contentPane = new JPanel();\n        contentPane.setLayout(new MigLayout());\n\n        createFileDrop();\n\n        samplePicker.setBorder(BorderFactory.createEtchedBorder());\n\n        JPanel kitContainer = new JPanel();\n        kitContainer.setLayout(new MigLayout(\"\", \"[grow,fill]\", \"\"));\n        kitContainer.add(bankBox, \"grow,split 3\");\n        kitContainer.add(previousBankButton);\n        kitContainer.add(nextBankButton, \"wrap\");\n        kitContainer.add(samplePicker, \"grow,wrap\");\n        kitContainer.add(kitSizeLabel, \"grow, split 2\");\n        kitContainer.add(saveRomButton, \"grow\");\n\n        loadKitButton.setText(\"Load kit\");\n        saveKitButton.setText(\"Save kit\");\n        saveRomButton.setText(\"Save ROM\");\n\n        kitNameTextField.setBorder(BorderFactory.createLoweredBevelBorder());\n\n        renameKitButton.setText(\"Rename kit\");\n\n        exportSampleButton.setEnabled(false);\n        exportSampleButton.setText(\"Export sample\");\n\n        addSampleButton.setEnabled(false);\n        volumeSpinner.setEnabled(false);\n        pitchSpinner.setEnabled(false);\n        trimSpinner.setEnabled(false);\n        dither.setEnabled(false);\n\n        contentPane.add(kitContainer, \"grow, cell 0 0, spany\");\n        contentPane.add(loadKitButton, \"grow, wrap\");\n        contentPane.add(saveKitButton, \"grow, wrap, sg button\");\n        contentPane.add(clearKitButton, \"gaptop 5, grow, wrap, sg button\");\n        contentPane.add(kitNameTextField, \"grow, wmin 60, split 2\");\n        contentPane.add(renameKitButton, \"wrap 10\");\n\n        contentPane.add(exportSampleButton, \"grow, wrap, sg button\");\n        contentPane.add(addSampleButton, \"grow, span 2, wrap, sg button\");\n        contentPane.add(reloadSampleButton, \"grow, span 2, wrap, sg button\");\n        contentPane.add(dither, \"grow, wrap\");\n        contentPane.add(new JLabel(\"Volume (dB):\"), \"split 2\");\n        contentPane.add(volumeSpinner, \"grow, wrap\");\n        contentPane.add(new JLabel(\"Pitch (semitone):\"), \"split 2\");\n        contentPane.add(pitchSpinner, \"grow, wrap\");\n        contentPane.add(new JLabel(\"Trim (frames):\"), \"split 2\");\n        contentPane.add(trimSpinner, \"grow, wrap\");\n        contentPane.add(sampleView, \"grow, span 2, wmin 10, hmin 64\");\n        sampleView.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mousePressed(MouseEvent e) {\n                playSample();\n            }\n        });\n\n        dither.setToolTipText(\"Removes 4-bit distortion by adding noise.\");\n        halfSpeed.setToolTipText(\"Half sample-rate, double kit length. Use with SPEED 0.5X kit setting.\");\n    }\n\n    private void createFileDrop() {\n        new FileDrop(contentPane, files -> {\n            for (File file : files) {\n                String fileName = file.getName().toLowerCase();\n                if (fileName.endsWith(\".wav\")) {\n                    if (romImage == null) {\n                        JOptionPane.showMessageDialog(contentPane,\n                                \"Open .gb file before adding samples.\",\n                                \"Can't add sample!\",\n                                JOptionPane.ERROR_MESSAGE);\n                        continue;\n                    }\n                    addSample(file);\n                } else if (fileName.endsWith(\".kit\")) {\n                    if (romImage == null) {\n                        JOptionPane.showMessageDialog(contentPane,\n                                \"Open .gb file before adding samples.\",\n                                \"Can't add sample!\",\n                                JOptionPane.ERROR_MESSAGE);\n                        continue;\n                    }\n                    loadKit(file);\n                } else {\n                    JOptionPane.showMessageDialog(contentPane,\n                            \"Unknown file type!\",\n                            \"File error\",\n                            JOptionPane.ERROR_MESSAGE);\n                    return;\n                }\n            }\n        });\n    }\n\n    private static void unSwizzle(byte[] packedNibbles) {\n        assert(packedNibbles.length % 16 == 0);\n\n        // Rotates the wave frame left and inverts the signal. Mirrors sbc.java.\n        byte[] tmpBuf = new byte[packedNibbles.length * 2];\n        for (int i = 0; i < packedNibbles.length; i += 16) {\n            for (int j = 0; j < 16; ++j) {\n                int b = packedNibbles[i + j];\n                int dst = ((2 * j + 31) % 32) + (i * 2);\n                tmpBuf[dst] = (byte) ((0xf0 - (b & 0xf0)) >> 4);\n                dst = 2 * (i + j);\n                tmpBuf[dst] = (byte) ((0xf - (b & 0xf)) << 4);\n            }\n        }\n\n        for (int i = 0; i < packedNibbles.length; ++i) {\n            packedNibbles[i] = (byte)(tmpBuf[i * 2] | tmpBuf[i * 2 + 1]);\n        }\n    }\n\n    private byte[] getNibbles(int index) {\n        if (index < 0) {\n            return null;\n        }\n        final int romBank = getSelectedROMBank();\n        int offset = romBank * RomUtilities.BANK_SIZE + index * 2;\n        int start = (0xff & romImage[offset]) | ((0xff & romImage[offset + 1]) << 8);\n        int stop = (0xff & romImage[offset + 2]) | ((0xff & romImage[offset + 3]) << 8);\n        if (stop <= start) {\n            return null;\n        }\n        byte[] arr = new byte[stop - start];\n        System.arraycopy(romImage,\n                (romBank - 1) * RomUtilities.BANK_SIZE + start,\n                arr,\n                0,\n                stop - start);\n        if (isBankSwizzled()) {\n            unSwizzle(arr);\n        }\n        return arr;\n    }\n\n    private boolean isBankSwizzled() {\n        return bankVersion() == KIT_VERSION_1;\n    }\n\n    private byte bankVersion() {\n        // This version field is new for lsdpatcher 1.11.1 and lsdj 9.2.2.\n        // Old kits may have preexisting data here: it cannot be\n        // assumed that all old kits are set to version 0.\n        return romImage[versionOffset()];\n    }\n\n    private int versionOffset() {\n        return getROMOffsetForSelectedBank() + 0x5f;\n    }\n\n    @Override\n    public void playSample() {\n        int index = samplePicker.getSelectedIndex();\n        Sample sample = samples[selectedBank][index];\n        if (sample == null) {\n            return;\n        }\n        dither.setSelected(sample.getDither());\n        byte[] nibbles = getNibbles(index);\n        if (nibbles == null) {\n            return;\n        }\n        try {\n            Sound.play(nibbles, halfSpeed.isSelected());\n        } catch (Exception e) {\n            JOptionPane.showMessageDialog(this, e.getMessage(), \"Audio error\",\n                    JOptionPane.INFORMATION_MESSAGE);\n            e.printStackTrace();\n        }\n        updateSampleView();\n    }\n\n    private void updateSampleView() {\n        int sampleIndex = samplePicker.getSelectedIndex();\n        byte[] nibbles = getNibbles(sampleIndex);\n        if (nibbles == null) {\n            return;\n        }\n        float duration = samples[selectedBank][sampleIndex].lengthInSamples();\n        duration /= halfSpeed.isSelected() ? 5734 : 11468;\n        sampleView.setBufferContent(nibbles, duration);\n        sampleView.repaint();\n    }\n\n    private boolean isKitBank(int a_bank) {\n        int l_offset = (a_bank) * RomUtilities.BANK_SIZE;\n        byte l_char_1 = romImage[l_offset++];\n        byte l_char_2 = romImage[l_offset];\n        return (l_char_1 == 0x60 && l_char_2 == 0x40);\n    }\n\n    private boolean isUninitializedBank(int a_bank) {\n        int l_offset = (a_bank) * RomUtilities.BANK_SIZE;\n        byte l_char_1 = romImage[l_offset++];\n        byte l_char_2 = romImage[l_offset];\n        return (l_char_1 == -1 && l_char_2 == -1);\n    }\n\n    private String getKitName(int a_bank) {\n        if (isUninitializedBank(a_bank)) {\n            return \"Empty\";\n        }\n\n        byte[] buf = new byte[6];\n        int offset = (a_bank) * RomUtilities.BANK_SIZE + 0x52;\n        for (int i = 0; i < 6; i++) {\n            buf[i] = romImage[offset++];\n        }\n        return new String(buf);\n    }\n\n    private void updateRomView() {\n        int tmp = bankBox.getSelectedIndex();\n        bankBox.removeActionListener(bankBoxListener);\n        bankBox.removeAllItems();\n\n        int l_ui_index = 0;\n        for (int bankNo = 0; bankNo < RomUtilities.BANK_COUNT; bankNo++) {\n            if (isKitBank(bankNo) || isUninitializedBank(bankNo)) {\n                bankBox.addItem(Integer.toHexString(++l_ui_index).toUpperCase() + \". \" + getKitName(bankNo));\n            }\n        }\n        bankBox.setSelectedIndex(tmp == -1 ? 0 : tmp);\n        bankBox.addActionListener(bankBoxListener);\n        updateBankView();\n        updateSampleView();\n    }\n\n    private int selectedBank;\n\n    private int getSelectedUiBank() {\n        if (bankBox.getSelectedIndex() > -1) {\n            selectedBank = bankBox.getSelectedIndex();\n        }\n        return selectedBank;\n    }\n\n    private int getSelectedROMBank() {\n        int l_rom_bank = 0;\n        int l_ui_bank = 0;\n\n        for (; ; ) {\n            if (isKitBank(l_rom_bank) || isUninitializedBank(l_rom_bank)) {\n                if (getSelectedUiBank() == l_ui_bank) {\n                    return l_rom_bank;\n                }\n                l_ui_bank++;\n            }\n            l_rom_bank++;\n        }\n\n    }\n\n    private int getROMOffsetForSelectedBank() {\n        return getSelectedROMBank() * RomUtilities.BANK_SIZE;\n    }\n\n    private void updateBankView() {\n        byte[] buf = new byte[3];\n        String[] s = new String[15];\n\n        int bankOffset = getROMOffsetForSelectedBank();\n\n        //update names\n        int offset = bankOffset + 0x22;\n        for (int instrNo = 0; instrNo < MAX_SAMPLES; instrNo++) {\n            boolean isNull = false;\n            for (int i = 0; i < 3; i++) {\n                buf[i] = romImage[offset++];\n                if (isNull) {\n                    buf[i] = '-';\n                } else {\n                    if (buf[i] == 0) {\n                        buf[i] = '-';\n                        isNull = true;\n                    }\n                }\n            }\n            s[instrNo] = new String(buf);\n        }\n        samplePicker.setListData(s);\n\n        updateKitSizeLabel();\n        addSampleButton.setEnabled(firstFreeSampleSlot() != -1);\n\n        updateButtonStates();\n    }\n\n    boolean kitTooBig() {\n        return totalSampleSizeInBytes() > MAX_SAMPLE_SPACE;\n    }\n\n    private int bytesFree() {\n        return MAX_SAMPLE_SPACE - totalSampleSizeInBytes();\n    }\n\n    private void updateKitSizeLabel() {\n        int sampleRate = halfSpeed.isSelected() ? 5734 : 11468;\n        float timeFree = (bytesFree() * 2.f) / sampleRate;\n        kitSizeLabel.setText(String.format(Locale.US, \"%.3f seconds free\", timeFree));\n        kitSizeLabel.setForeground(timeFree < 0 ? Color.red : Color.black);\n    }\n\n    private boolean isEmpty(Sample[] samples) {\n        for (Sample s : samples) {\n            if (s != null) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    private void bankBox_actionPerformed() {\n        requestFocus();\n        if (selectedBank == bankBox.getSelectedIndex()) {\n            return;\n        }\n        selectedBank = bankBox.getSelectedIndex();\n        if (isUninitializedBank(getSelectedROMBank())) {\n            createKit();\n        }\n        if (isEmpty(samples[selectedBank])) {\n            flushWavFiles();\n            createSamplesFromRom();\n        }\n        updateBankView();\n        samplePicker.setSelectedIndex(-1);\n        selectionChanged();\n    }\n\n    private String getRomSampleName(int index) {\n        int offset = getROMOffsetForSelectedBank() + 0x22 + index * 3;\n        String name = \"\";\n        name += (char) romImage[offset++];\n        name += (char) romImage[offset++];\n        name += (char) romImage[offset];\n        return name;\n    }\n\n    private void createSamplesFromRom() {\n        for (int sampleIt = 0; sampleIt < MAX_SAMPLES; ++sampleIt) {\n            if (samples[selectedBank][sampleIt] != null) {\n                continue;\n            }\n            byte[] nibbles = getNibbles(sampleIt);\n\n            if (nibbles != null) {\n                String name = getRomSampleName(sampleIt);\n                samples[selectedBank][sampleIt] = Sample.createFromNibbles(nibbles, name);\n            } else {\n                samples[selectedBank][sampleIt] = null;\n            }\n        }\n    }\n\n    private void showFileErrorMessage(Exception e) {\n        e.printStackTrace();\n        JOptionPane.showMessageDialog(this,\n                e.getMessage(),\n                \"File error\",\n                JOptionPane.ERROR_MESSAGE);\n    }\n\n    private void saveKit() {\n        File f = FileDialogLauncher.save(this, \"Save Kit\", \"kit\");\n        if (f == null) {\n            return;\n        }\n        byte[] buf = new byte[RomUtilities.BANK_SIZE];\n        int offset = getROMOffsetForSelectedBank();\n        try {\n            RandomAccessFile bankFile = new RandomAccessFile(f, \"rw\");\n            for (int i = 0; i < buf.length; i++) {\n                buf[i] = romImage[offset++];\n            }\n            bankFile.write(buf);\n            bankFile.close();\n\n            saveKitSettings(f);\n        } catch (IOException e) {\n            showFileErrorMessage(e);\n        }\n        updateRomView();\n    }\n\n    private void saveKitSettings(File kitFile) throws IOException {\n        File kitSettingsFile = new File(kitFile.getAbsolutePath() + SETTINGS_FILE_EXTENSION);\n        boolean hasFiles = false;\n        for (Sample s : samples[selectedBank]) {\n            if (s != null && s.getFile() != null) {\n                hasFiles = true;\n                break;\n            }\n        }\n        if (!hasFiles) {\n            return;\n        }\n        try (FileWriter fileWriter = new FileWriter(kitSettingsFile)) {\n            for (Sample s : samples[selectedBank]) {\n                if (s == null || s.getFile() == null) {\n                    fileWriter.write(\"\\n\");\n                    continue;\n                }\n                fileWriter.write(s.getFile().getAbsolutePath() +\n                        \"|\" + s.getVolumeDb() +\n                        \"|\" + s.getTrim() +\n                        \"|\" + s.getPitchSemitones() +\n                        \"|\" + s.getDither() + \"\\n\");\n            }\n        }\n    }\n\n    private void loadKit() {\n        File kitFile = FileDialogLauncher.load(this, \"Load Sample Kit\", \"kit\");\n        if (kitFile != null) {\n            loadKit(kitFile);\n        }\n    }\n\n    private void loadKit(File kitFile) {\n        createKit();\n        renameKit(kitFile.getName());\n        byte[] buf = new byte[RomUtilities.BANK_SIZE];\n        int offset = getROMOffsetForSelectedBank();\n        try {\n            RandomAccessFile bankFile = new RandomAccessFile(kitFile, \"r\");\n            bankFile.readFully(buf);\n            if (buf[0] != 0x60 || buf[1] != 0x40) {\n                JOptionPane.showMessageDialog(this,\n                        \"Malformed kit file!\",\n                        \"File error\",\n                        JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n            for (byte aBuf : buf) {\n                romImage[offset++] = aBuf;\n            }\n            bankFile.close();\n            flushWavFiles();\n            createSamplesFromRom();\n            loadKitSettings(kitFile);\n            updateBankView();\n        } catch (IOException | UnsupportedAudioFileException e) {\n            showFileErrorMessage(e);\n        }\n        updateRomView();\n    }\n\n    private void loadKitSettings(File kitFile) throws IOException, UnsupportedAudioFileException {\n        File kitSettingsFile = new File(kitFile.getAbsolutePath() + SETTINGS_FILE_EXTENSION);\n        if (!kitSettingsFile.exists()) {\n            return;\n        }\n        try (BufferedReader fileReader = new BufferedReader(new FileReader(kitSettingsFile))) {\n            for (int i = 0; i < MAX_SAMPLES; ++i) {\n                String line = fileReader.readLine();\n                if (line.isEmpty()) {\n                    continue;\n                }\n                String[] chunks = line.split(\"\\\\|\");\n                File sampleFile = new File(chunks[0]);\n                if (!sampleFile.exists()) {\n                    continue;\n                }\n                int volume = Integer.parseInt(chunks[1]);\n                int trim = 0;\n                if (chunks.length > 2) {\n                    trim = Integer.parseInt(chunks[2]);\n                }\n                int pitch = 0;\n                if (chunks.length > 3) {\n                    pitch = Integer.parseInt(chunks[3]);\n                }\n                boolean dither = true;\n                if (chunks.length > 4) {\n                    dither = chunks[4].equals(\"true\");\n                }\n                samples[selectedBank][i] = Sample.createFromWav(\n                        sampleFile,\n                        dither,\n                        halfSpeed.isSelected(),\n                        volume,\n                        trim,\n                        pitch);\n            }\n        }\n    }\n\n    private String dropExtension(File f) {\n        String ext = null;\n        String s = f.getName();\n        int i = s.lastIndexOf('.');\n\n        if (i > 0 && i < s.length() - 1) {\n            ext = s.substring(0, i);\n        }\n        return ext;\n    }\n\n    private void createKit() {\n        //clear all bank\n        int offset = getROMOffsetForSelectedBank();\n        int max_offset = getROMOffsetForSelectedBank() + RomUtilities.BANK_SIZE;\n        while (offset < max_offset) {\n            romImage[offset++] = -1;\n        }\n\n        //clear kit name\n        offset = getROMOffsetForSelectedBank() + 0x52;\n        for (int i = 0; i < 6; i++) {\n            romImage[offset++] = ' ';\n        }\n\n        //clear instrument names\n        offset = getROMOffsetForSelectedBank() + 0x22;\n        for (int i = 0; i < 15; i++) {\n            romImage[offset++] = 0;\n            romImage[offset++] = '-';\n            romImage[offset++] = '-';\n        }\n\n        flushWavFiles();\n        updateRomView();\n    }\n\n    private void flushWavFiles() {\n        for (int i = 0; i < MAX_SAMPLES; ++i) {\n            samples[selectedBank][i] = null;\n        }\n    }\n\n    private void renameKit(String s) {\n        s = s.toUpperCase();\n        int offset = getROMOffsetForSelectedBank() + 0x52;\n        for (int i = 0; i < 6; i++) {\n            if (i < s.length()) {\n                romImage[offset++] = (byte) s.charAt(i);\n            } else {\n                romImage[offset++] = ' ';\n            }\n        }\n        compileKit();\n        updateRomView();\n    }\n\n    private int firstFreeSampleSlot() {\n        for (int sampleIt = 0; sampleIt < MAX_SAMPLES; ++sampleIt) {\n            if (samples[selectedBank][sampleIt] == null) {\n                return sampleIt;\n            }\n        }\n        return -1;\n    }\n\n    private void addSample(File wavFile) {\n        if (firstFreeSampleSlot() == -1) {\n            JOptionPane.showMessageDialog(contentPane,\n                    \"Can't add sample, kit is full!\",\n                    \"Kit full\",\n                    JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n        String sampleName = (dropExtension(wavFile).toUpperCase() + \"---\").substring(0,3);\n        Sample sample;\n        try {\n            sample = Sample.createFromWav(wavFile, dither.isSelected(), halfSpeed.isSelected(), 0, 0, 0);\n            int bytesFreeAfterAdd = MAX_SAMPLE_SPACE - totalSampleSizeInBytes() - sample.lengthInBytes();\n            if (bytesFreeAfterAdd < 0) {\n                int trim = -bytesFreeAfterAdd / 16;\n                sample.setTrim(trim);\n                sample.reload(halfSpeed.isSelected());\n                JOptionPane.showMessageDialog(this,\n                        \"Trimmed sample to fit.\",\n                        \"Kit full!\",\n                        JOptionPane.INFORMATION_MESSAGE);\n            }\n        } catch (Exception e) {\n            showFileErrorMessage(e);\n            return;\n        }\n\n        int index = firstFreeSampleSlot();\n        assert index != -1;\n        samples[selectedBank][index] = sample;\n        renameSample(index, sampleName);\n        compileKit();\n        updateRomView();\n        samplePicker.setSelectedIndex(index);\n        playSample();\n        updateButtonStates();\n    }\n\n    private void renameSample(int sampleIndex, String sampleName) {\n        samples[selectedBank][sampleIndex].setName(sampleName);\n        int offset = getROMOffsetForSelectedBank() + 0x22 + sampleIndex * 3;\n        for (int i = 0; i < 3; ++i) {\n            if (i < sampleName.length()) {\n                romImage[offset] = (byte) sampleName.toUpperCase().charAt(i);\n            } else {\n                romImage[offset] = '-';\n            }\n            offset++;\n        }\n\n    }\n\n    private void addSample() {\n        File f = FileDialogLauncher.load(this, \"Load Sample\", \"wav\");\n        if (f != null) {\n            addSample(f);\n        }\n    }\n\n    private void compileKit() {\n        if (kitTooBig()) {\n            return;\n        }\n\n        byte[] newSamples = new byte[RomUtilities.BANK_SIZE];\n        int[] lengths = new int[15];\n        sbc.compile(newSamples, samples[selectedBank], lengths, useGameBoyAdvancePolarity.isSelected());\n\n        //copy sampledata to ROM image\n        int offset = getROMOffsetForSelectedBank() + 0x60;\n        int offset2 = 0x60;\n        System.arraycopy(newSamples, offset2, romImage, offset, RomUtilities.BANK_SIZE - offset2);\n\n        //update samplelength info in rom image\n        int bankOffset = 0x4060;\n        offset = getROMOffsetForSelectedBank();\n        romImage[offset++] = 0x60;\n        romImage[offset++] = 0x40;\n        for (int i = 0; i < 15; i++) {\n            bankOffset += lengths[i];\n            if (lengths[i] != 0) {\n                romImage[offset++] = (byte) (bankOffset & 0xff);\n                romImage[offset++] = (byte) (bankOffset >> 8);\n            } else {\n                romImage[offset++] = 0;\n                romImage[offset++] = 0;\n            }\n        }\n\n        // Resets forced loop data.\n        romImage[getROMOffsetForSelectedBank() + 0x5c] = 0;\n        romImage[getROMOffsetForSelectedBank() + 0x5d] = 0;\n\n        // Version number.\n        romImage[versionOffset()] = KIT_VERSION_1;\n    }\n\n    private int totalSampleSizeInBytes() {\n        int total = 0;\n        for (Sample s : samples[selectedBank]) {\n            total += s == null ? 0 : s.lengthInBytes();\n        }\n        return total;\n    }\n\n    private void dropSample() {\n        ArrayList<Integer> indices = samplePicker.getSelectedIndices();\n        for (int indexIt = 0; indexIt < indices.size(); ++indexIt) {\n            // Assumes that indices are sorted...\n            int index = indices.get(indexIt);\n\n            // Moves up samples.\n            if (14 - index >= 0) {\n                System.arraycopy(samples[selectedBank],\n                        index + 1,\n                        samples[selectedBank],\n                        index,\n                        14 - index);\n            }\n            samples[selectedBank][14] = null;\n\n            // Moves up instr names.\n            int offset = getROMOffsetForSelectedBank() + 0x22 + index * 3;\n            int i;\n            for (i = offset; i < getROMOffsetForSelectedBank() + 0x22 + 14 * 3; i += 3) {\n                romImage[i] = romImage[i + 3];\n                romImage[i + 1] = romImage[i + 4];\n                romImage[i + 2] = romImage[i + 5];\n            }\n            romImage[i] = 0;\n            romImage[i + 1] = '-';\n            romImage[i + 2] = '-';\n\n            // Adjusts indices.\n            for (int indexIt2 = indexIt + 1; indexIt2 < indices.size(); ++indexIt2) {\n                indices.set(indexIt2, indices.get(indexIt2) - 1);\n            }\n        }\n        compileKit();\n        updateBankView();\n    }\n\n    private void exportSample() {\n        File f = FileDialogLauncher.save(this, \"Save Sample\", \"wav\");\n        if (f != null) {\n            try {\n                WaveFile.write(samples[selectedBank][samplePicker.getSelectedIndex()].workSampleData(), f);\n            } catch (IOException e) {\n                showFileErrorMessage(e);\n            }\n        }\n    }\n\n    @Override\n    public void selectionChanged() {\n        updateButtonStates();\n    }\n\n    private int maxTrim(Sample sample) {\n        int maxTrim = sample.untrimmedLengthInSamples() / 32 - 1;\n        return Math.max(0, maxTrim);\n    }\n\n    private int modelMaxTrim;\n    private void updateTrimModel(Sample sample) {\n        if (sample == null) {\n            return;\n        }\n        int maxTrim = maxTrim(sample);\n        if (modelMaxTrim == maxTrim) {\n            return;\n        }\n        int trim = sample.getTrim();\n        assert trim >= 0;\n        trim = Math.min(trim, maxTrim);\n        trimSpinner.setModel(new SpinnerNumberModel(trim, 0, maxTrim, 1));\n        addEnterHandler(trimSpinner);\n        modelMaxTrim = maxTrim;\n    }\n\n    private void updateButtonStates() {\n        int index = samplePicker.getSelectedIndex();\n        exportSampleButton.setEnabled(getNibbles(index) != null);\n        Sample sample = index >= 0 ? samples[selectedBank][index] : null;\n        boolean enableVolume = sample != null && sample.canAdjustVolume();\n        handlingSpinnerChange = true;\n        dither.setEnabled(enableVolume);\n        volumeSpinner.setEnabled(enableVolume);\n        volumeSpinner.setValue(enableVolume ? sample.getVolumeDb() : 0);\n        pitchSpinner.setEnabled(enableVolume);\n        pitchSpinner.setValue(enableVolume ? sample.getPitchSemitones() : 0);\n        trimSpinner.setEnabled(enableVolume && sample.untrimmedLengthInSamples() > 32);\n        if (trimSpinner.isEnabled()) {\n            updateTrimModel(sample);\n            trimSpinner.setValue(enableVolume ? sample.getTrim() : 0);\n        }\n        handlingSpinnerChange = false;\n        reloadSampleButton.setEnabled(enableVolume);\n        addSampleButton.setEnabled(totalSampleSizeInBytes() < MAX_SAMPLE_SPACE);\n        saveRomButton.setEnabled(!kitTooBig() &&\n                (!Arrays.equals(document.romImage(), romImage) ||\n                        document.isRomDirty()));\n\n        previousBankButton.setEnabled(selectedBank > 0);\n        nextBankButton.setEnabled(selectedBank + 1 < bankBox.getItemCount());\n    }\n\n    private void trimAllSamples() {\n        int numberOfSamples = firstFreeSampleSlot() > 1 ? firstFreeSampleSlot() : MAX_SAMPLES;\n        int samplesToTrim = 0, usedBytes = 0;\n        for (int sampleIt = 0; sampleIt < numberOfSamples; ++sampleIt) {\n            Sample sample = samples[selectedBank][sampleIt];\n            if (sample != null) {\n                samplesToTrim = sample.canAdjustVolume() ? samplesToTrim + 1 : samplesToTrim;\n                usedBytes = sample.canAdjustVolume() ? usedBytes : usedBytes + sample.untrimmedLengthInBytes();\n            }\n        }\n        if (samplesToTrim < 1) {\n            JOptionPane.showMessageDialog(this,\n                \"No samples to trim\",\n                \"No samples to trim\",\n                JOptionPane.INFORMATION_MESSAGE);\n            return;\n        }\n        int equalSampleLength = (MAX_SAMPLE_SPACE - usedBytes)  / samplesToTrim;\n        // first calculate if any samples are shorter than equal length and add\n        int addLength = 0, sampleCount = 0;\n        for (int sampleIt = 0; sampleIt < MAX_SAMPLES; ++sampleIt) {\n            Sample sample = samples[selectedBank][sampleIt];\n            if (sample != null && sample.canAdjustVolume()) {\n                if (sample.untrimmedLengthInBytes() < equalSampleLength) {\n                    addLength += equalSampleLength - sample.untrimmedLengthInBytes();\n                    ++sampleCount;\n                }\n            }\n        }\n        int maxEqualSampleLength = samplesToTrim > sampleCount\n            ? equalSampleLength + addLength / (samplesToTrim - sampleCount)\n            : equalSampleLength;\n        for (int sampleIt = 0; sampleIt < MAX_SAMPLES; ++sampleIt) {\n            Sample sample = samples[selectedBank][sampleIt];\n            if (sample != null && sample.canAdjustVolume()) {\n                int trim = sample.untrimmedLengthInBytes() > maxEqualSampleLength \n                    ? (int)Math.round((sample.untrimmedLengthInBytes() - maxEqualSampleLength) / 16.0)\n                    : 0;\n                sample.setTrim(trim);\n            }\n        }\n        samplePicker.setSelectedIndex(numberOfSamples - 1);\n        Sample sample = samples[selectedBank][numberOfSamples - 1];\n        if (bytesFree() < 0) {\n            // adjust last sample to account for rounding\n            int fixedTrim = sample.getTrim() - bytesFree() / 16;\n            assert fixedTrim > 0;\n            trimSpinner.setValue(fixedTrim);\n            sample.setTrim(fixedTrim);\n            sample.processSamples();\n            compileKit();\n        }\n        // Makes sure trim is in valid range.\n        int maxTrim = maxTrim(sample);\n        if ((int)trimSpinner.getValue() > maxTrim) {\n            trimSpinner.setValue(maxTrim);\n            sample.setTrim(maxTrim);\n            sample.processSamples();\n            compileKit();\n        }\n        updateButtonStates();\n        reloadAllSamples();\n        updateRomView();\n        JOptionPane.showMessageDialog(this,\n                \"Trimmed all samples to fit.\",\n                \"Done\",\n                JOptionPane.INFORMATION_MESSAGE);\n    }\n\n    private void duplicateSample(Sample sample) {\n        if (sample == null) {\n          return;\n        }\n        int sampleSlot = firstFreeSampleSlot();\n        Sample dupeSample;\n        if (sampleSlot != -1) {\n            // copy sample data\n            try {\n            dupeSample = Sample.dupeSample(sample);\n            dupeSample.reload(halfSpeed.isSelected());\n            } catch (Exception e) {\n                showFileErrorMessage(e);\n                return;\n            }\n            samples[selectedBank][sampleSlot] = dupeSample;\n            renameSample(sampleSlot, sample.getName());\n            if (bytesFree() < 0 && sample.canAdjustVolume()) {\n                int fixedTrim = dupeSample.getTrim() - bytesFree() / 16;\n                assert fixedTrim > 0;\n                dupeSample.setTrim(fixedTrim);\n            } else if (bytesFree() < 0) {\n                samples[selectedBank][sampleSlot] = null;\n                JOptionPane.showMessageDialog(contentPane,\n                        \"Can't add sample, kit is full!\",\n                        \"Kit full\",\n                        JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n        } else {\n            JOptionPane.showMessageDialog(contentPane,\n                    \"Can't add sample, kit is full!\",\n                    \"Kit full\",\n                    JOptionPane.ERROR_MESSAGE);\n            return;\n        } \n        reloadAllSamples();\n        updateRomView();\n        samplePicker.setSelectedIndex(sampleSlot);\n        playSample();\n        updateButtonStates();\n    }\n    \n    private void pasteSample() {\n        for (Sample sample : clipboard) {\n            if (sample != null) {\n                duplicateSample(sample);\n            }\n        }\n    }\n\n    @Override\n    public void dupeSample() {\n        int index = samplePicker.getSelectedIndex();\n        duplicateSample(samples[selectedBank][index]);\n    }\n\n    @Override\n    public void deleteSample() {\n        dropSample();\n    }\n\n    @Override\n    public void replaceSample() {\n        File wavFile = FileDialogLauncher.load(this, \"Load Sample\", \"wav\");\n        if (wavFile == null) {\n            return;\n        }\n        try {\n            final int index = samplePicker.getSelectedIndex();\n            Sample newSample = Sample.createFromWav(wavFile, dither.isSelected(), halfSpeed.isSelected(), 0, 0, 0);\n            Sample existingSample = samples[selectedBank][index];\n            int bytesFreeAfterAdd = bytesFree() - newSample.lengthInBytes() + existingSample.lengthInBytes();\n            if (bytesFreeAfterAdd < 0) {\n                int trim = -bytesFreeAfterAdd / 16;\n                newSample.setTrim(trim);\n                newSample.reload(halfSpeed.isSelected());\n                JOptionPane.showMessageDialog(this,\n                        \"Trimmed sample to fit.\",\n                        \"Kit full!\",\n                        JOptionPane.INFORMATION_MESSAGE);\n            }\n            samples[selectedBank][index] = newSample;\n            renameSample(index, dropExtension(wavFile).toUpperCase());\n            compileKit();\n            updateRomView();\n            playSample();\n        } catch (IOException | UnsupportedAudioFileException e) {\n            e.printStackTrace();\n            showFileErrorMessage(e);\n        }\n    }\n\n    @Override\n    public void renameSample(String s) {\n        renameSample(samplePicker.getSelectedIndex(), s);\n        updateRomView();\n    }\n\n    @Override\n    public void copySample() {\n        ArrayList<Integer> indices = samplePicker.getSelectedIndices();\n        for (int index = 0; index < clipboard.length; ++index) {\n            clipboard[index] = index < indices.size() ? samples[selectedBank][indices.get(index)] : null;\n        }\n    }\n\n    private class PadKeyHandler implements KeyEventPostProcessor {\n        @Override\n        public boolean postProcessKeyEvent(KeyEvent e) {\n            // Makes sure we do nothing if a text field has focus.\n            if (e.getID() != KeyEvent.KEY_TYPED || e.isConsumed()) {\n                return false;\n            }\n            if (e.getModifiersEx() != 0) {\n                return false;\n            }\n            String playChars = \"1234qwerasdfzxc\";\n            int index = playChars.indexOf(Character.toLowerCase(e.getKeyChar()));\n            if (index == -1) {\n                return false;\n            }\n            samplePicker.setSelectedIndex(index);\n            playSample();\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/Sample.java",
    "content": "package kitEditor;\n\nimport java.io.*;\nimport java.util.Random;\nimport javax.sound.sampled.*;\n\nclass Sample {\n    private File file;\n    private String name;\n    private short[] originalSamples;\n    private short[] processedSamples;\n    private int untrimmedLengthInSamples = -1;\n    private int readPos;\n    private int volumeDb = 0;\n    private int pitchSemitones = 0;\n    private int trim = 0;\n    private boolean dither = true;\n\n    public Sample(short[] iBuf, String iName) {\n        if (iBuf != null) {\n            for (int j : iBuf) {\n                assert (j >= Short.MIN_VALUE);\n                assert (j <= Short.MAX_VALUE);\n            }\n            processedSamples = iBuf;\n        }\n        name = iName;\n    }\n\n    public Sample(Sample s) {\n        file = s.file;\n        name = s.name;\n        originalSamples = s.originalSamples;\n        processedSamples = s.processedSamples;\n        untrimmedLengthInSamples = s.untrimmedLengthInSamples;\n        readPos = s.readPos;\n        volumeDb = s.volumeDb;\n        pitchSemitones = s.pitchSemitones;\n        trim = s.trim;\n        dither = s.dither;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String sampleName) {\n        name = sampleName.toUpperCase().substring(0,3);\n    }\n\n    public int lengthInSamples() {\n        return processedSamples.length;\n    }\n\n    public int untrimmedLengthInSamples() {\n        return untrimmedLengthInSamples == -1 ? lengthInSamples() : untrimmedLengthInSamples;\n    }\n\n    public int untrimmedLengthInBytes() {\n        int l = untrimmedLengthInSamples() / 2;\n        l -= l % 0x10;\n        return l;\n    }\n\n    public short[] workSampleData() {\n        return (originalSamples != null ? originalSamples : processedSamples).clone();\n    }\n\n    public int lengthInBytes() {\n        int l = lengthInSamples() / 2;\n        l -= l % 0x10;\n        return l;\n    }\n\n    public void seekStart() {\n        readPos = 0;\n    }\n\n    public short read() {\n        return processedSamples[readPos++];\n    }\n\n    public boolean canAdjustVolume() {\n        return originalSamples != null;\n    }\n\n\n\n    // ------------------\n\n    static Sample createFromNibbles(byte[] nibbles, String name) {\n        short[] buf = new short[nibbles.length * 2];\n\n        for (int i = 0; i < nibbles.length; ++i) {\n            int n = nibbles[i];\n            buf[i * 2] = (short)(n & 0xf0);\n            buf[i * 2 + 1] = (short)((n & 0xf) << 4);\n        }\n        for (int bufIt = 0; bufIt < buf.length; ++bufIt) {\n            short s = (byte)(buf[bufIt] - 0x80);\n            s *= 256;\n            buf[bufIt] = s;\n        }\n        return new Sample(buf, name);\n    }\n\n    // ------------------\n\n    public static Sample createFromWav(File file,\n                                       boolean dither,\n                                       boolean halfSpeed,\n                                       int volumeDb,\n                                       int trim,\n                                       int pitch)\n            throws IOException, UnsupportedAudioFileException {\n        Sample s = new Sample(null, file.getName().split(\"\\\\.\")[0]);\n        s.file = file;\n        s.dither = dither;\n        s.volumeDb = volumeDb;\n        s.trim = trim;\n        s.pitchSemitones = pitch;\n        s.reload(halfSpeed);\n        return s;\n    }\n\n    public static Sample dupeSample(Sample sample) {\n      return new Sample(sample);\n    }\n\n    public void reload(boolean halfSpeed) throws IOException, UnsupportedAudioFileException {\n        if (file == null) {\n            return;\n        }\n        double outFactor = Math.pow(2.0, -pitchSemitones / 12.0);\n        originalSamples = readSamples(file, halfSpeed, outFactor);\n        processSamples();\n    }\n\n    public void processSamples() {\n        int[] intBuffer = toIntBuffer(originalSamples);\n        normalize(intBuffer);\n        intBuffer = trim(intBuffer);\n        if (dither) {\n            dither(intBuffer);\n        }\n        processedSamples = toShortBuffer(intBuffer);\n}\n\n    private int[] trim(int[] intBuffer) {\n        int headPos = headPos(intBuffer);\n        int tailPos = tailPos(intBuffer);\n        if (headPos > tailPos) {\n            return new int[0];\n        }\n        untrimmedLengthInSamples = tailPos + 1 - headPos;\n        tailPos = Math.max(headPos, tailPos - trim * 32);\n        int[] newBuffer = new int[tailPos + 1 - headPos];\n        System.arraycopy(intBuffer, headPos, newBuffer, 0, newBuffer.length);\n\n        if (newBuffer.length >= 32) {\n            return newBuffer;\n        }\n\n        // Extends to 32 samples.\n        int[] zeroPadded = new int[32];\n        System.arraycopy(newBuffer, 0, zeroPadded, 0, newBuffer.length);\n        return zeroPadded;\n    }\n\n    final int SILENCE_THRESHOLD = Short.MAX_VALUE / 16;\n\n    private int headPos(int[] buf) {\n        int i;\n        for (i = 0; i < buf.length; ++i) {\n            if (Math.abs(buf[i]) >= SILENCE_THRESHOLD) {\n                break;\n            }\n        }\n        return i;\n    }\n\n    private int tailPos(int[] buf) {\n        int i;\n        for (i = buf.length - 1; i >= 0; --i) {\n            if (Math.abs(buf[i]) >= SILENCE_THRESHOLD) {\n                break;\n            }\n        }\n        return i;\n    }\n\n    private short[] toShortBuffer(int[] intBuffer) {\n        short[] shortBuffer = new short[intBuffer.length];\n        for (int i = 0; i < intBuffer.length; ++i) {\n            int s = intBuffer[i];\n            s = Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, s));\n            shortBuffer[i] = (short)s;\n        }\n        return shortBuffer;\n    }\n\n    private int[] toIntBuffer(short[] shortBuffer) {\n        int[] intBuffer = new int[shortBuffer.length];\n        for (int i = 0; i < shortBuffer.length; ++i) {\n            intBuffer[i] = shortBuffer[i];\n        }\n        return intBuffer;\n    }\n\n    private static short[] readSamples(File file, boolean halfSpeed, double outRateFactor) throws UnsupportedAudioFileException, IOException {\n        AudioInputStream ais = AudioSystem.getAudioInputStream(file);\n        float inSampleRate = ais.getFormat().getSampleRate();\n        AudioFormat outFormat = new AudioFormat(inSampleRate, 16, 1, true, false);\n        AudioInputStream convertedAis = AudioSystem.getAudioInputStream(outFormat, ais);\n        byte[] b = new byte[convertedAis.available()];\n        int read = convertedAis.read(b);\n        assert read == b.length;\n        short[] samples = new short[b.length / 2];\n        for (int i = 0; i < samples.length; ++i) {\n            samples[i] = (short) ((b[i * 2 + 1] * 256) + ((short)b[i * 2] & 0xff));\n        }\n        convertedAis.close();\n        ais.close();\n\n        double outSampleRate = halfSpeed ? 5734 : 11468;\n        outSampleRate *= outRateFactor;\n        return Sound.resample(inSampleRate, outSampleRate, samples);\n    }\n\n    // Adds triangular probability density function dither noise.\n    private void dither(int[] samples) {\n        Random random = new Random();\n        float state = random.nextFloat();\n        for (int i = 0; i < samples.length; ++i) {\n            int value = samples[i];\n            float r = state;\n            state = random.nextFloat();\n            int noiseLevel = 256 * 16;\n            value += (r - state) * noiseLevel;\n            samples[i] = value;\n        }\n    }\n\n    private void normalize(int[] samples) {\n        double peak = Double.MIN_VALUE;\n        for (int sample : samples) {\n            double s = sample;\n            s = s < 0 ? s / Short.MIN_VALUE : s / Short.MAX_VALUE;\n            peak = Math.max(s, peak);\n        }\n        if (peak == 0) {\n            return;\n        }\n        double volumeAdjust = Math.pow(10, volumeDb / 20.0);\n        for (int i = 0; i < samples.length; ++i) {\n            samples[i] = (int)((samples[i] * volumeAdjust) / peak);\n        }\n    }\n\n    public int getVolumeDb() {\n        return volumeDb;\n    }\n\n    public void setVolumeDb(int value) {\n        volumeDb = value;\n    }\n\n    public File getFile() {\n        return file;\n    }\n\n    public void setTrim(int value) {\n        assert value >= 0;\n        trim = value;\n    }\n\n    public int getTrim() {\n        return trim;\n    }\n\n    public void setPitchSemitones(int value) {\n        pitchSemitones = value;\n    }\n\n    public int getPitchSemitones() {\n        return pitchSemitones;\n    }\n\n    public void setDither(boolean value) {\n        dither = value;\n    }\n\n    public boolean getDither() {\n        return dither;\n    }\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/SamplePicker.java",
    "content": "package kitEditor;\n\nimport net.miginfocom.swing.MigLayout;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.*;\nimport java.util.ArrayList;\nimport java.util.TreeSet;\n\nimport static java.awt.event.InputEvent.SHIFT_DOWN_MASK;\n\npublic class SamplePicker extends JPanel {\n    private Listener listener;\n\n    interface Listener {\n        void selectionChanged();\n        void playSample();\n        void deleteSample();\n        void dupeSample();\n        void replaceSample();\n        void renameSample(String s);\n        void copySample();\n    }\n\n    class Pad extends JToggleButton {\n        int id;\n        Pad(int id) {\n            this.id = id;\n            setPreferredSize(new Dimension(64, 64));\n            addMouseListener(new MouseAdapter() {\n                @Override\n                public void mousePressed(MouseEvent e) {\n                    super.mouseClicked(e);\n                    showPopup(e);\n                }\n\n                @Override\n                public void mouseReleased(MouseEvent e) {\n                    super.mouseClicked(e);\n                    showPopup(e);\n                }\n\n                private void showPopup(MouseEvent e) {\n                    if (!e.isPopupTrigger() || getText().equals(\"---\")) {\n                        return;\n                    }\n                    JPopupMenu menu = new JPopupMenu();\n                    JMenuItem copy = new JMenuItem(\"Copy\");\n                    menu.add(copy);\n                    copy.addActionListener(e1 -> listener.copySample());\n                    JMenuItem rename = new JMenuItem(\"Rename...\");\n                    menu.add(rename);\n                    rename.addActionListener(e1 -> {\n                        String name = JOptionPane.showInputDialog(\"Enter new sample name\");\n                        if (name != null) {\n                            listener.renameSample(name);\n                        }\n                    });\n                    JMenuItem replace = new JMenuItem(\"Replace...\");\n                    menu.add(replace);\n                    replace.addActionListener(e1 -> listener.replaceSample());\n                    JMenuItem duplicate = new JMenuItem(\"Duplicate\");\n                    menu.add(duplicate);\n                    duplicate.addActionListener(e1 -> listener.dupeSample());\n                    JMenuItem delete = new JMenuItem(\"Delete\");\n                    menu.add(delete);\n                    delete.addActionListener(e1 -> listener.deleteSample());\n                    menu.show(e.getComponent(), e.getX(), e.getY());\n                }\n            });\n        }\n    }\n\n    private final ArrayList<Pad> pads;\n    private final TreeSet<Integer> selectedIndices = new TreeSet<>();\n\n    SamplePicker() {\n        setLayout(new MigLayout());\n        pads = new ArrayList<>();\n        for (int i = 0; i < 15; i++) {\n            Pad pad = createPad();\n            pad.addMouseListener(new MouseAdapter() {\n                @Override\n                public void mousePressed(MouseEvent e) {\n                    super.mousePressed(e);\n                    if ((e.getModifiersEx() & SHIFT_DOWN_MASK) == 0) {\n                        selectedIndices.clear();\n                        for (int i = 0; i < 15; ++i) {\n                            JToggleButton button = pads.get(i);\n                            button.setSelected(false);\n                        }\n                    }\n\n                    Pad sender = (Pad)e.getSource();\n                    int min = Math.min(sender.id, selectedIndices.isEmpty() ? Integer.MAX_VALUE : selectedIndices.first());\n                    int max = Math.max(sender.id, selectedIndices.isEmpty() ? Integer.MIN_VALUE : selectedIndices.last());\n                    for (int i = 0; i < 15; ++i) {\n                        if (i >= min && i <= max) {\n                            selectedIndices.add(i);\n                            pads.get(i).setSelected(true);\n                        }\n                    }\n                    listener.selectionChanged();\n                    if (e.getButton() == MouseEvent.BUTTON1) {\n                        listener.playSample();\n                    }\n                }\n            });\n        }\n    }\n\n    @Override\n    public void grabFocus() {\n        pads.get(0).grabFocus();\n    }\n\n    private Pad createPad() {\n        Pad pad = new Pad(pads.size());\n        pad.setToolTipText(\"Play by keys: 1234 QWER ASDF ZXC\");\n        pads.add(pad);\n        add(pad, (pads.size() % 4) == 0 ? \"wrap, sg button\" : \"\");\n        return pad;\n    }\n\n    public void setListData(String[] listData) {\n        assert listData.length == pads.size();\n        for (int i = 0; i < pads.size(); ++i) {\n            pads.get(i).setText(listData[i]);\n        }\n    }\n\n    public void setSelectedIndex(int selectedIndex) {\n        selectedIndices.clear();\n        for (int i = 0; i < 15; ++i) {\n            JToggleButton button = pads.get(i);\n            button.setSelected(false);\n        }\n        if (selectedIndex == -1) {\n            return;\n        }\n        selectedIndices.add(selectedIndex);\n        pads.get(selectedIndex).setSelected(true);\n        listener.selectionChanged();\n    }\n\n    public int getSelectedIndex() {\n        for (Pad pad : pads) {\n            if (pad.isSelected()) {\n                return pad.id;\n            }\n        }\n        return selectedIndices.isEmpty() ? -1 : selectedIndices.first();\n    }\n\n    public ArrayList<Integer> getSelectedIndices() {\n        return new ArrayList<>(selectedIndices);\n    }\n\n    public void addListSelectionListener(Listener listener) {\n        this.listener = listener;\n    }\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/SampleView.java",
    "content": "package kitEditor;\n\nimport java.awt.*;\nimport java.awt.geom.GeneralPath;\nimport java.util.Locale;\n\npublic class SampleView extends Canvas {\n    private byte[] buf;\n    private float duration;\n\n    public void setBufferContent(byte[] newBuffer, float duration) {\n        buf = newBuffer;\n        setBackground(Color.black);\n        this.duration = duration;\n    }\n\n    @Override\n    public void paint(Graphics gg) {\n        Graphics2D g = (Graphics2D) gg;\n        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,\n                RenderingHints.VALUE_ANTIALIAS_ON);\n\n        double w = g.getClipBounds().getWidth();\n        double h = g.getClipBounds().getHeight();\n\n        if (buf == null) {\n            return;\n        }\n\n        GeneralPath gp = new GeneralPath();\n        gp.moveTo(0, h / 2);\n        for (int it = 0; it < buf.length; ++it) {\n            // Only draws every second sample. This is probably OK.\n            double val = buf[it] & 0xf;\n            val -= 7.5;\n            val /= 7.5;\n            gp.lineTo(it * w / (buf.length - 1), h * (1 - val) / 2);\n        }\n        g.setColor(Color.YELLOW);\n        g.draw(gp);\n\n        drawDuration(g, (int) w, (int) h);\n    }\n\n    private void drawDuration(Graphics2D g, int w, int h) {\n        String durationText = String.format(Locale.US, \"%.3fs\", duration);\n        int x = -g.getFontMetrics().stringWidth(durationText) - 1;\n        int y = -2;\n        g.setColor(Color.BLACK);\n        g.drawString(durationText, w + x, h + y);\n        --x;\n        --y;\n        g.setColor(Color.WHITE);\n        g.drawString(durationText, w + x, h + y);\n    }\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/Sound.java",
    "content": "// Copyright (C) 2001, Johan Kotlinski\n\npackage kitEditor;\n\nimport com.laszlosystems.libresample4j.Resampler;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport javax.sound.sampled.*;\n\npublic class Sound {\n\n    private static final ArrayList<Clip> clipPool = new ArrayList<>();\n    private static final int PLAYBACK_RATE = 48000;\n\n    private static short[] unpackNibbles(byte[] gbSample) {\n        byte[] waveData = new byte[gbSample.length * 2];\n        int src = 0;\n        int dst = 0;\n\n        while (src < gbSample.length) {\n            byte sample = gbSample[src++];\n            waveData[dst++] = (byte) (0xf0 & sample);\n            waveData[dst++] = (byte) ((0x0f & sample) << 4);\n        }\n\n        short[] s = new short[waveData.length];\n        for (int i = 0; i < s.length; ++i) {\n            int v = waveData[i] & 0xf0;\n            v -= 0x78;\n            v *= Short.MAX_VALUE;\n            v /= 0x78;\n            s[i] = (short)v;\n        }\n        return s;\n    }\n\n    private static Clip getClip() throws LineUnavailableException {\n        for (Clip clip : clipPool) {\n            if (!clip.isRunning()) {\n                clip.close();\n                return clip;\n            }\n        }\n        Clip newClip = AudioSystem.getClip();\n        clipPool.add(newClip);\n        return newClip;\n    }\n\n    static void play(byte[] gbSample, boolean halfSpeed) throws LineUnavailableException, IOException {\n        final int sampleRate = halfSpeed ? 5734 : 11468;\n        byte[] b = toByteArray(resampleForPlayback(sampleRate, unpackNibbles(gbSample)));\n        Clip clip = getClip();\n        clip.open(new AudioInputStream(new ByteArrayInputStream(b),\n                new AudioFormat(PLAYBACK_RATE, 16, 1, true, false),\n                b.length / 2));\n        clip.start();\n    }\n\n    private static short[] resampleForPlayback(int srcRate, short[] src) {\n        short[] dst = new short[Sound.PLAYBACK_RATE * src.length / srcRate];\n        // Nearest neighbor resampling is good for emulating Game Boy sound.\n        for (int i = 0; i < dst.length; ++i) {\n            dst[i] = src[i * srcRate / Sound.PLAYBACK_RATE];\n        }\n        return dst;\n    }\n\n    private static byte[] toByteArray(short[] waveData) {\n        byte[] b = new byte[waveData.length * 2];\n        for (int i = 0; i < waveData.length; ++i) {\n            b[i * 2] = (byte)(waveData[i] & 0xff);\n            b[i * 2 + 1] = (byte)(waveData[i] >> 8);\n        }\n        return b;\n    }\n\n    static void stopAll() {\n        for (Clip clip : clipPool) {\n            clip.stop();\n        }\n    }\n\n    public static short[] resample(double inSampleRate, double outSampleRate, short[] samples) {\n        if (inSampleRate == outSampleRate) {\n            return samples;\n        }\n        float[] inBuf = new float[samples.length];\n        float dcOffset = 0;\n        for (int i = 0; i < inBuf.length; ++i) {\n            inBuf[i] = (float) samples[i] / -Short.MIN_VALUE;\n            dcOffset += inBuf[i] / inBuf.length;\n        }\n\n        // Removes DC offset.\n        for (int i = 0; i < inBuf.length; ++i) {\n            inBuf[i] -= dcOffset;\n        }\n\n        double factor = outSampleRate / inSampleRate;\n        float[] outBuf = new float[(int)(inBuf.length * factor + 1)];\n        Resampler resampler = new Resampler(true, factor, factor);\n        Resampler.Result result = resampler.process(factor, inBuf, 0, inBuf.length, true, outBuf, 0, outBuf.length);\n\n        // avoid clipping\n        float peak = 0;\n        for (float v : outBuf) {\n            peak = Math.max(peak, Math.abs(v));\n        }\n        if (peak > 1) {\n            for (int i = 0; i < outBuf.length; ++i) {\n                outBuf[i] /= peak;\n            }\n        }\n\n        short[] finalBuf = new short[result.outputSamplesGenerated];\n        for (int i = 0; i < finalBuf.length; ++i) {\n            finalBuf[i] = (short)(outBuf[i] * Short.MAX_VALUE);\n        }\n        return finalBuf;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/WaveFile.java",
    "content": "package kitEditor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\n\npublic class WaveFile {\n    public static void write(short[] pcm, File f) throws IOException {\n        RandomAccessFile wavFile = new RandomAccessFile(f, \"rw\");\n\n        int payloadSize = pcm.length * 2;\n        int fileSize = pcm.length * 2 + 0x2c;\n        int waveSize = fileSize - 8;\n\n        byte[] header = {\n                0x52, 0x49, 0x46, 0x46,  // RIFF\n                (byte) waveSize,\n                (byte) (waveSize >> 8),\n                (byte) (waveSize >> 16),\n                (byte) (waveSize >> 24),\n                0x57, 0x41, 0x56, 0x45,  // WAVE\n                // --- fmt chunk\n                0x66, 0x6D, 0x74, 0x20,  // fmt\n                16, 0, 0, 0,  // fmt size\n                1, 0,  // pcm\n                1, 0,  // channel count\n                (byte) 0xcc, 0x2c, 0, 0,  // freq (11468 hz)\n                (byte) 0xcc, 0x2c, 0, 0,  // avg. bytes/sec\n                1, 0,  // block align\n                16, 0,  // bits per sample\n                // --- data chunk\n                0x64, 0x61, 0x74, 0x61,  // data\n                (byte) payloadSize,\n                (byte) (payloadSize >> 8),\n                (byte) (payloadSize >> 16),\n                (byte) (payloadSize >> 24)\n        };\n\n        wavFile.write(header);\n\n        byte[] byteBuffer = new byte[pcm.length * 2];\n        int dst = 0;\n        for (short sample : pcm) {\n            byteBuffer[dst++] = (byte) sample;\n            byteBuffer[dst++] = (byte) (sample >> 8);\n        }\n        wavFile.write(byteBuffer);\n        wavFile.close();\n    }\n}\n"
  },
  {
    "path": "src/main/java/kitEditor/sbc.java",
    "content": "package kitEditor;\n\n// Sample bank creator.\n\nclass sbc {\n\n    public static void compile(byte[] dst, Sample[] samples, int[] byteLength, boolean gameBoyAdvancePolarity) {\n        int offset = 0x60; //don't overwrite sample bank info!\n        for (int sampleIt = 0; sampleIt < samples.length; sampleIt++) {\n            Sample sample = samples[sampleIt];\n            if (sample == null) {\n                break;\n            }\n\n            sample.seekStart();\n            int sampleLength = sample.lengthInSamples();\n\n            int addedBytes = 0;\n            int[] outputBuffer = new int[32];\n            int outputCounter = 0;\n            for (int i = 0; i < sampleLength; i++) {\n                int s = sample.read();\n                s = (int)(Math.round((double)s / (256 * 16) + 7.5));\n                s = Math.min(0xf, Math.max(0, s));\n                if (!gameBoyAdvancePolarity) {\n                    // Use DMG polarity, where 0xf = -1.0 and 0 = 1.0.\n                    s = 0xf - s;\n                }\n\n                // Starting from LSDj 9.2.0, first sample is skipped to compensate for wave refresh bug.\n                // This rotates the wave frame rightwards.\n                outputBuffer[(outputCounter + 1) % 32] = s;\n\n                if (outputCounter == 31) {\n                    for (int j = 0; j != 32; j += 2) {\n                        dst[offset++] = (byte) (outputBuffer[j] * 0x10 + outputBuffer[j + 1]);\n                    }\n                    outputCounter = -1;\n                    addedBytes += 0x10;\n                }\n                outputCounter++;\n            }\n\n            byteLength[sampleIt] = addedBytes;\n        }\n        while (offset < 0x4000) {\n            dst[offset++] = -1; // rst opcode\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/lsdpatch/LSDPatcher.java",
    "content": "// Copyright (C) 2001, Johan Kotlinski\n\npackage lsdpatch;\n\nimport utils.CommandLineFunctions;\nimport utils.GlobalHolder;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.font.TextAttribute;\nimport java.util.HashMap;\nimport java.util.prefs.Preferences;\n\npublic class LSDPatcher {\n    private static void initUi() {\n        JFrame frame = new MainWindow();\n        // Validate frames that have preset sizes\n        // Pack frames that have useful preferred size info, e.g. from their layout\n        frame.pack();\n        frame.validate();\n\n        // Center the window\n        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();\n        Dimension frameSize = frame.getSize();\n        if (frameSize.height > screenSize.height) {\n            frameSize.height = screenSize.height;\n        }\n        if (frameSize.width > screenSize.width) {\n            frameSize.width = screenSize.width;\n        }\n        frame.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);\n        frame.setVisible(true);\n    }\n\n    private static void usage() {\n        System.out.printf(\"LSDJPatcher v%s\\n\\n\", NewVersionChecker.getCurrentVersion());\n        System.out.println(\"java -jar LSDJPatcher.jar\");\n        System.out.println(\" Opens the GUI.\\n\");\n\n        System.out.println(\"java -jar LSDJPatcher.jar fnt2png [--extended] <fntfile> <pngfile>\");\n        System.out.println(\" Exports the font file into a PNG\\n\");\n\n        System.out.println(\"java -jar LSDJPatcher.jar png2fnt <font title> <pngfile> <fntfile>\");\n        System.out.println(\" Converts the PNG into a font with given name.\\n\");\n\n        System.out.println(\"java -jar LSDJPatcher.jar romfnt2png [--extended] <romFile> <fontIndex>\");\n        System.out.println(\" Extracts the nth font from the given rom into a png named like the font.\\n\");\n\n        System.out.println(\"java -jar LSDJPatcher.jar png2romfnt <romFile> <pngfile> <index> <fontname>\");\n        System.out.println(\" Imports the PNG into the rom with given name.\\n\");\n\n        System.out.println(\"java -jar LSDJPatcher.jar clone <inRomFile> <outRomlFile>\");\n        System.out.println(\" Clones all customizations from a ROM file to another.\\n\");\n\n    }\n\n    public static void main(String[] args) {\n        if (args.length >= 1) {\n            processArguments(args);\n            return;\n        }\n        try {\n            // Use the system's UI look when applicable\n            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\n            System.setProperty(\"apple.laf.useScreenMenuBar\", \"true\");\n\n            // Use font anti-aliasing when applicable\n            System.setProperty(\"awt.useSystemAAFontSettings\",\"on\");\n            System.setProperty(\"swing.aatext\", \"true\");\n            useJLabelFontForMenus();\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        Preferences preferences = Preferences.userRoot().node(LSDPatcher.class.getName());\n        GlobalHolder.set(preferences, Preferences.class);\n\n        initUi();\n    }\n\n    private static void useJLabelFontForMenus() {\n        // On some systems, the default font given to menus is a bit wonky with anti-aliasing. Using the one given\n        // to JLabels will give a better result.\n        Font systemFont = new JLabel().getFont();\n        HashMap<TextAttribute, Object> attributes = new HashMap<>(systemFont.getAttributes());\n        attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_SEMIBOLD);\n        attributes.put(TextAttribute.SIZE, systemFont.getSize());\n        Font selectedFont = Font.getFont(attributes);\n        UIManager.put(\"Menu.font\", selectedFont);\n        UIManager.put(\"MenuBar.font\", selectedFont);\n        UIManager.put(\"MenuItem.font\", selectedFont);\n    }\n\n    private static void processArguments(String[] args) {\n        String command = args[0].toLowerCase();\n\n        boolean includeGfxCharacters = false;\n        if(args.length > 2 && args[1].equalsIgnoreCase(\"--extended\")) {\n            includeGfxCharacters = true;\n        }\n\n        if (command.compareTo(\"fnt2png\") == 0 && args.length == 3) {\n            CommandLineFunctions.fontToPng(args[1], args[2]);\n        } else if (command.compareTo(\"fnt2png\") == 0 && args.length == 4 && includeGfxCharacters) {\n            CommandLineFunctions.fontToPng(args[2], args[3]);\n        } else if (command.compareTo(\"png2fnt\") == 0 && args.length == 4) {\n            CommandLineFunctions.pngToFont(args[1], args[2], args[3]);\n        } else if (command.compareTo(\"romfnt2png\") == 0 && args.length == 3) {\n            // -1 to allow 1-3 range instead of 0-2\n            CommandLineFunctions.extractFontToPng(args[1], Integer.parseInt(args[2]) - 1, false);\n        } else if (command.compareTo(\"romfnt2png\") == 0 && args.length == 4 && includeGfxCharacters) {\n            // -1 to allow 1-3 range instead of 0-2\n            CommandLineFunctions.extractFontToPng(args[2], Integer.parseInt(args[3]) - 1, true);\n        } else if (command.compareTo(\"png2romfnt\") == 0 && args.length == 5) {\n            // -1 to allow 1-3 range instead of 0-2\n            CommandLineFunctions.loadPngToRom(args[1], args[2], Integer.parseInt(args[3]) - 1, args[4]);\n        } else if (command.compareTo(\"clone\") == 0 && args.length == 3) {\n            // -1 to allow 1-3 range instead of 0-2\n            CommandLineFunctions.copyAllCustomizations(args[1], args[2]);\n        } else {\n            usage();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/lsdpatch/MainWindow.java",
    "content": "// Copyright (C) 2020, Johan Kotlinski\n\npackage lsdpatch;\n\nimport Document.*;\nimport fontEditor.FontEditor;\nimport kitEditor.KitEditor;\nimport net.miginfocom.swing.MigLayout;\nimport paletteEditor.PaletteEditor;\nimport songManager.SongManager;\nimport utils.EditorPreferences;\nimport utils.FileDialogLauncher;\nimport utils.RomUtilities;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\npublic class MainWindow extends JFrame implements IDocumentListener, KitEditor.Listener {\n    JTextField romTextField = new JTextField();\n    JTextField savTextField = new JTextField();\n\n    JButton upgradeRomButton = new JButton(\"Upgrade ROM\");\n    JButton songManagerButton = new JButton(\"Songs\");\n    JButton editKitsButton = new JButton(\"Sample Kits\");\n    JButton editFontsButton = new JButton(\"Fonts\");\n    JButton editPalettesButton = new JButton(\"Palettes\");\n    JButton saveButton = new JButton(\"Save...\");\n\n    MainWindow() {\n        document.subscribe(this);\n\n        updateTitle();\n        JPanel panel = new JPanel();\n        getContentPane().add(panel);\n        MigLayout rootLayout = new MigLayout(\"wrap 6\");\n        panel.setLayout(rootLayout);\n\n        addSelectors(panel);\n\n        panel.add(new JSeparator(), \"span 5\");\n\n        upgradeRomButton.addActionListener(e -> openRomUpgradeTool());\n        panel.add(upgradeRomButton);\n\n        songManagerButton.addActionListener(e -> openSongManager());\n        panel.add(songManagerButton);\n\n        editKitsButton.addActionListener(e ->\n                new KitEditor(this, document, this).setLocationRelativeTo(this));\n        panel.add(editKitsButton);\n\n        editFontsButton.addActionListener(e -> openFontEditor());\n        panel.add(editFontsButton);\n\n        editPalettesButton.addActionListener(e -> openPaletteEditor());\n        panel.add(editPalettesButton, \"grow x\");\n\n        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                if (!document.isDirty() || JOptionPane.showConfirmDialog(null,\n                        \"Quit without saving changes?\",\n                        \"Unsaved changes\",\n                        JOptionPane.OK_CANCEL_OPTION,\n                        JOptionPane.WARNING_MESSAGE) == JOptionPane.OK_OPTION) {\n                    setDefaultCloseOperation(EXIT_ON_CLOSE);\n                }\n                super.windowClosing(e);\n            }\n        });\n\n        setResizable(false);\n\n        NewVersionChecker.checkGithub(this);\n    }\n\n    private void openRomUpgradeTool() {\n        RomUpgradeTool romUpgradeTool = new RomUpgradeTool(this, document);\n        romUpgradeTool.setLocationRelativeTo(this);\n        romUpgradeTool.setVisible(true);\n    }\n\n    private void openSongManager() {\n        SongManager savManager = new SongManager(this, document);\n        savManager.setLocationRelativeTo(this);\n        savManager.setVisible(true);\n    }\n\n    private void openPaletteEditor() {\n        PaletteEditor editor = new PaletteEditor(this, document);\n        editor.setLocationRelativeTo(this);\n        editor.setVisible(true);\n    }\n\n    private void openFontEditor() {\n        FontEditor fontEditor = new FontEditor(this, document);\n        fontEditor.setLocationRelativeTo(this);\n        fontEditor.setVisible(true);\n    }\n\n    final Document document = new Document();\n\n    private void addSelectors(JPanel panel) {\n        romTextField.setMinimumSize(new Dimension(300, 0));\n        romTextField.setText(EditorPreferences.lastPath(\"gb\"));\n        romTextField.setEditable(false);\n        panel.add(romTextField, \"span 4, grow x\");\n\n        JButton browseRomButton = new JButton(\"Browse...\");\n        browseRomButton.addActionListener(e -> onBrowseRomButtonPress());\n        panel.add(browseRomButton);\n\n        saveButton.setEnabled(false);\n        saveButton.addActionListener(e -> onSave(true));\n        panel.add(saveButton, \"span 1 4, grow y\");\n\n        savTextField.setMinimumSize(new Dimension(300, 0));\n        savTextField.setEditable(false);\n        savTextField.setText(EditorPreferences.lastPath(\"sav\"));\n        panel.add(savTextField, \"span 4, grow x\");\n\n        JButton browseSavButton = new JButton(\"Browse...\");\n        browseSavButton.addActionListener(e -> onBrowseSavButtonPress());\n        panel.add(browseSavButton);\n\n        try {\n            document.loadRomImage(EditorPreferences.lastPath(\"gb\"));\n        } catch (IOException e) {\n            resetRomTextField();\n        }\n        try {\n            document.loadSavFile(EditorPreferences.lastPath(\"sav\"));\n        } catch (IOException e) {\n            resetSavTextField();\n        }\n        updateButtonsFromTextFields();\n    }\n\n    private void resetRomTextField() {\n        romTextField.setText(\"Select LSDj ROM file -->\");\n    }\n\n    private void resetSavTextField() {\n        savTextField.setText(\"Select LSDj .sav file -->\");\n    }\n\n    private void onBrowseRomButtonPress() {\n        if (document.isDirty() && JOptionPane.showConfirmDialog(null,\n                \"Load new ROM without saving changes?\",\n                \"Unsaved changes\",\n                JOptionPane.OK_CANCEL_OPTION,\n                JOptionPane.WARNING_MESSAGE) == JOptionPane.CANCEL_OPTION) {\n            return;\n        }\n\n        File romFile = FileDialogLauncher.load(this,\n                \"Select LSDj ROM Image\",\n                new String[]{ \"gb\", \"gbc\" });\n        if (romFile == null) {\n            return;\n        }\n\n        String romPath = romFile.getAbsolutePath();\n        try {\n            document.loadRomImage(romPath);\n            romTextField.setText(romPath);\n        } catch (IOException e) {\n            resetRomTextField();\n            e.printStackTrace();\n            JOptionPane.showMessageDialog(this,\n                    e.getMessage() == null ? \"Could not load \" + romPath : e.getMessage(),\n                    \"ROM load failed!\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n\n        String savPath = romPath\n                .replaceFirst(\".gbc$\", \".sav\")\n                .replaceFirst(\".gb$\", \".sav\");\n        try {\n            document.loadSavFile(savPath);\n            savTextField.setText(savPath);\n            EditorPreferences.setLastPath(\"sav\", savPath);\n        } catch (IOException e) {\n            resetSavTextField();\n            e.printStackTrace();\n        }\n        updateButtonsFromTextFields();\n    }\n\n    private void onBrowseSavButtonPress() {\n        if (document.isSavDirty() && JOptionPane.showConfirmDialog(null,\n                \"Load new .sav without saving changes?\",\n                \"Unsaved changes\",\n                JOptionPane.OK_CANCEL_OPTION,\n                JOptionPane.WARNING_MESSAGE) == JOptionPane.CANCEL_OPTION) {\n            return;\n        }\n\n        File savFile = FileDialogLauncher.load(this, \"Load Save File\", \"sav\");\n        if (savFile == null) {\n            return;\n        }\n        try {\n            document.loadSavFile(savFile.getAbsolutePath());\n            savTextField.setText(savFile.getAbsolutePath());\n        } catch (IOException e) {\n            resetSavTextField();\n            JOptionPane.showMessageDialog(this,\n                    e.getMessage(),\n                    \".sav load failed!\",\n                    JOptionPane.ERROR_MESSAGE);\n            e.printStackTrace();\n        }\n        updateButtonsFromTextFields();\n    }\n\n    void updateButtonsFromTextFields() {\n        byte[] romImage = document.romImage();\n        boolean romOk = romImage != null;\n        String savPath = savTextField.getText();\n        boolean savPathOk = savPath.endsWith(\".sav\") && new File(savPath).exists();\n        boolean foundPalettes = romOk && RomUtilities.validatePaletteData(romImage);\n\n        romTextField.setBackground(romOk ? Color.white : Color.pink);\n        savTextField.setBackground(savPathOk ? Color.white : Color.pink);\n\n        editKitsButton.setEnabled(romOk);\n        editFontsButton.setEnabled(romOk && foundPalettes);\n        editPalettesButton.setEnabled(romOk && foundPalettes);\n        upgradeRomButton.setEnabled(romOk && foundPalettes);\n        songManagerButton.setEnabled(savPathOk && romOk);\n    }\n\n    public void onDocumentDirty(boolean dirty) {\n        updateTitle();\n        upgradeRomButton.setEnabled(!dirty);\n        saveButton.setEnabled(dirty);\n    }\n\n    private void updateTitle() {\n        String title = \"LSDPatcher v\" + NewVersionChecker.getCurrentVersion();\n        if (document.romImage() != null) {\n            title = title + \" - \" + document.romFile().getName();\n            if (document.isDirty()) {\n                title = title + '*';\n            }\n        }\n        setTitle(title);\n    }\n\n    private void onSave(boolean saveSavFile) {\n        File f = FileDialogLauncher.save(this,\n                \"Save ROM Image\",\n                new String[]{ \"gb\", \"gbc\" });\n        if (f == null) {\n            return;\n        }\n        String romPath = f.getAbsolutePath();\n\n        try (FileOutputStream fileOutputStream = new FileOutputStream(romPath)) {\n            byte[] romImage = document.romImage();\n            RomUtilities.fixChecksum(romImage);\n            fileOutputStream.write(romImage);\n            fileOutputStream.close();\n            if (document.savFile() != null && saveSavFile) {\n                String savPath = romPath\n                        .replace(\".gbc\", \".sav\")\n                        .replace(\".gb\", \".sav\");\n                document.savFile().saveAs(savPath);\n                savTextField.setText(savPath);\n                document.loadSavFile(savPath);\n                document.clearSavDirty();\n                EditorPreferences.setLastPath(\"sav\", savPath);\n            }\n            romTextField.setText(romPath);\n            document.setRomFile(new File(romPath));\n            document.clearRomDirty();\n            EditorPreferences.setLastPath(\"gb\", romPath);\n            saveButton.setEnabled(false);\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(this,\n                    e.getMessage(),\n                    \"File save failed!\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n    }\n\n    @Override\n    public void saveRom() {\n        onSave(false);\n    }\n}\n"
  },
  {
    "path": "src/main/java/lsdpatch/NewVersionChecker.java",
    "content": "package lsdpatch;\n\nimport utils.GlobalHolder;\n\nimport javax.swing.*;\nimport java.io.IOException;\nimport java.net.URL;\n\npublic class NewVersionChecker {\n    public static String getCurrentVersion() {\n        String version = GlobalHolder.class.getPackage().getImplementationVersion();\n        if (version == null) {\n            return \"DEV\";\n        }\n        return version;\n    }\n\n    public static void checkGithub(JFrame parent) {\n        String currentVersion = getCurrentVersion();\n        if (currentVersion.equals(\"DEV\")) {\n            return;\n        }\n        String response;\n        try {\n            String apiPath = \"https://api.github.com/repos/jkotlinski/lsdpatch/releases/latest\";\n            response = WwwUtil.fetchWwwPage(new URL(apiPath));\n        } catch (IOException e) {\n            return;\n        }\n        if (response.contains(\"\\\"v\" + currentVersion + '\"')) {\n            return;\n        }\n        if (JOptionPane.showConfirmDialog(parent,\n                \"A new LSDPatcher release is available. Do you want to see it?\",\n                \"Version upgrade\",\n               JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {\n            WwwUtil.openInBrowser(\"https://github.com/jkotlinski/lsdpatch/releases\");\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/lsdpatch/RomUpgradeTool.java",
    "content": "// Copyright (C) 2020, Johan Kotlinski\n\npackage lsdpatch;\n\nimport Document.Document;\nimport net.miginfocom.swing.MigLayout;\nimport structures.LSDJFont;\nimport utils.RomUtilities;\n\nimport javax.swing.*;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.io.*;\nimport java.net.URL;\nimport java.util.regex.MatchResult;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.ZipInputStream;\n\npublic class RomUpgradeTool extends JFrame {\n    final String changeLogPath = \"https://www.littlesounddj.com/lsd/latest/CHANGELOG.txt\";\n    final String licensePath = \"https://www.littlesounddj.com/lsd/latest/rom_images/LICENSE.txt\";\n    final String developPath = \"https://www.littlesounddj.com/lsd/latest/rom_images/develop/\";\n    final String stablePath = \"https://www.littlesounddj.com/lsd/latest/rom_images/stable/\";\n    final String arduinoBoyPath = \"https://www.littlesounddj.com/lsd/latest/rom_images/arduinoboy/\";\n\n    private final File localRomFile;\n    private byte[] localRomImage;\n    private final byte[] remoteRomImage;\n    private final Document document;\n\n    RomUpgradeTool(JFrame parent, Document document) {\n        parent.setEnabled(false);\n\n        this.document = document;\n        localRomFile = document.romFile();\n        localRomImage = document.romImage();\n        remoteRomImage = new byte[localRomImage.length];\n\n        JPanel panel = new JPanel();\n        getContentPane().add(panel);\n        panel.setLayout(new MigLayout(\"wrap\"));\n\n        panel.add(new JLabel(\"Upgrade ROM to latest:\"));\n        JButton upgradeStableButton = new JButton(\"Stable version (recommended!)\");\n        JButton upgradeDevelopButton = new JButton(\"Development version (experimental!)\");\n        JButton upgradeArduinoBoyButton = new JButton(\"ArduinoBoy version\");\n        JButton upgradeFromFileButton = new JButton(\"Select ROM file...\");\n        JButton viewChangeLogButton = new JButton(\"View Changelog\");\n        JButton viewLicenseButton = new JButton(\"View License Information\");\n        panel.add(upgradeStableButton, \"growx\");\n        panel.add(upgradeDevelopButton, \"growx\");\n        panel.add(upgradeArduinoBoyButton, \"growx\");\n        panel.add(upgradeFromFileButton, \"growx\");\n        panel.add(viewChangeLogButton, \"growx, gaptop 10\");\n        panel.add(viewLicenseButton, \"growx\");\n        pack();\n\n        upgradeStableButton.addActionListener(e -> upgrade(stablePath));\n        upgradeDevelopButton.addActionListener(e -> upgrade(developPath));\n        upgradeArduinoBoyButton.addActionListener(e -> upgrade(arduinoBoyPath));\n        upgradeFromFileButton.addActionListener(e -> upgradeFromSelectedFile());\n        viewChangeLogButton.addActionListener(e -> WwwUtil.openInBrowser(changeLogPath));\n        viewLicenseButton.addActionListener(e -> WwwUtil.openInBrowser(licensePath));\n\n        setResizable(false);\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                super.windowClosing(e);\n                parent.setEnabled(true);\n            }\n        });\n    }\n\n    private boolean versionCompare(String localVersion, String remoteVersion) {\n        assert(remoteVersion.startsWith(\"lsdj\"));\n        remoteVersion = remoteVersion.substring(4, 9).replace('_', '.');\n        return remoteVersion.compareTo(localVersion) > 0;\n    }\n\n    private void upgrade(String basePath) {\n        try {\n            String localVersion = localVersion();\n            String remoteVersion = fetchLatestRemoteVersion(basePath);\n            if (localVersion == null || remoteVersion == null) {\n                JOptionPane.showMessageDialog(null,\n                        \"Version information not found!\",\n                        \"Update failed!\",\n                        JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n            if (!versionCompare(localVersion, remoteVersion)) {\n                JOptionPane.showMessageDialog(this,\n                        localRomFile.getName() + \" is already updated.\",\n                        \"No updates found!\",\n                        JOptionPane.INFORMATION_MESSAGE);\n                return;\n            }\n            int reply = JOptionPane.showConfirmDialog(this,\n                    \"Current ROM version: \" + localVersion() + '\\n' +\n                            \"Upgrade to \" + remoteVersion + '?',\n                    \"Upgrade?\",\n                    JOptionPane.YES_NO_OPTION);\n            if (reply != JOptionPane.YES_OPTION) {\n                return;\n            }\n            ZipInputStream zipInputStream = new ZipInputStream(new URL(basePath + remoteVersion).openStream());\n            zipInputStream.getNextEntry();\n            int dstIndex = 0;\n            while (dstIndex != remoteRomImage.length) {\n                dstIndex += zipInputStream.read(remoteRomImage, dstIndex, remoteRomImage.length - dstIndex);\n            }\n            importAll();\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(null,\n                    e.getMessage(),\n                    \"Fetching new version failed!\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n    }\n\n    private String localVersion() {\n        byte[] romImage = localRomImage;\n        for (int i = 0; i < romImage.length; ++i) {\n            if (romImage[i] == 'V' && romImage[i + 2] == '.' && romImage[i + 4] == '.') {\n                String s = \"\";\n                s += (char)romImage[i + 1];\n                s += (char)romImage[i + 2];\n                s += (char)romImage[i + 3];\n                s += (char)romImage[i + 4];\n                s += (char)romImage[i + 5];\n                return s;\n            }\n        }\n        return null;\n    }\n\n    private String fetchLatestRemoteVersion(String basePath) throws IOException {\n        String page = WwwUtil.fetchWwwPage(new URL(basePath));\n        Pattern p = Pattern.compile(\"lsdj\\\\d_\\\\d_[0-9A-Z][-a-zA-Z]*\\\\.zip\");\n        Matcher m = p.matcher(page);\n        if (m.find()) {\n            MatchResult matchResult = m.toMatchResult();\n            return matchResult.group();\n        } else {\n            return null;\n        }\n    }\n\n    private void importAll() {\n        if (importKits() == 0) {\n            JOptionPane.showMessageDialog(this,\n                    \"Kit copy error.\",\n                    \"Kit import result.\", JOptionPane.INFORMATION_MESSAGE);\n            return;\n        }\n        if (!importFonts()) {\n            JOptionPane.showMessageDialog(this,\n                    \"Font copy error.\",\n                    \"Font import result.\", JOptionPane.INFORMATION_MESSAGE);\n            return;\n        }\n        if (!importPalettes()) {\n            JOptionPane.showMessageDialog(this,\n                    \"Palette copy error.\",\n                    \"Palette import result.\", JOptionPane.INFORMATION_MESSAGE);\n            return;\n        }\n\n        document.setRomImage(remoteRomImage);\n        localRomImage = remoteRomImage;\n\n        JOptionPane.showMessageDialog(this,\n                \"Upgraded to \" + localVersion() + \" successfully!\",\n                \"ROM upgrade OK!\",\n                JOptionPane.INFORMATION_MESSAGE);\n\n        dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));\n    }\n\n    private boolean importPalettes() {\n        boolean isOk = false;\n        RandomAccessFile otherOpenRom = null;\n        try {\n            byte[] otherRomImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n            otherOpenRom = new RandomAccessFile(localRomFile, \"r\");\n            byte[] romImage = remoteRomImage;\n\n            otherOpenRom.readFully(otherRomImage);\n            otherOpenRom.close();\n\n            if (!RomUtilities.validatePaletteData(remoteRomImage)) {\n                throw new Exception(\"Could not read palette data from remote ROM image!\");\n            }\n            if (!RomUtilities.validatePaletteData(otherRomImage)) {\n                throw new Exception(\"Could not read palette data from local ROM image!\");\n            }\n\n            int ownPaletteOffset = RomUtilities.findPaletteOffset(romImage);\n            int ownPaletteNameOffset = RomUtilities.findPaletteNameOffset(romImage);\n\n            int otherPaletteOffset = RomUtilities.findPaletteOffset(otherRomImage);\n            int otherPaletteNameOffset = RomUtilities.findPaletteNameOffset(otherRomImage);\n\n            if (RomUtilities.getNumberOfPalettes(otherRomImage) > RomUtilities.getNumberOfPalettes(romImage))\n            {\n                throw new Exception(\"Current file doesn't have enough palette slots to get the palettes imported to.\");\n            }\n\n            System.arraycopy(otherRomImage, otherPaletteOffset, romImage, ownPaletteOffset, RomUtilities.PALETTE_SIZE * RomUtilities.getNumberOfPalettes(otherRomImage));\n            System.arraycopy(otherRomImage, otherPaletteNameOffset, romImage, ownPaletteNameOffset, RomUtilities.PALETTE_NAME_SIZE * RomUtilities.getNumberOfPalettes(otherRomImage));\n\n            isOk = true;\n        } catch (Exception e) {\n            JOptionPane.showMessageDialog(this, e.getMessage(), \"File error\",\n                    JOptionPane.ERROR_MESSAGE);\n        } finally {\n            if (otherOpenRom != null) {\n                try {\n                    otherOpenRom.close();\n                } catch (IOException e) {\n                    JOptionPane.showMessageDialog(this, e.getMessage(), \"File error (wth)\",\n                            JOptionPane.ERROR_MESSAGE);\n                }\n            }\n        }\n        return isOk;\n    }\n\n    private boolean importFonts() {\n        boolean isOk = false;\n        RandomAccessFile otherOpenRom = null;\n        try {\n            byte[] otherRomImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n            otherOpenRom = new RandomAccessFile(localRomFile, \"r\");\n            byte[] romImage = remoteRomImage;\n\n            otherOpenRom.readFully(otherRomImage);\n            otherOpenRom.close();\n\n            int ownFontOffset = RomUtilities.findFontOffset(romImage);\n            int otherFontOffset = RomUtilities.findFontOffset(otherRomImage);\n\n            System.arraycopy(otherRomImage, otherFontOffset, romImage, ownFontOffset, LSDJFont.FONT_SIZE * LSDJFont.FONT_COUNT);\n\n            int ownGfxOffset = RomUtilities.findGfxFontOffset(romImage);\n            int otherGfxOffset = RomUtilities.findGfxFontOffset(otherRomImage);\n            System.arraycopy(otherRomImage, otherGfxOffset, romImage, ownGfxOffset, LSDJFont.GFX_SIZE);\n\n            for (int i = 0; i < LSDJFont.FONT_COUNT; ++i) {\n                RomUtilities.setFontName(romImage, i, RomUtilities.getFontName(otherRomImage, i));\n            }\n\n            isOk = true;\n        } catch (Exception e) {\n            JOptionPane.showMessageDialog(this, e.getMessage(), \"File error\",\n                    JOptionPane.ERROR_MESSAGE);\n        } finally {\n            if (otherOpenRom != null) {\n                try {\n                    otherOpenRom.close();\n                } catch (IOException e) {\n                    JOptionPane.showMessageDialog(this, e.getMessage(), \"File error (wth)\",\n                            JOptionPane.ERROR_MESSAGE);\n                }\n            }\n        }\n        return isOk;\n    }\n\n    private boolean isKitBank(int a_bank) {\n        int l_offset = (a_bank) * RomUtilities.BANK_SIZE;\n        byte l_char_1 = remoteRomImage[l_offset++];\n        byte l_char_2 = remoteRomImage[l_offset];\n        return (l_char_1 == 0x60 && l_char_2 == 0x40);\n    }\n\n    private boolean isEmptyKitBank(int a_bank) {\n        int l_offset = (a_bank) * RomUtilities.BANK_SIZE;\n        byte l_char_1 = remoteRomImage[l_offset++];\n        byte l_char_2 = remoteRomImage[l_offset];\n        return (l_char_1 == -1 && l_char_2 == -1);\n    }\n\n    private int importKits() {\n        try {\n            int outBank = 0;\n            int copiedBankCount = 0;\n            FileInputStream in = new FileInputStream(localRomFile.getAbsolutePath());\n            while (in.available() > 0) {\n                byte[] inBuf = new byte[RomUtilities.BANK_SIZE];\n                int readBytes = in.read(inBuf);\n                assert(readBytes == inBuf.length);\n                if (inBuf[0] == 0x60 && inBuf[1] == 0x40) {\n                    //is kit bank\n                    outBank++;\n                    while (!isKitBank(outBank) && !isEmptyKitBank(outBank)) {\n                        outBank++;\n                    }\n                    int outPtr = outBank * RomUtilities.BANK_SIZE;\n                    for (int i = 0; i < RomUtilities.BANK_SIZE; i++) {\n                        remoteRomImage[outPtr++] = inBuf[i];\n                    }\n                    copiedBankCount++;\n                }\n            }\n            in.close();\n            return copiedBankCount;\n        } catch (Exception e) {\n            JOptionPane.showMessageDialog(this, e.getMessage(), \"File error\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n        return 0;\n    }\n\n    private void upgradeFromSelectedFile() {\n        try {\n            JFileChooser fileChooser = new JFileChooser();\n            fileChooser.setDialogTitle(\"Select ROM File\");\n            fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter(\"Game Boy ROM (*.gb)\", \"gb\"));\n\n            int result = fileChooser.showOpenDialog(this);\n            if (result != JFileChooser.APPROVE_OPTION) {\n                return;\n            }\n\n            File customRomFile = fileChooser.getSelectedFile();\n            if (!customRomFile.exists() || customRomFile.length() != remoteRomImage.length) {\n                JOptionPane.showMessageDialog(this,\n                        \"Invalid ROM file! File must be \" + remoteRomImage.length + \" bytes.\",\n                        \"Invalid File\",\n                        JOptionPane.ERROR_MESSAGE);\n                return;\n            }\n\n            int reply = JOptionPane.showConfirmDialog(this,\n                    \"Upgrade to \" + customRomFile.getName() + \"?\\n\\n\" +\n                    \"This will copy all kits, fonts, and palettes from\\n\" +\n                    localRomFile.getName() + \" to the selected ROM.\",\n                    \"Upgrade to Selected ROM?\",\n                    JOptionPane.YES_NO_OPTION);\n            if (reply != JOptionPane.YES_OPTION) {\n                return;\n            }\n\n            try (FileInputStream fis = new FileInputStream(customRomFile)) {\n                int bytesRead = 0;\n                while (bytesRead < remoteRomImage.length) {\n                    int read = fis.read(remoteRomImage, bytesRead, remoteRomImage.length - bytesRead);\n                    if (read == -1) {\n                        throw new IOException(\"Unexpected end of file\");\n                    }\n                    bytesRead += read;\n                }\n            }\n\n            importAll();\n        } catch (IOException e) {\n            JOptionPane.showMessageDialog(this,\n                    e.getMessage(),\n                    \"Error loading ROM\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/lsdpatch/WwwUtil.java",
    "content": "package lsdpatch;\n\nimport java.awt.*;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\n\npublic class WwwUtil {\n    static String fetchWwwPage(URL url) throws IOException {\n        InputStream is;\n        BufferedReader br;\n        String line;\n        StringBuilder lines = new StringBuilder();\n\n        is = url.openStream();\n        br = new BufferedReader(new InputStreamReader(is));\n\n        while ((line = br.readLine()) != null) {\n            lines.append(line);\n        }\n        is.close();\n        return lines.toString();\n    }\n\n    static void openInBrowser(String path) {\n        try {\n            Desktop.getDesktop().browse(new URI(path));\n        } catch (URISyntaxException | IOException e) {\n            e.printStackTrace();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/paletteEditor/ColorPicker.java",
    "content": "package paletteEditor;\n\nimport net.miginfocom.swing.MigLayout;\n\nimport javax.swing.*;\nimport java.awt.*;\n\npublic class ColorPicker extends JPanel implements HuePanel.Listener, SaturationBrightnessPanel.Listener {\n    interface Listener {\n        void colorChanged(int r, int g, int b);\n    }\n\n    private Listener listener;\n\n    final HuePanel huePanel;\n    final SaturationBrightnessPanel saturationBrightnessPanel;\n\n    public ColorPicker() {\n        huePanel = new HuePanel();\n        saturationBrightnessPanel = new SaturationBrightnessPanel(huePanel);\n\n        huePanel.subscribe(this);\n        saturationBrightnessPanel.subscribe(this);\n\n        setLayout(new MigLayout());\n\n        add(saturationBrightnessPanel);\n        add(huePanel, \"gap 5\");\n    }\n\n    public void setColor(RGB555 rgb) {\n        int r = rgb.r << 3;\n        int g = rgb.g << 3;\n        int b = rgb.b << 3;\n        r *= 0xff;\n        r /= 0xf8;\n        g *= 0xff;\n        g /= 0xf8;\n        b *= 0xff;\n        b /= 0xf8;\n\n        float[] hsb = new float[3];\n        Color.RGBtoHSB(r, g, b, hsb);\n        huePanel.setHue(hsb[0]);\n        saturationBrightnessPanel.setSaturationBrightness(hsb[1], hsb[2]);\n        saturationBrightnessPanel.printRGB555(rgb);\n    }\n\n    public void subscribe(Listener listener) {\n        this.listener = listener;\n    }\n\n    private void broadcastColor() {\n        float hue = huePanel.hue();\n        float saturation = saturationBrightnessPanel.saturation();\n        float brightness = saturationBrightnessPanel.brightness();\n\n        int rgb = Color.HSBtoRGB(hue, saturation, brightness);\n        byte b = (byte)((rgb & 255) >> 3);\n        rgb >>= 8;\n        byte g = (byte)((rgb & 255) >> 3);\n        rgb >>= 8;\n        byte r = (byte)((rgb & 255) >> 3);\n        if (listener != null) {\n            listener.colorChanged(r, g, b);\n        }\n    }\n\n    @Override\n    public void hueChanged() {\n        broadcastColor();\n    }\n\n    @Override\n    public void saturationBrightnessChanged() {\n        broadcastColor();\n    }\n}\n"
  },
  {
    "path": "src/main/java/paletteEditor/ColorUtil.java",
    "content": "package paletteEditor;\n\nimport static java.lang.Math.pow;\nimport static java.lang.Math.round;\n\npublic class ColorUtil {\n    private static final int[] scaleChannelWithCurve = {\n            0, 6, 12, 20, 28, 36, 45, 56, 66, 76, 88, 100, 113, 125, 137, 149, 161, 172,\n            182, 192, 202, 210, 218, 225, 232, 238, 243, 247, 250, 252, 254, 255\n    };\n\n    enum ColorSpace {\n        Emulator,\n        Reality,\n        Raw\n    }\n    static public ColorSpace colorSpace = ColorSpace.Emulator;\n\n    public static void setColorSpace(ColorSpace colorSpace_) {\n        colorSpace = colorSpace_;\n    }\n\n    public static int to8bit(int color) {\n        assert(color >= 0);\n        assert(color < 32);\n        color <<= 3;\n        color *= 0xff;\n        return color / 0xf8;\n    }\n\n    // From Sameboy.\n    public static int colorCorrect(java.awt.Color c) {\n        return colorCorrect(c.getRed(), c.getGreen(), c.getBlue());\n    }\n\n    public static int colorCorrect(int r, int g, int b) {\n        if (colorSpace == ColorSpace.Raw) {\n            r = (((r >> 3) << 3) * 0xff) / 0xf8;\n            g = (((g >> 3) << 3) * 0xff) / 0xf8;\n            b = (((b >> 3) << 3) * 0xff) / 0xf8;\n            return (r << 16) | (g << 8) | b;\n        }\n\n        r >>= 3;\n        g >>= 3;\n        b >>= 3;\n\n        r = scaleChannelWithCurve[r];\n        g = scaleChannelWithCurve[g];\n        b = scaleChannelWithCurve[b];\n\n        double gamma = 2.2;\n        int new_g = (int)round(pow((pow(g / 255.0, gamma) * 3 + pow(b / 255.0, gamma)) / 4, 1 / gamma) * 255);\n        int new_r = r;\n        int new_b = b;\n\n        if (colorSpace == ColorSpace.Reality) {\n            // r = new_r;\n            g = new_g;\n            // b = new_b;\n\n            new_r = new_r * 15 / 16 + (g + b) / 32;\n            new_g = new_g * 15 / 16 + (r + b) / 32;\n            new_b = new_b * 15 / 16 + (r + g) / 32;\n\n            new_r = new_r * (162 - 45) / 255 + 45;\n            new_g = new_g * (167 - 41) / 255 + 41;\n            new_b = new_b * (157 - 38) / 255 + 38;\n        }\n\n        return (new_r << 16) | (new_g << 8) | new_b;\n    }\n}"
  },
  {
    "path": "src/main/java/paletteEditor/HuePanel.java",
    "content": "package paletteEditor;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.awt.event.MouseMotionListener;\nimport java.awt.image.BufferedImage;\nimport java.util.LinkedList;\n\nclass HuePanel extends JPanel implements MouseListener, MouseMotionListener {\n    int selectedPosition;\n    final int width = 24;\n    final int height = 244;\n    private final LinkedList<Listener> listeners = new LinkedList<>();\n\n    public interface Listener {\n        void hueChanged();\n    }\n\n    HuePanel() {\n        setPreferredSize(new Dimension(width, height));\n        addMouseListener(this);\n        addMouseMotionListener(this);\n    }\n\n    void setHue(float hue) {\n        assert(hue >= 0);\n        assert(hue <= 1);\n        selectedPosition = (int) (hue * height);\n        repaint();\n    }\n\n    @Override\n    public void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);\n        for (int y = 0; y < height; ++y) {\n            Color color = Color.getHSBColor((float) y / height, 1, 1);\n            color = new Color(ColorUtil.colorCorrect(color));\n            for (int x = 0; x < width; ++x) {\n                image.setRGB(x, y, color.getRGB());\n            }\n        }\n        g.drawImage(image, 0, 0, null);\n\n        Graphics2D g2d = (Graphics2D) g;\n        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n        g2d.setColor(Color.BLACK);\n        int r = 7;\n        int w = 2;\n        g2d.setStroke(new BasicStroke(w));\n        g2d.drawOval(width / 2 - r, selectedPosition - r, 2 * r, 2 * r);\n        r -= w;\n        g2d.setColor(Color.WHITE);\n        g2d.drawOval(width / 2 - r, selectedPosition - r, 2 * r, 2 * r);\n    }\n\n    public float hue() {\n        float hue = selectedPosition;\n        hue /= height;\n        assert(hue >= 0);\n        assert(hue <= 1);\n        return hue;\n    }\n\n    boolean mousePressed;\n\n    @Override\n    public void mouseClicked(MouseEvent e) {\n    }\n\n    @Override\n    public void mousePressed(MouseEvent e) {\n        mousePressed = true;\n        mouseDragged(e);\n    }\n\n    @Override\n    public void mouseReleased(MouseEvent e) {\n        mousePressed = false;\n    }\n\n    @Override\n    public void mouseEntered(MouseEvent e) {\n    }\n\n    @Override\n    public void mouseExited(MouseEvent e) {\n    }\n\n    @Override\n    public void mouseDragged(MouseEvent e) {\n        selectedPosition = Math.max(0, Math.min(height, e.getY()));\n        for (Listener listener : listeners) {\n            listener.hueChanged();\n        }\n        repaint();\n    }\n\n    @Override\n    public void mouseMoved(MouseEvent e) {\n    }\n\n    public void subscribe(Listener listener) {\n        listeners.add(listener);\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/paletteEditor/PaletteEditor.java",
    "content": "package paletteEditor;\n\nimport java.awt.*;\n\nimport java.awt.color.ColorSpace;\nimport java.awt.event.*;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.util.Objects;\n\nimport Document.Document;\nimport net.miginfocom.swing.MigLayout;\nimport utils.FileDialogLauncher;\nimport utils.RomUtilities;\nimport utils.StretchIcon;\n\nimport javax.swing.*;\n\npublic class PaletteEditor extends JFrame implements SwatchPair.Listener {\n    private byte[] romImage = null;\n    private int paletteOffset = -1;\n    private int nameOffset = -1;\n    private final int previewScale = 2;\n    java.io.File clipboard;\n    private boolean desaturate = false;\n\n    private final JLabel songScreenShot = new JLabel();\n    private final JLabel instrScreenShot = new JLabel();\n\n    private final ColorPicker colorPicker = new ColorPicker();\n\n    private final SwatchPanel swatchPanel = new SwatchPanel();\n\n    private final JComboBox<String> paletteSelector;\n\n    JMenuItem menuItemPaste;\n\n    private BufferedImage songImage;\n    private BufferedImage instrImage;\n\n    private int lastSelectedPaletteIndex = -1;\n\n    private boolean updatingSwatches = false;\n    private boolean populatingPaletteSelector = false;\n\n    private void setupMenuBar() {\n        JMenuBar menuBar = new JMenuBar();\n        setJMenuBar(menuBar);\n\n        JMenu mnFile = new JMenu(\"File\");\n        mnFile.setMnemonic(KeyEvent.VK_F);\n        menuBar.add(mnFile);\n\n        JMenuItem openMenuItem = new JMenuItem(\"Open...\");\n        openMenuItem.setMnemonic(KeyEvent.VK_O);\n        openMenuItem.addActionListener(e -> showOpenDialog());\n        mnFile.add(openMenuItem);\n\n        JMenuItem saveMenuItem = new JMenuItem(\"Save...\");\n        saveMenuItem.setMnemonic(KeyEvent.VK_S);\n        saveMenuItem.addActionListener(e -> showSaveDialog());\n        mnFile.add(saveMenuItem);\n\n        JMenu mnEdit = new JMenu(\"Edit\");\n        mnEdit.setMnemonic(KeyEvent.VK_E);\n        menuBar.add(mnEdit);\n\n        JMenuItem menuItemCopy = new JMenuItem(\"Copy Palette\");\n        menuItemCopy.addActionListener(e -> copyPalette());\n        menuItemCopy.setMnemonic(KeyEvent.VK_C);\n        mnEdit.add(menuItemCopy);\n\n        menuItemPaste = new JMenuItem(\"Paste Palette\");\n        menuItemPaste.setMnemonic(KeyEvent.VK_P);\n        menuItemPaste.addActionListener(e -> pastePalette());\n        menuItemPaste.setEnabled(false);\n        mnEdit.add(menuItemPaste);\n    }\n\n    public PaletteEditor(JFrame parent, Document document) {\n        parent.setEnabled(false);\n\n        setupMenuBar();\n\n        setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);\n\n        setTitle(\"Palette Editor\");\n        JPanel contentPane = new JPanel();\n        setContentPane(contentPane);\n        contentPane.setLayout(new MigLayout());\n\n        JPanel midPanel = new JPanel();\n        midPanel.setLayout(new MigLayout());\n\n        JPanel topRowPanel = new JPanel();\n        topRowPanel.setLayout(new MigLayout());\n\n        paletteSelector = new JComboBox<>();\n        paletteSelector.setEditable(true);\n        paletteSelector.addActionListener(e -> onPaletteRenamed());\n        paletteSelector.addItemListener(e -> onPaletteSelected());\n        topRowPanel.add(paletteSelector);\n\n        String colorSpaceTooltip = \"Check all screen types to make sure your palette works for everyone.\";\n        JLabel colorSpaceLabel = new JLabel(\"Screen:\");\n        colorSpaceLabel.setToolTipText(colorSpaceTooltip);\n        topRowPanel.add(colorSpaceLabel);\n\n        JComboBox<String> colorSpaceBox = new JComboBox<>();\n        String ips = \"Modern LCD\";\n        String gbc = \"Game Boy Color\";\n        String gray = \"Desaturated\";\n        String raw = \"Raw sRGB\";\n        colorSpaceBox.addItem(ips);\n        colorSpaceBox.addItem(gbc);\n        colorSpaceBox.addItem(gray);\n        colorSpaceBox.addItem(raw);\n        topRowPanel.add(colorSpaceBox);\n        colorSpaceBox.setToolTipText(colorSpaceTooltip);\n        colorSpaceBox.addItemListener(e -> {\n            if (e.getItem() == raw) {\n                ColorUtil.setColorSpace(ColorUtil.ColorSpace.Raw);\n            } else if (e.getItem() == ips) {\n                ColorUtil.setColorSpace(ColorUtil.ColorSpace.Emulator);\n            } else {\n                ColorUtil.setColorSpace(ColorUtil.ColorSpace.Reality);\n            }\n            desaturate = (e.getItem() == gray);\n            updateSongAndInstrScreens();\n            colorPicker.repaint();\n            updateAllSwatches();\n        });\n\n        midPanel.add(topRowPanel, \"grow, wrap\");\n        midPanel.add(colorPicker);\n\n        swatchPanel.addListener(this);\n\n        songScreenShot.setPreferredSize(new Dimension(160 * previewScale, 144 * previewScale));\n        contentPane.add(songScreenShot);\n        songScreenShot.addMouseListener(new MouseListener() {\n            @Override\n            public void mousePressed(MouseEvent e) {\n                songImagePressed(e);\n            }\n            @Override\n            public void mouseClicked(MouseEvent e) { }\n            @Override\n            public void mouseReleased(MouseEvent e) { }\n            @Override\n            public void mouseEntered(MouseEvent e) { }\n            @Override\n            public void mouseExited(MouseEvent e) { }\n        });\n\n        contentPane.add(midPanel);\n        contentPane.add(swatchPanel);\n\n        instrScreenShot.setPreferredSize(new Dimension(160 * previewScale, 144 * previewScale));\n        instrScreenShot.addMouseListener(new MouseListener() {\n            @Override\n            public void mousePressed(MouseEvent e) {\n                instrImagePressed(e);\n            }\n            @Override\n            public void mouseClicked(MouseEvent e) {}\n            @Override\n            public void mouseReleased(MouseEvent e) {}\n            @Override\n            public void mouseEntered(MouseEvent e) {}\n            @Override\n            public void mouseExited(MouseEvent e) {}\n        });\n        contentPane.add(instrScreenShot, \"gap 10\");\n\n        try {\n            songImage = javax.imageio.ImageIO.read(Objects.requireNonNull(getClass().getResource(\"/song.bmp\")));\n            instrImage = javax.imageio.ImageIO.read(Objects.requireNonNull(getClass().getResource(\"/instr.bmp\")));\n        } catch (java.io.IOException e) {\n            e.printStackTrace();\n        }\n\n        setRomImage(document.romImage());\n        updateSongAndInstrScreens();\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                super.windowClosing(e);\n                document.setRomImage(romImage);\n                parent.setEnabled(true);\n            }\n        });\n\n        pack();\n\n        // Needs to be here for the swatchSelected callback.\n        swatchPanel.normalSwatchPair.selectBackground();\n    }\n\n    private void songImagePressed(MouseEvent e) {\n        selectColor(songImage.getRGB(e.getX() / previewScale, e.getY() / previewScale));\n    }\n\n    private void instrImagePressed(MouseEvent e) {\n        selectColor(instrImage.getRGB(e.getX() / previewScale, e.getY() / previewScale));\n    }\n\n    private void selectColor(int rgb) {\n        switch (rgb) {\n            case ScreenShotColors.NORMAL_BG:\n                swatchPanel.normalSwatchPair.selectBackground();\n                break;\n            case ScreenShotColors.NORMAL_MID:\n            case ScreenShotColors.NORMAL_FG:\n                swatchPanel.normalSwatchPair.selectForeground();\n                break;\n            case ScreenShotColors.SHADED_BG:\n                swatchPanel.shadedSwatchPair.selectBackground();\n                break;\n            case ScreenShotColors.SHADED_MID:\n            case ScreenShotColors.SHADED_FG:\n                swatchPanel.shadedSwatchPair.selectForeground();\n                break;\n            case ScreenShotColors.ALT_BG:\n                swatchPanel.alternateSwatchPair.selectBackground();\n                break;\n            case ScreenShotColors.ALT_MID:\n            case ScreenShotColors.ALT_FG:\n                swatchPanel.alternateSwatchPair.selectForeground();\n                break;\n            case ScreenShotColors.CUR_BG:\n                swatchPanel.cursorSwatchPair.selectBackground();\n                break;\n            case ScreenShotColors.CUR_MID:\n            case ScreenShotColors.CUR_FG:\n                swatchPanel.cursorSwatchPair.selectForeground();\n                break;\n            case ScreenShotColors.SCROLL_BG:\n                swatchPanel.scrollBarSwatchPair.selectBackground();\n                break;\n            case ScreenShotColors.SCROLL_MID:\n            case ScreenShotColors.SCROLL_FG:\n                swatchPanel.scrollBarSwatchPair.selectForeground();\n                break;\n        }\n    }\n\n    private void setRomImage(byte[] romImage) {\n        this.romImage = romImage;\n        paletteOffset = RomUtilities.findPaletteOffset(romImage);\n        if (paletteOffset == -1) {\n            System.err.println(\"Could not find palette offset!\");\n        }\n        nameOffset = RomUtilities.findPaletteNameOffset(romImage);\n        if (nameOffset == -1) {\n            System.err.println(\"Could not find palette name offset!\");\n        }\n        populatePaletteSelector();\n    }\n\n    private int selectedPalette() {\n        int palette = paletteSelector.getSelectedIndex();\n        assert palette >= 0;\n        assert palette < RomUtilities.getNumberOfPalettes(romImage);\n        return palette;\n    }\n\n    // Returns color scaled to 0-0xff.\n    private java.awt.Color color(int offset) {\n        // gggrrrrr 0bbbbbgg\n        int r = (romImage[offset] & 0x1f) << 3;\n        int g = ((romImage[offset + 1] & 3) << 6) | ((romImage[offset] & 0xe0) >> 2);\n        int b = (romImage[offset + 1] << 1) & 0xf8;\n        r *= 0xff;\n        g *= 0xff;\n        b *= 0xff;\n        r /= 0xf8;\n        g /= 0xf8;\n        b /= 0xf8;\n        return new java.awt.Color(r, g, b);\n    }\n\n    private int selectedPaletteOffset() {\n        return paletteOffset + selectedPalette() * RomUtilities.PALETTE_SIZE;\n    }\n\n    private void updateRomFromSwatches() {\n        swatchPanel.writeToRom(romImage, selectedPaletteOffset());\n    }\n\n    private java.awt.Color firstColor(int colorSet) {\n        assert colorSet >= 0;\n        assert colorSet < RomUtilities.NUM_COLOR_SETS;\n        int offset = selectedPaletteOffset() + colorSet * RomUtilities.COLOR_SET_SIZE;\n        return color(offset);\n    }\n\n    private java.awt.Color secondColor(int colorSet) {\n        assert colorSet >= 0;\n        assert colorSet < RomUtilities.NUM_COLOR_SETS;\n        int offset = selectedPaletteOffset() + colorSet * RomUtilities.COLOR_SET_SIZE + 3 * 2;\n        return color(offset);\n    }\n\n    private java.awt.Color midColor(int colorSet) {\n        assert colorSet >= 0;\n        assert colorSet < RomUtilities.NUM_COLOR_SETS;\n        int offset = selectedPaletteOffset() + colorSet * RomUtilities.COLOR_SET_SIZE + 2;\n        return color(offset);\n    }\n\n    private String paletteName(int palette) {\n        assert palette >= 0;\n        assert palette < RomUtilities.getNumberOfPalettes(romImage);\n        String s = \"\";\n        s += (char) romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE];\n        s += (char) romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE + 1];\n        s += (char) romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE + 2];\n        s += (char) romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE + 3];\n        return s;\n    }\n\n    private void setPaletteName(int palette, String name) {\n        if (name == null) {\n            return;\n        }\n        name = name.toUpperCase();\n        if (name.length() >= RomUtilities.PALETTE_NAME_SIZE) {\n            name = name.substring(0, RomUtilities.PALETTE_NAME_SIZE - 1);\n        } else {\n            StringBuilder nameBuilder = new StringBuilder(name);\n            while (nameBuilder.length() < RomUtilities.PALETTE_NAME_SIZE - 1) {\n                nameBuilder.append(\" \");\n            }\n            name = nameBuilder.toString();\n        }\n\n        romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE] = (byte) name.charAt(0);\n        romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE + 1] = (byte) name.charAt(1);\n        romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE + 2] = (byte) name.charAt(2);\n        romImage[nameOffset + palette * RomUtilities.PALETTE_NAME_SIZE + 3] = (byte) name.charAt(3);\n    }\n\n    private void populatePaletteSelector() {\n        populatingPaletteSelector = true;\n        paletteSelector.removeAllItems();\n        // -2 to hide the GB palettes\n        for (int i = 0; i < RomUtilities.getNumberOfPalettes(romImage); ++i) {\n            paletteSelector.addItem(paletteName(i));\n        }\n        populatingPaletteSelector = false;\n    }\n\n    private java.awt.image.BufferedImage modifyUsingPalette(java.awt.image.BufferedImage srcImage) {\n        int w = srcImage.getWidth(); int h = srcImage.getHeight();\n        java.awt.image.BufferedImage dstImage = new java.awt.image.BufferedImage(w, h, java.awt.image.BufferedImage.TYPE_INT_RGB);\n\n        int normalBg = ColorUtil.colorCorrect(firstColor(0));\n        int normalMid = ColorUtil.colorCorrect(midColor(0));\n        int normalFg = ColorUtil.colorCorrect(secondColor(0));\n        int shadedBg = ColorUtil.colorCorrect(firstColor(1));\n        int shadedMid = ColorUtil.colorCorrect(midColor(1));\n        int shadedFg = ColorUtil.colorCorrect(secondColor(1));\n        int alternateBg = ColorUtil.colorCorrect(firstColor(2));\n        int alternateMid = ColorUtil.colorCorrect(midColor(2));\n        int alternateFg = ColorUtil.colorCorrect(secondColor(2));\n        int cursorBg = ColorUtil.colorCorrect(firstColor(3));\n        int cursorMid = ColorUtil.colorCorrect(midColor(3));\n        int cursorFg = ColorUtil.colorCorrect(secondColor(3));\n        int scrollBg = ColorUtil.colorCorrect(firstColor(4));\n        int scrollMid = ColorUtil.colorCorrect(midColor(4));\n        int scrollFg = ColorUtil.colorCorrect(secondColor(4));\n\n        for (int y = 0; y < h; ++y) {\n            for (int x = 0; x < w; ++x) {\n                int rgb = srcImage.getRGB(x, y);\n                int correctedRgb;\n                switch (rgb) {\n                    case ScreenShotColors.NORMAL_BG:\n                        correctedRgb = normalBg;\n                        break;\n                    case ScreenShotColors.NORMAL_MID:\n                        correctedRgb = normalMid;\n                        break;\n                    case ScreenShotColors.NORMAL_FG:\n                        correctedRgb = normalFg;\n                        break;\n                    case ScreenShotColors.SHADED_BG:\n                        correctedRgb = shadedBg;\n                        break;\n                    case ScreenShotColors.SHADED_MID:\n                        correctedRgb = shadedMid;\n                        break;\n                    case ScreenShotColors.SHADED_FG:\n                        correctedRgb = shadedFg;\n                        break;\n                    case ScreenShotColors.ALT_BG:\n                        correctedRgb = alternateBg;\n                        break;\n                    case ScreenShotColors.ALT_MID:\n                        correctedRgb = alternateMid;\n                        break;\n                    case ScreenShotColors.ALT_FG:\n                        correctedRgb = alternateFg;\n                        break;\n                    case ScreenShotColors.CUR_BG:\n                        correctedRgb = cursorBg;\n                        break;\n                    case ScreenShotColors.CUR_MID:\n                        correctedRgb = cursorMid;\n                        break;\n                    case ScreenShotColors.CUR_FG:\n                        correctedRgb = cursorFg;\n                        break;\n                    case ScreenShotColors.SCROLL_BG:\n                        correctedRgb = scrollBg;\n                        break;\n                    case ScreenShotColors.SCROLL_MID:\n                        correctedRgb = scrollMid;\n                        break;\n                    case ScreenShotColors.SCROLL_FG:\n                        correctedRgb = scrollFg;\n                        break;\n                    default:\n                        System.err.printf(\"%x%n\", rgb);\n                        correctedRgb = 0xff00ff;\n                }\n                dstImage.setRGB(x, y, correctedRgb);\n            }\n        }\n        if (desaturate) {\n            ColorSpace colorSpace = ColorSpace.getInstance(java.awt.color.ColorSpace.CS_GRAY);\n            return new java.awt.image.ColorConvertOp(colorSpace, null).filter(dstImage, dstImage);\n        }\n        return dstImage;\n    }\n\n    private void updateSongAndInstrScreens() {\n        songScreenShot.setIcon(new StretchIcon(modifyUsingPalette(songImage)));\n        instrScreenShot.setIcon(new StretchIcon(modifyUsingPalette(instrImage)));\n    }\n\n    private void updateSwatches(int colorSetIndex, SwatchPair swatchPair) {\n        Color backgroundColor = firstColor(colorSetIndex);\n        Color foregroundColor = secondColor(colorSetIndex);\n        swatchPair.setColors(foregroundColor, backgroundColor);\n    }\n\n    private void updateAllSwatches() {\n        updatingSwatches = true;\n        updateSwatches(0, swatchPanel.normalSwatchPair);\n        updateSwatches(1, swatchPanel.shadedSwatchPair);\n        updateSwatches(2, swatchPanel.alternateSwatchPair);\n        updateSwatches(3, swatchPanel.cursorSwatchPair);\n        updateSwatches(4, swatchPanel.scrollBarSwatchPair);\n        updatingSwatches = false;\n        swatchChanged();\n    }\n\n    private Swatch selectedSwatch;\n\n    @Override\n    public void swatchSelected(Swatch swatch) {\n        selectedSwatch = swatch;\n        colorPicker.setColor(swatch.rgb());\n        colorPicker.subscribe((r, g, b) -> {\n                    updatingSwatches = true;\n                    swatch.setRGB(r, g, b);\n                    updateRomFromSwatches();\n                    updateSongAndInstrScreens();\n                    updatingSwatches = false;\n                });\n    }\n\n    @Override\n    public void swatchChanged() {\n        if (!updatingSwatches) {\n            updateRomFromSwatches();\n            updateSongAndInstrScreens();\n            if (selectedSwatch != null) {\n                colorPicker.setColor(selectedSwatch.rgb());\n            }\n        }\n    }\n\n    private void savePalette(String path) {\n        Object selectedItem = paletteSelector.getSelectedItem();\n        if (selectedItem == null) {\n            javax.swing.JOptionPane.showMessageDialog(this, \"Couldn't read the palette name.\");\n            return;\n        }\n        String paletteName = (String) selectedItem;\n        assert paletteName.length() == 4;\n        try {\n            java.io.FileOutputStream f = new java.io.FileOutputStream(path);\n            f.write(paletteName.charAt(0));\n            f.write(paletteName.charAt(1));\n            f.write(paletteName.charAt(2));\n            f.write(paletteName.charAt(3));\n            for (int i = selectedPaletteOffset(); i < selectedPaletteOffset() + RomUtilities.PALETTE_SIZE; ++i) {\n                f.write(romImage[i]);\n            }\n            f.close();\n        } catch (java.io.IOException e) {\n            javax.swing.JOptionPane.showMessageDialog(this, \"Save failed!\");\n        }\n    }\n\n    private void loadPalette(java.io.File file) {\n        String name = \"\";\n        try {\n            java.io.RandomAccessFile f = new java.io.RandomAccessFile(file, \"r\");\n            name += (char) f.read();\n            name += (char) f.read();\n            name += (char) f.read();\n            name += (char) f.read();\n            setPaletteName(paletteSelector.getSelectedIndex(), name);\n            for (int i = selectedPaletteOffset(); i < selectedPaletteOffset() + RomUtilities.PALETTE_SIZE; ++i) {\n                romImage[i] = (byte) f.read();\n            }\n            f.close();\n        } catch (java.io.IOException e) {\n            javax.swing.JOptionPane.showMessageDialog(this, \"Load failed!\");\n        }\n        int index = paletteSelector.getSelectedIndex();\n        while (areDuplicateNames()) {\n            addNumberToPaletteName(index);\n        }\n        populatePaletteSelector();\n        paletteSelector.setSelectedIndex(index);\n    }\n\n    private void showOpenDialog() {\n        File f = FileDialogLauncher.load(this, \"Load Palette\", \"lsdpal\");\n        if (f != null) {\n            loadPalette(f);\n        }\n    }\n\n    private void showSaveDialog() {\n        File f = FileDialogLauncher.save(this, \"Save Palette\", \"lsdpal\");\n        if (f != null) {\n            savePalette(f.toString());\n        }\n    }\n\n    private void copyPalette() {\n\t    try {\n\t\t    clipboard = java.io.File.createTempFile(\"lsdpatcher\", \"palette\");\n\t    } catch (Exception e) {\n\t\t    e.printStackTrace();\n\t    }\n\t    savePalette(clipboard.getAbsolutePath());\n\t    menuItemPaste.setEnabled(true);\n    }\n\n    private boolean areDuplicateNames() {\n\t    int paletteCount = RomUtilities.getNumberOfPalettes(romImage);\n\t    for (int i = 0; i < paletteCount; ++i) {\n\t\t    for (int j = i + 1; j < paletteCount; ++j) {\n\t\t\t    if (paletteName(i).equals(paletteName(j))) {\n\t\t\t\t    return true;\n\t\t\t    }\n\t\t    }\n\t    }\n\t    return false;\n    }\n\n    private void addNumberToPaletteName(int paletteIndex) {\n\t    char[] name = paletteName(paletteIndex).toCharArray();\n\t    char lastChar = name[name.length - 1];\n\t    if (Character.isDigit(lastChar)) {\n\t\t    ++lastChar;\n\t    } else {\n\t\t    lastChar = '1';\n\t    }\n\t    name[name.length - 1] = lastChar;\n\t    setPaletteName(paletteIndex, new String(name));\n    }\n\n    private void onPaletteSelected() {\n        if (paletteSelector.getSelectedIndex() != -1) {\n            lastSelectedPaletteIndex = paletteSelector.getSelectedIndex();\n            updateSongAndInstrScreens();\n            updateAllSwatches();\n        }\n    }\n\n    private void onPaletteRenamed() {\n        if (paletteSelector.getSelectedIndex() == -1) {\n            setPaletteName(lastSelectedPaletteIndex, (String)paletteSelector.getSelectedItem());\n            if (!populatingPaletteSelector) {\n                populatePaletteSelector();\n                paletteSelector.setSelectedIndex(lastSelectedPaletteIndex);\n            }\n        }\n    }\n\n\tprivate void pastePalette() {\n\t\tint paletteIndex = paletteSelector.getSelectedIndex();\n\t\tloadPalette(clipboard);\n\t\twhile (areDuplicateNames()) {\n\t\t\taddNumberToPaletteName(paletteIndex);\n\t\t}\n\t\tpopulatePaletteSelector();\n\t\tpaletteSelector.setSelectedIndex(paletteIndex);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/paletteEditor/RGB555.java",
    "content": "package paletteEditor;\n\npublic class RGB555 {\n    int r;\n    int g;\n    int b;\n\n    public RGB555() {\n        r = -1;\n        g = -1;\n        b = -1;\n    }\n\n    public RGB555(int r, int g, int b) {\n        setR(r);\n        setG(g);\n        setB(b);\n    }\n\n    public void setR(int r) {\n        assert(r >= 0 && r < 32);\n        this.r = r;\n    }\n\n    public void setG(int g) {\n        assert(g >= 0 && g < 32);\n        this.g = g;\n    }\n\n    public void setB(int b) {\n        assert(b >= 0 && b < 32);\n        this.b = b;\n    }\n\n    public int r() {\n        return r;\n    }\n\n    public int g() {\n        return g;\n    }\n\n    public int b() {\n        return b;\n    }\n}"
  },
  {
    "path": "src/main/java/paletteEditor/SaturationBrightnessPanel.java",
    "content": "package paletteEditor;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseListener;\nimport java.awt.event.MouseMotionListener;\nimport java.awt.image.BufferedImage;\n\npublic class SaturationBrightnessPanel extends JPanel implements HuePanel.Listener, MouseListener, MouseMotionListener {\n    private RGB555 rgb555 = null;\n    public void printRGB555(RGB555 rgb555) {\n        this.rgb555 = rgb555;\n    }\n\n    interface Listener {\n        void saturationBrightnessChanged();\n    }\n\n    private Listener listener;\n\n    final Point selection = new Point();\n\n    HuePanel huePanel;\n\n    boolean mousePressed;\n\n    public SaturationBrightnessPanel(HuePanel huePanel) {\n        this.huePanel = huePanel;\n        huePanel.subscribe(this);\n        setPreferredSize(new Dimension(254, 244));\n        addMouseListener(this);\n        addMouseMotionListener(this);\n    }\n\n    public void setSaturationBrightness(float saturation, float brightness) {\n        assert(saturation >= 0);\n        assert(saturation <= 1);\n        assert(brightness >= 0);\n        assert(brightness <= 1);\n        selection.setLocation(saturation * getWidth(), (1 - brightness) * getHeight());\n        repaint();\n    }\n\n    public void subscribe(Listener listener) {\n        this.listener = listener;\n    }\n\n    @Override\n    public void hueChanged() {\n        repaint();\n    }\n\n    public float saturation() {\n        return (float) selection.getX() / getWidth();\n    }\n\n    public float brightness() {\n        return 1 - (float)selection.getY() / getHeight();\n    }\n\n    @Override\n    public void paintComponent(Graphics g) {\n        super.paintComponent(g);\n        BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);\n        for (int y = 0; y < getHeight(); ++y) {\n            for (int x = 0; x < getWidth(); ++x) {\n                float s = (float) x / getWidth();\n                float b = 1 - (float) y / getHeight();\n                Color color = Color.getHSBColor(huePanel.hue(), s, b);\n                color = new Color(ColorUtil.colorCorrect(color));\n                image.setRGB(x, y, color.getRGB());\n            }\n        }\n        g.drawImage(image, 0, 0, null);\n\n        Graphics2D g2d = (Graphics2D) g;\n        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\n        int radius = 7;\n        int w = 2;\n        g2d.setColor(Color.BLACK);\n        g2d.setStroke(new BasicStroke(w));\n        g2d.drawOval((int) selection.getX() - radius,\n                (int) selection.getY() - radius,\n                2 * radius, 2 * radius);\n        g2d.setColor(Color.WHITE);\n        radius -= w;\n        g2d.drawOval((int) selection.getX() - radius,\n                (int) selection.getY() - radius,\n                2 * radius, 2 * radius);\n\n        String colorString = rgb555.r + \",\" + rgb555.g + \",\" + rgb555.b;\n        FontMetrics fm = g2d.getFontMetrics();\n        g2d.drawString(colorString,\n                getWidth() - fm.stringWidth(colorString),\n                getHeight() - fm.getDescent());\n    }\n\n    @Override\n    public void mouseClicked(MouseEvent e) {\n    }\n\n    @Override\n    public void mousePressed(MouseEvent e) {\n        mousePressed = true;\n        mouseDragged(e);\n    }\n\n    @Override\n    public void mouseReleased(MouseEvent e) {\n        mousePressed = false;\n    }\n\n    @Override\n    public void mouseEntered(MouseEvent e) {\n    }\n\n    @Override\n    public void mouseExited(MouseEvent e) {\n    }\n\n    @Override\n    public void mouseDragged(MouseEvent e) {\n        selection.x = Math.min(getWidth(), Math.max(0, e.getX()));\n        selection.y = Math.min(getHeight(), Math.max(0, e.getY()));\n        if (listener != null) {\n            listener.saturationBrightnessChanged();\n        }\n        repaint();\n    }\n\n    @Override\n    public void mouseMoved(MouseEvent e) {\n    }\n}\n"
  },
  {
    "path": "src/main/java/paletteEditor/ScreenShotColors.java",
    "content": "package paletteEditor;\n\npublic class ScreenShotColors {\n        static final public int NORMAL_BG = 0xff1a4577;\n        static final public int NORMAL_MID = 0xffb06a76;\n        static final public int NORMAL_FG = 0xfff58f77;\n        static final public int SHADED_BG = 0xffebf1fd;\n        static final public int SHADED_MID = 0xffcba9c5;\n        static final public int SHADED_FG = 0xffa64556;\n        static final public int ALT_BG = 0xff2d79bd;\n        static final public int ALT_MID = 0xff76c0c3;\n        static final public int ALT_FG = 0xffbdebd0;\n        static final public int CUR_BG = 0xff88bdf5;\n        static final public int CUR_MID = 0xff7578a8;\n        static final public int CUR_FG = 0xff6e231a;\n        static final public int SCROLL_BG = 0xffc4ecd0;\n        static final public int SCROLL_MID = 0xff7fc1c4;\n        static final public int SCROLL_FG = 0xff3a7abd;\n}\n"
  },
  {
    "path": "src/main/java/paletteEditor/Swatch.java",
    "content": "package paletteEditor;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.util.LinkedList;\nimport java.util.Random;\n\npublic class Swatch extends JPanel {\n    public RGB555 rgb() {\n        return rgb555;\n    }\n\n    public interface Listener {\n        void swatchChanged();\n    }\n    final LinkedList<Listener> listeners = new LinkedList<>();\n    private final RGB555 rgb555 = new RGB555();\n\n    public Swatch() {\n        setPreferredSize(new Dimension(50, 37));\n        setBorder(BorderFactory.createLoweredBevelBorder());\n    }\n\n    public int r() {\n        return rgb555.r();\n    }\n\n    public int g() {\n        return rgb555.g();\n    }\n\n    public int b() {\n        return rgb555.b();\n    }\n\n    public void setRGB(int r, int g, int b) {\n        boolean changed = r != rgb555.r() || g != rgb555.g() || b != rgb555.b();\n        rgb555.setR(r);\n        rgb555.setG(g);\n        rgb555.setB(b);\n        if (changed) {\n            for (Listener listener : listeners) {\n                listener.swatchChanged();\n            }\n        }\n        setBackground(new Color(ColorUtil.colorCorrect(new Color(r << 3, g << 3, b << 3))));\n    }\n\n    public void randomize(Random rand) {\n        setRGB(rand.nextInt(32), rand.nextInt(32), rand.nextInt(32));\n    }\n\n    public void addListener(Listener listener) {\n        listeners.add(listener);\n    }\n\n    public void deselect() {\n        setBorder(BorderFactory.createLoweredBevelBorder());\n    }\n}\n"
  },
  {
    "path": "src/main/java/paletteEditor/SwatchPair.java",
    "content": "package paletteEditor;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.*;\n\nclass SwatchPair implements Swatch.Listener {\n    public interface Listener {\n        void swatchSelected(Swatch swatch);\n        void swatchChanged();\n    }\n    private final LinkedList<Listener> listeners = new LinkedList<>();\n    public void addListener(Listener listener) {\n        listeners.add(listener);\n    }\n    @Override\n    public void swatchChanged() {\n        for (Listener listener : listeners) {\n            listener.swatchChanged();\n        }\n    }\n\n    public final Swatch bgSwatch = new Swatch();\n    public final Swatch fgSwatch = new Swatch();\n\n    public SwatchPair() {\n        createSwatches();\n\n        bgSwatch.addListener(this);\n        fgSwatch.addListener(this);\n    }\n\n    public void registerToPanel(JPanel panel, String entryName) {\n        panel.add(bgSwatch, \"grow\");\n        bgSwatch.setToolTipText(entryName + \" background\");\n        panel.add(fgSwatch, \"grow, wrap\");\n        fgSwatch.setToolTipText(entryName + \" text\");\n    }\n\n    public void selectBackground() {\n        select(bgSwatch);\n    }\n\n    public void selectForeground() {\n        select(fgSwatch);\n    }\n\n    private void select(Swatch swatch) {\n        for (Listener listener : listeners) {\n            listener.swatchSelected(swatch);\n        }\n    }\n\n    private void createSwatches() {\n        bgSwatch.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mousePressed(MouseEvent e) {\n                super.mousePressed(e);\n                select(bgSwatch);\n            }});\n\n        fgSwatch.addMouseListener(new MouseAdapter() {\n            @Override\n            public void mousePressed(MouseEvent e) {\n                super.mousePressed(e);\n                select(fgSwatch);\n            }});\n    }\n\n    public void setColors(Color foregroundColor, Color backgroundColor) {\n        bgSwatch.setRGB(backgroundColor.getRed() >> 3,\n                backgroundColor.getGreen() >> 3,\n                backgroundColor.getBlue() >> 3);\n        fgSwatch.setRGB(foregroundColor.getRed() >> 3,\n                foregroundColor.getGreen() >> 3,\n                foregroundColor.getBlue() >> 3);\n    }\n\n    public void randomize(Random rand) {\n        bgSwatch.randomize(rand);\n        fgSwatch.randomize(rand);\n    }\n\n    private RGB555 findMidTone(RGB555 bg, RGB555 fg) {\n        ColorUtil.ColorSpace prevColorSpace = ColorUtil.colorSpace;\n        ColorUtil.colorSpace = ColorUtil.ColorSpace.Emulator;\n        Color target = midToneTarget(bg, fg);\n        RGB555 bestRgb = findBestRgb(new RGB555(15, 15, 15), target);\n        ColorUtil.colorSpace = prevColorSpace;\n        return bestRgb;\n    }\n\n    private Color midToneTarget(RGB555 bg, RGB555 fg) {\n        int r1 = ColorUtil.to8bit(bg.r());\n        int g1 = ColorUtil.to8bit(bg.g());\n        int b1 = ColorUtil.to8bit(bg.b());\n        int r2 = ColorUtil.to8bit(fg.r());\n        int g2 = ColorUtil.to8bit(fg.g());\n        int b2 = ColorUtil.to8bit(fg.b());\n        Color bgColor = new Color(ColorUtil.colorCorrect( new Color(r1, g1, b1)));\n        Color fgColor = new Color(ColorUtil.colorCorrect( new Color(r2, g2, b2)));\n        int k = 55;\n        int midR = (bgColor.getRed() * k + fgColor.getRed() * (100 - k)) / 100;\n        int midG = (bgColor.getGreen() * k + fgColor.getGreen() * (100 - k)) / 100;\n        int midB = (bgColor.getBlue() * k + fgColor.getBlue() * (100 - k)) / 100;\n        return new Color(midR, midG, midB);\n    }\n\n    private RGB555 findBestRgb(RGB555 start, Color target) {\n        TreeMap<Double, RGB555> map = new TreeMap<>();\n        double startDiff = diff(target, start.r(), start.g(), start.b());\n        map.put(startDiff, start);\n        add(map, target, start, 1, 0, 0);\n        add(map, target, start, -1, 0, 0);\n        add(map, target, start, 0, 1, 0);\n        add(map, target, start, 0, -1, 0);\n        add(map, target, start, 0, 0, 1);\n        add(map, target, start, 0, 0, -1);\n        if (map.firstKey() == startDiff) {\n            return start;\n        }\n        return findBestRgb(map.firstEntry().getValue(), target);\n    }\n\n    private void add(TreeMap<Double, RGB555> map, Color target, RGB555 start, int rd, int gd, int bd) {\n        int r = start.r() + rd;\n        int g = start.g() + gd;\n        int b = start.b() + bd;\n        if (r < 0 || r > 31 || g < 0 || g > 31 || b < 0 || b > 31) {\n            return;\n        }\n        RGB555 rgb555 = new RGB555(start.r() + rd, start.g() + gd, start.b() + bd);\n        map.put(diff(target, r, g, b), rgb555);\n    }\n\n    private static double diff(Color target, int r, int g, int b) {\n        int rgb24 = ColorUtil.colorCorrect(\n                ColorUtil.to8bit(r),\n                ColorUtil.to8bit(g),\n                ColorUtil.to8bit(b));\n        int rr = rgb24 >> 16;\n        int gg = (rgb24 >> 8) & 0xff;\n        int bb = rgb24 & 0xff;\n\n        // red-mean\n        double rm = (rr + target.getRed()) / 2.0;\n        double rd = rr - target.getRed();\n        double gd = gg - target.getGreen();\n        double bd = bb - target.getBlue();\n\n        return Math.sqrt((2.0 + rm / 256.0) * rd * rd +\n                4.0 * gd * gd +\n                ((2.0 + (255.0 - rm) / 256.0) * bd * bd));\n    }\n\n    public void writeToRom(byte[] romImage, int offset) {\n        int r1 = bgSwatch.r();\n        int g1 = bgSwatch.g();\n        int b1 = bgSwatch.b();\n        // gggrrrrr 0bbbbbgg\n        romImage[offset] = (byte) (r1 | (g1 << 5));\n        romImage[offset + 1] = (byte) ((g1 >> 3) | (b1 << 2));\n\n        int r2 = fgSwatch.r();\n        int g2 = fgSwatch.g();\n        int b2 = fgSwatch.b();\n        romImage[offset + 6] = (byte) (r2 | (g2 << 5));\n        romImage[offset + 7] = (byte) ((g2 >> 3) | (b2 << 2));\n\n        // Mid-tone.\n        RGB555 rgbMid = findMidTone(new RGB555(r1, g1, b1), new RGB555(r2, g2, b2));\n\n        romImage[offset + 2] = (byte) (rgbMid.r() | (rgbMid.g() << 5));\n        romImage[offset + 3] = (byte) ((rgbMid.g() >> 3) | (rgbMid.b() << 2));\n        romImage[offset + 4] = romImage[offset + 2];\n        romImage[offset + 5] = romImage[offset + 3];\n    }\n\n    public void deselect() {\n        bgSwatch.deselect();\n        fgSwatch.deselect();\n    }\n}"
  },
  {
    "path": "src/main/java/paletteEditor/SwatchPanel.java",
    "content": "package paletteEditor;\n\nimport net.miginfocom.swing.MigLayout;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.util.LinkedList;\nimport java.util.Random;\n\npublic class SwatchPanel extends JPanel implements SwatchPair.Listener {\n    SwatchPair.Listener listener;\n\n    private final LinkedList<SwatchPair> swatchPairs = new LinkedList<>();\n    private final Random random = new Random();\n\n    public final SwatchPair normalSwatchPair = new SwatchPair();\n    public final SwatchPair shadedSwatchPair = new SwatchPair();\n    public final SwatchPair alternateSwatchPair = new SwatchPair();\n    public final SwatchPair cursorSwatchPair = new SwatchPair();\n    public final SwatchPair scrollBarSwatchPair = new SwatchPair();\n\n    private Swatch selectedSwatch;\n\n    public SwatchPanel() {\n        setLayout(new MigLayout());\n\n        JButton randomizeButton = new JButton(\"Randomize all\");\n        randomizeButton.addActionListener((e) -> randomize());\n        add(randomizeButton, \"grow, span, wrap\");\n\n        JButton cloneButton = new JButton(\"Clone color\");\n        cloneButton.setPreferredSize(new Dimension(0, 0));\n        cloneButton.addActionListener(e -> cloneStart());\n        add(cloneButton, \"grow, span, wrap\");\n\n        JButton swapButton = new JButton(\"Swap color\");\n        swapButton.setPreferredSize(new Dimension(0, 0));\n        swapButton.addActionListener(e -> swapStart());\n        add(swapButton, \"grow, span, wrap\");\n\n        add(normalSwatchPair, \"Normal\");\n        add(shadedSwatchPair, \"Shaded\");\n        add(alternateSwatchPair, \"Alternate\");\n        add(cursorSwatchPair, \"Cursor\");\n        add(scrollBarSwatchPair, \"Scroll Bar\");\n    }\n\n    enum CommandState {\n        OFF,\n        SWAP,\n        CLONE\n    }\n    CommandState commandState;\n    private void swapStart() {\n        if (selectedSwatch == null) {\n            return;\n        }\n        commandState = CommandState.SWAP;\n        updateCursor();\n    }\n    private void cloneStart() {\n        if (selectedSwatch == null) {\n            return;\n        }\n        commandState = CommandState.CLONE;\n        updateCursor();\n    }\n\n    private void updateCursor() {\n        setCursor(new Cursor(commandState == CommandState.OFF\n                ? Cursor.DEFAULT_CURSOR\n                : Cursor.HAND_CURSOR));\n    }\n\n    public void addListener(SwatchPair.Listener listener) {\n        this.listener = listener;\n    }\n\n    public void add(SwatchPair swatchPair, String swatchPairName) {\n        swatchPair.registerToPanel(this, swatchPairName);\n        swatchPairs.add(swatchPair);\n        swatchPair.addListener(this);\n    }\n\n    public void randomize() {\n        for (SwatchPair swatchPair : swatchPairs) {\n            swatchPair.randomize(random);\n        }\n    }\n\n    private void handleClone(Swatch swatch) {\n        if (commandState != CommandState.CLONE) {\n            return;\n        }\n        swatch.setRGB(selectedSwatch.r(), selectedSwatch.g(), selectedSwatch.b());\n        commandState = CommandState.OFF;\n        updateCursor();\n    }\n\n    private void handleSwap(Swatch swatch) {\n        if (commandState != CommandState.SWAP) {\n            return;\n        }\n        int r = swatch.r();\n        int g = swatch.g();\n        int b = swatch.b();\n        swatch.setRGB(selectedSwatch.r(), selectedSwatch.g(), selectedSwatch.b());\n        selectedSwatch.setRGB(r, g, b);\n        commandState = CommandState.OFF;\n        updateCursor();\n    }\n\n    @Override\n    public void swatchSelected(Swatch swatch) {\n        handleSwap(swatch);\n        handleClone(swatch);\n        selectedSwatch = swatch;\n        for (SwatchPair swatchPair : swatchPairs) {\n            swatchPair.deselect();\n        }\n        int w = 2;\n        swatch.setBorder(BorderFactory.createCompoundBorder(\n                BorderFactory.createMatteBorder(w, w, w, w, Color.BLACK),\n                BorderFactory.createMatteBorder(w, w, w, w, Color.WHITE)));\n        if (listener != null) {\n            listener.swatchSelected(swatch);\n        }\n    }\n\n    @Override\n    public void swatchChanged() {\n        if (listener != null) {\n            listener.swatchChanged();\n        }\n    }\n\n    public void writeToRom(byte[] romImage, int selectedPaletteOffset) {\n        normalSwatchPair.writeToRom(romImage, selectedPaletteOffset);\n        shadedSwatchPair.writeToRom(romImage, selectedPaletteOffset + 8);\n        alternateSwatchPair.writeToRom(romImage, selectedPaletteOffset + 16);\n        cursorSwatchPair.writeToRom(romImage, selectedPaletteOffset + 24);\n        scrollBarSwatchPair.writeToRom(romImage, selectedPaletteOffset + 32);\n    }\n}\n"
  },
  {
    "path": "src/main/java/songManager/SongManager.java",
    "content": "package songManager;\n\nimport Document.Document;\nimport Document.LSDSavFile;\nimport net.miginfocom.swing.MigLayout;\nimport utils.EditorPreferences;\nimport utils.FileDialogLauncher;\n\nimport java.awt.*;\nimport javax.swing.JButton;\nimport javax.swing.JList;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.io.*;\n\nimport javax.swing.event.ListSelectionListener;\nimport javax.swing.event.ListSelectionEvent;\nimport javax.swing.*;\n\npublic class SongManager extends JFrame implements ListSelectionListener {\n    JButton addLsdSngButton = new JButton();\n    JButton clearSlotButton = new JButton();\n    JButton exportLsdSngButton = new JButton();\n    JProgressBar jRamUsageIndicator = new JProgressBar();\n    JList<String> songList = new JList<>( new String[] { \" \" } );\n    JScrollPane songs = new JScrollPane(songList);\n\n    byte[] romImage;\n\n    LSDSavFile savFile;\n    \n    public SongManager(JFrame parent, Document document) {\n        parent.setEnabled(false);\n\n        romImage = document.romImage();\n        savFile = document.savFile();\n\n        addLsdSngButton.setText(\"Add songs...\");\n        addLsdSngButton.addActionListener(e -> addLsdSngButton_actionPerformed());\n        clearSlotButton.setEnabled(false);\n        clearSlotButton.setText(\"Remove songs\");\n        clearSlotButton.addActionListener(e -> clearSlotButton_actionPerformed());\n        exportLsdSngButton.setEnabled(false);\n        exportLsdSngButton.setToolTipText(\"Export song to .lsdprj\");\n        exportLsdSngButton.setText(\"Export songs...\");\n        exportLsdSngButton.addActionListener(e -> exportLsdSngButton_actionPerformed());\n        songList.addListSelectionListener(this);\n\n        jRamUsageIndicator.setString(\"\");\n        jRamUsageIndicator.setStringPainted(true);\n\n        this.setResizable(false);\n        this.setTitle(\"Song Manager\");\n\n        songs.setMinimumSize(new Dimension(0, 180));\n\n        java.awt.Container panel = this.getContentPane();\n        MigLayout layout = new MigLayout(\"wrap\", \"[]8[]\");\n        panel.setLayout(layout);\n        panel.add(songs, \"cell 0 0 1 6, growx, growy\");\n        panel.add(jRamUsageIndicator, \"cell 0 6 1 1, growx\");\n        panel.add(addLsdSngButton, \"cell 1 0 1 1, growx\");\n        panel.add(exportLsdSngButton, \"cell 1 1 1 1, growx\");\n        panel.add(clearSlotButton, \"cell 1 2 1 1, growx, gaptop 10, aligny top\");\n\n        pack();\n        setVisible(true);\n\n        addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosing(WindowEvent e) {\n                super.windowClosing(e);\n                document.setSavFile(savFile);\n                document.setRomImage(romImage);\n                parent.setEnabled(true);\n            }\n        });\n\n        savFile.populateSongList(songList);\n        updateRamUsageIndicator();\n    }\n\n    public void clearSlotButton_actionPerformed() {\n        if (songList.isSelectionEmpty()) {\n            JOptionPane.showMessageDialog(this, \"Please select a song!\",\n                    \"No song selected!\", JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n        int[] songs = songList.getSelectedIndices();\n        \n        for (int song : songs)\n            savFile.clearSong(song);\n        savFile.populateSongList(songList);\n        updateRamUsageIndicator();\n    }\n\n    private void updateRamUsageIndicator() {\n        jRamUsageIndicator.setMaximum(savFile.totalBlockCount());\n        jRamUsageIndicator.setValue(savFile.usedBlockCount());\n        jRamUsageIndicator.setString(\"File mem. used: \"\n                + savFile.usedBlockCount() + \"/\" + savFile.totalBlockCount());\n    }\n\n    public void exportLsdSngButton_actionPerformed() {\n        if (songList.isSelectionEmpty()) {\n            JOptionPane.showMessageDialog(this, \"Please select a song!\",\n                    \"No song selected!\", JOptionPane.ERROR_MESSAGE);\n            return;\n        }\n        \n        int[] songs = songList.getSelectedIndices();\n\n        if (songs.length == 1) {\n            File f = FileDialogLauncher.save(this, \"Export Song\", \"lsdprj\");\n            if (f == null) {\n                return;\n            }\n            savFile.exportSongToFile(songs[0], f.getAbsolutePath(), romImage);\n        } else if (songs.length > 1) {\n            JFileChooser fileChooser = new JFileChooser(EditorPreferences.lastDirectory(\"lsdprj\"));\n            fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);\n            fileChooser.setDialogTitle(\n                    \"Batch export selected songs to .lsdprj files\");\n            int ret_val = fileChooser.showDialog(null, \"Choose Directory\");\n            \n            if (JFileChooser.APPROVE_OPTION == ret_val) {\n                String directory = fileChooser.getSelectedFile().getAbsolutePath();\n\n                for (int song : songs) {\n                    String filename = savFile.getFileName(song).toLowerCase()\n                            + \"-\" + savFile.version(song) + \".lsdprj\";\n                    String path = directory + File.separator + filename;\n                    String[] options = { \"Yes\", \"No\", \"Cancel\" };\n                    File f = new File(path);\n                    if (f.exists()) {\n                        int overWrite = JOptionPane.showOptionDialog(\n                                this, \"File \\\"\" \n                                + filename\n                                + \"\\\" already exists.\\n\"\n                                + \"Overwrite existing file?\", \"Warning\",\n                                JOptionPane.YES_NO_CANCEL_OPTION,\n                                JOptionPane.WARNING_MESSAGE, null, options,\n                                options[1]);\n\n                        if (overWrite == JOptionPane.YES_OPTION) {\n                            boolean deleted;\n                            try {\n                                deleted = f.delete();\n                            } catch (Exception fileInUse) {\n                                deleted = false;\n                            }\n                            if (!deleted) {\n                                JOptionPane.showMessageDialog(this,\n                                        \"Could not delete file.\");\n                                continue;\n                            }\n                        } else if (overWrite == JOptionPane.NO_OPTION) {\n                            continue;\n                        } else if (overWrite == JOptionPane.CANCEL_OPTION)\n                            return;\n                    }\n                    if (savFile.getBlocksUsed(song) > 0) {\n                        savFile.exportSongToFile(song, path, romImage);\n                    }\n                }\n            }\n        }\n    }\n\n    public void addLsdSngButton_actionPerformed() {\n        FileDialog fileDialog = new FileDialog(this,\n                \"Load Songs\",\n                FileDialog.LOAD);\n        fileDialog.setDirectory(EditorPreferences.lastDirectory(\"lsdprj\"));\n        fileDialog.setFile(\"*.lsdsng;*.lsdprj\");\n        fileDialog.setMultipleMode(true);\n        fileDialog.setVisible(true);\n\n        File[] files = fileDialog.getFiles();\n        if (files.length == 0) {\n            return;\n        }\n\n        try {\n            for (File f : files) {\n                if (f.getName().toLowerCase().endsWith(\".lsdsng\") ||\n                        f.getName().toLowerCase().endsWith(\".lsdprj\")) {\n                    savFile.addSongFromFile(f.getAbsoluteFile().toString(), romImage);\n                    EditorPreferences.setLastPath(\"lsdprj\", f.getAbsolutePath());\n                } else {\n                    JOptionPane.showMessageDialog(this,\n                            \"Unknown file extension: \" + f.getName(),\n                            \"Song add failed\",\n                            JOptionPane.ERROR_MESSAGE);\n                }\n            }\n        } catch (Exception e) {\n            JOptionPane.showMessageDialog(this,\n                    e.getMessage(),\n                    \"Song add failed\",\n                    JOptionPane.ERROR_MESSAGE);\n        }\n        savFile.populateSongList(songList);\n        updateRamUsageIndicator();\n    }\n\n    @Override\n    public void valueChanged(ListSelectionEvent e) {\n        boolean enable = !songList.isSelectionEmpty();\n        clearSlotButton.setEnabled(enable);\n\n        int[] songs = songList.getSelectedIndices();\n        if (songs.length == 1) {\n            enable = savFile.isValid(songs[0]);\n        }\n        exportLsdSngButton.setEnabled(enable);\n    }\n}\n"
  },
  {
    "path": "src/main/java/structures/LSDJFont.java",
    "content": "package structures;\n\nimport java.awt.Color;\nimport java.awt.image.BufferedImage;\n\n/**\n * Helper class to access and manipulate font data.\n * This class acts as a span over data owned elsewhere and acts in it as it was its own.\n * @author Eiyeron\n */\npublic class LSDJFont extends ROMDataManipulator {\n    public static final int TILE_COUNT = 71;\n    public static final int GFX_TILE_COUNT = 46;\n    public static final int FONT_NUM_TILES_X = 8;\n    public static final int FONT_NUM_TILES_Y = (int)Math.ceil(TILE_COUNT/ (float)FONT_NUM_TILES_X);\n    public static final int GFX_FONT_NUM_TILES_Y = (int)Math.ceil((TILE_COUNT + GFX_TILE_COUNT)/ (float)FONT_NUM_TILES_X);\n    public static final int FONT_MAP_WIDTH = FONT_NUM_TILES_X * 8;\n    public static final int FONT_MAP_HEIGHT = FONT_NUM_TILES_Y * 8;\n    public static final int GFX_FONT_MAP_HEIGHT = (GFX_FONT_NUM_TILES_Y) * 8;\n    public static final int FONT_HEADER_SIZE = 130;\n    public static final int FONT_COUNT = 3;\n    public static final int FONT_SIZE = 0xe96;\n    public static final int FONT_NAME_LENGTH = 4;\n    public static final int FONT_TILE_SIZE = 16;\n    public static final int GFX_SIZE = FONT_TILE_SIZE * GFX_TILE_COUNT;\n\n    private int gfxDataOffset = -1;\n\n    public void setGfxDataOffset(int gfxDataOffset) {\n        this.gfxDataOffset = gfxDataOffset;\n    }\n\n    private int getTileDataLocation(int index) {\n        if (index >= TILE_COUNT) {\n            index -= TILE_COUNT;\n            return getGfxTileDataLocation(index);\n        }\n        if (index < 0 || index >= TILE_COUNT)\n        {\n            // TODO exception?\n            return -1;\n        }\n        return getDataOffset() + index * FONT_TILE_SIZE;\n    }\n\n    private int getGfxTileDataLocation(int index) {\n        if (index < 0 || index >= GFX_TILE_COUNT)\n        {\n            // TODO exception?\n            return -1;\n        }\n        return gfxDataOffset + index * FONT_TILE_SIZE;\n    }\n\n    public int getPixel(int x, int y) {\n        if (x < 0 || x >= FONT_MAP_WIDTH || y < 0 || y >= GFX_FONT_MAP_HEIGHT)\n            return -1;\n\n        int tileToRead = (y / 8) * 8 + x / 8;\n        int tileOffset = getTileDataLocation(tileToRead) + (y % 8) * 2;\n        int xMask = 7 - (x % 8);\n        int value = (romImage[tileOffset] >> xMask) & 1;\n        value |= ((romImage[tileOffset + 1] >> xMask) & 1) << 1;\n        return value;\n    }\n    // - Tile data manipulation -\n    // Note : those functions only affect the normal variant tileset.\n    // In the future it might be good to either provide alternative functions\n    // or to extend them to allow editing the other variants too.\n\n    public int getTilePixel(int tile, int localX, int localY) {\n        return getPixel((tile % FONT_NUM_TILES_X) * 8 + (localX % 8), (tile / FONT_NUM_TILES_X) * 8 + (localY % 8));\n    }\n\n    private void setPixel(int x, int y, int color) {\n        assert color >= 1 && color <= 3;\n        if (x < 0 || x >= FONT_MAP_WIDTH || y < 0 || y >= GFX_FONT_MAP_HEIGHT)\n            return;\n        int localX = x % 8;\n        int localY = y % 8;\n        int tileToEdit = (y / 8) * 8 + x / 8;\n\n        int tileOffset = getTileDataLocation(tileToEdit) + localY * 2;\n        int xMask = 0x80 >> localX;\n        romImage[tileOffset] &= 0xff ^ xMask;\n        romImage[tileOffset + 1] &= 0xff ^ xMask;\n        switch (color) {\n            case 3:\n                romImage[tileOffset + 1] |= xMask;\n            case 2:\n                romImage[tileOffset] |= xMask;\n        }\n    }\n\n    public void setTilePixel(int tile, int localX, int localY, int color) {\n        setPixel((tile % FONT_NUM_TILES_X) * 8 + (localX % 8),\n                (tile / FONT_NUM_TILES_X) * 8 + (localY % 8), color);\n    }\n\n    public void rotateTileUp(int tile) {\n        int tileOffset = getTileDataLocation(tile);\n        byte line0origin1 = romImage[tileOffset];\n        byte line0origin2 = romImage[tileOffset + 1];\n        for (int i = 0; i < 8; i++) {\n            int lineTargetOffset = tileOffset + i * 2;\n            int lineOriginOffset = tileOffset + (i + 1 % 8) * 2;\n            romImage[lineTargetOffset] = romImage[lineOriginOffset];\n            romImage[lineTargetOffset + 1] = romImage[lineOriginOffset + 1];\n        }\n        romImage[tileOffset + 7 * 2] = line0origin1;\n        romImage[tileOffset + 7 * 2 + 1] = line0origin2;\n    }\n\n    public void rotateTileDown(int tile) {\n        int tileOffset = getTileDataLocation(tile);\n        byte line7origin1 = romImage[tileOffset + 7 * 2];\n        byte line7origin2 = romImage[tileOffset + 7 * 2 + 1];\n        for (int i = 7; i > 0; i--) {\n            int lineTargetOffset = tileOffset + i * 2;\n            int lineOriginOffset = tileOffset + (i - 1) * 2;\n            romImage[lineTargetOffset] = romImage[lineOriginOffset];\n            romImage[lineTargetOffset + 1] = romImage[lineOriginOffset + 1];\n        }\n        romImage[tileOffset] = line7origin1;\n        romImage[tileOffset + 1] = line7origin2;\n    }\n\n    public void rotateTileRight(int tile) {\n        int tileOffset = getTileDataLocation(tile);\n        for (int i = 0; i < FONT_TILE_SIZE; i++) {\n            byte currentByte = romImage[tileOffset + i];\n            byte shiftedByte = (byte) (((currentByte & 1) << 7) | ((currentByte >> 1) & 0x7F));\n            romImage[tileOffset + i] = shiftedByte;\n        }\n    }\n\n    public void rotateTileLeft(int tile) {\n        int tileOffset = getTileDataLocation(tile);\n        for (int i = 0; i < FONT_TILE_SIZE; i++) {\n            byte currentByte = romImage[tileOffset + i];\n            byte shiftedByte = (byte) (((currentByte & 0x80) >> 7) | (currentByte << 1));\n            romImage[tileOffset + i] = shiftedByte;\n        }\n    }\n\n\n    /**\n     * Generates the inverted and shaded font variants from the normal tileset.\n     */\n    public void generateShadedAndInvertedTiles() {\n        for (int i = 2; i < TILE_COUNT; i++) {\n            generateShadedTileVariant(i);\n            generateInvertedTileVariant(i);\n        }\n    }\n\n    public void generateInvertedTileVariant(int index) {\n        if (index < 2 || index > TILE_COUNT) {\n            // TODO exception?\n            return;\n        }\n        int sourceLocation = getTileDataLocation(index); // The two first tiles are not mirrored.\n        int invertedLocation = sourceLocation + 0x4d2;\n        for (int i = 0; i < FONT_TILE_SIZE; i += 2) {\n            romImage[invertedLocation + i] = (byte) ~romImage[sourceLocation + i + 1];\n            romImage[invertedLocation + i + 1] = (byte) ~romImage[sourceLocation + i];\n        }\n    }\n\n\n    public void generateShadedTileVariant(int index) {\n        if (index < 2 || index > TILE_COUNT) {\n            // TODO exception?\n            return;\n        }\n        int sourceLocation = getTileDataLocation(index); // The two first tiles are not mirrored.\n        int shadedLocation = sourceLocation + 0x4d2 * 2;\n        for (int i = 0; i < FONT_TILE_SIZE; i += 2) {\n            int sourceByte = romImage[sourceLocation + i];\n            if (i % 4 == 2) {\n                romImage[shadedLocation + i] = (byte)(sourceByte | 0xaa);\n            } else {\n                romImage[shadedLocation + i] = (byte)(sourceByte | 0x55);\n            }\n            romImage[shadedLocation + i + 1] = romImage[sourceLocation + i + 1];\n        }\n    }\n\n    private int grayIndexToColor(int index) {\n        switch (index) {\n            case 0 :\n                return 0xFFFFFF;\n            case 1 :\n                return 0x969696;\n            case 2 :\n                return 0x808080;\n            case 3 :\n                return 0x000000;\n            default:\n                return 0xDeadBeef;\n        }\n    }\n\n    public String loadImageData(String name, BufferedImage image) {\n        int numTiles = image.getHeight()/8 * image.getWidth()/8;\n        // Limiting to either loading text tiles or load all tiles. No partial graphical tiles loading.\n        int maxTileIndex = numTiles < LSDJFont.GFX_TILE_COUNT + LSDJFont.TILE_COUNT ? LSDJFont.TILE_COUNT :LSDJFont.TILE_COUNT + LSDJFont.GFX_TILE_COUNT;\n        for (int y = 0; y < image.getHeight(); y++) {\n            for (int x = 0; x < image.getWidth(); x++) {\n                int currentTileIndex = (y / 8) * 8 + x / 8;\n                if (currentTileIndex >= maxTileIndex) break;\n                int rgb = image.getRGB(x, y);\n                float[] color = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);\n                int lum = (int) (color[2] * 255);\n\n                int col = 0;\n                if (lum >= 192)\n                    col = 1;\n                else if (lum >= 64)\n                    col = 2;\n                else if (lum >= 0)\n                    col = 3;\n                setTilePixel(currentTileIndex, x%8, y%8, col);\n            }\n        }\n        StringBuilder sub;\n        if (name.length() < 4) {\n            sub = new StringBuilder(name);\n            for (int i = 0; i < 4 - sub.length(); i++)\n                sub.append(\" \");\n        } else\n            sub = new StringBuilder(name.substring(0, 4));\n        return sub.toString();\n    }\n\n    public BufferedImage saveDataToImage(Boolean includeGfxCharacters) {\n        BufferedImage image = new BufferedImage(LSDJFont.FONT_MAP_WIDTH, includeGfxCharacters ? LSDJFont.GFX_FONT_MAP_HEIGHT : LSDJFont.FONT_MAP_HEIGHT,\n                BufferedImage.TYPE_INT_RGB);\n\n        int tileCount = includeGfxCharacters ? TILE_COUNT + GFX_TILE_COUNT : TILE_COUNT;\n        for (int tile = 0; tile < tileCount; ++tile) {\n            int baseX = (tile %  FONT_NUM_TILES_X)*8;\n            int baseY = (tile / FONT_NUM_TILES_X)*8;\n            for (int y = 0; y < 8; ++y) {\n                for (int x = 0; x < 8; ++x) {\n                    int colorIndex = getTilePixel(tile, x, y);\n                    image.setRGB(baseX + x, baseY + y, grayIndexToColor(colorIndex));\n                }\n            }\n        }\n        return image;\n    }\n}\n"
  },
  {
    "path": "src/main/java/structures/ROMDataManipulator.java",
    "content": "package structures;\n\n/**\n * Base class to centralize the concept of having an access to a ROM's binary data for edition.\n */\npublic abstract class ROMDataManipulator {\n\n    protected int dataOffset = 0;\n    protected byte[] romImage = null;\n\n    public void setRomImage(byte[] romImage) {\n        this.romImage = romImage;\n    }\n\n    public int getDataOffset() {\n        return dataOffset;\n    }\n\n    public  void setDataOffset(int dataOffset) {\n        this.dataOffset = dataOffset;\n    }\n}\n"
  },
  {
    "path": "src/main/java/utils/CommandLineFunctions.java",
    "content": "package utils;\n\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.util.Arrays;\nimport java.util.Vector;\n\nimport javax.imageio.ImageIO;\n\nimport structures.LSDJFont;\n\npublic class CommandLineFunctions {\n    public static void pngToFont(String name, String pngFile, String fntFile) {\n        try {\n            byte[] buffer = new byte[LSDJFont.FONT_NUM_TILES_X * LSDJFont.FONT_NUM_TILES_Y * 16];\n            BufferedImage image = ImageIO.read(new File(pngFile));\n            if (image.getWidth() != LSDJFont.FONT_MAP_WIDTH && image.getHeight() != LSDJFont.FONT_MAP_HEIGHT) {\n                System.err.println(\"Wrong size!\");\n                return;\n            }\n\n            LSDJFont font = new LSDJFont();\n            font.setRomImage(buffer);\n            font.setDataOffset(0);\n            font.generateShadedAndInvertedTiles();\n            String sub = font.loadImageData(name, image);\n\n            FontIO.saveFnt(new File(fntFile), sub, buffer);\n\n            System.out.println(\"OK!\");\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static void fontToPng(String fntFile, String pngFile) {\n        try {\n            byte[] buffer = new byte[LSDJFont.FONT_NUM_TILES_X * LSDJFont.GFX_FONT_NUM_TILES_Y * 16];\n            FontIO.loadFnt(new File(fntFile), buffer);\n            LSDJFont font = new LSDJFont();\n            font.setRomImage(buffer);\n            font.setDataOffset(0);\n            BufferedImage image = font.saveDataToImage(false);\n            ImageIO.write(image, \"PNG\", new File(pngFile));\n            System.out.println(\"OK!\");\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static void extractFontToPng(String romFileName, int numFont, boolean includeGfxCharacters) {\n        if (numFont < 0 || numFont > 2) {\n            // Already -1-ed.\n            System.err.println(\"the font index must be comprised between 1 and 3.\");\n            return;\n        }\n        try {\n            byte[] romImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n            RandomAccessFile romFile = new RandomAccessFile(new File(romFileName), \"r\");\n            romFile.readFully(romImage);\n            romFile.close();\n            LSDJFont font = new LSDJFont();\n\n            font.setRomImage(romImage);\n            int selectedFontOffset = RomUtilities.findFontOffset(romImage) + ((numFont + 1) % 3) * LSDJFont.FONT_SIZE\n                    + LSDJFont.FONT_HEADER_SIZE;\n            font.setDataOffset(selectedFontOffset);\n            font.setGfxDataOffset(RomUtilities.findGfxFontOffset(romImage));\n            BufferedImage image = font.saveDataToImage(includeGfxCharacters);\n            ImageIO.write(image, \"PNG\", new File(RomUtilities.getFontName(romImage, numFont) + \".png\"));\n\n            System.out.println(\"OK!\");\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static void loadPngToRom(String romFileName, String imageFileName, int numFont, String fontName) {\n        if (numFont < 0 || numFont > 2) {\n            // Already -1-ed.\n            System.err.println(\"the font index must be comprised between 1 and 3.\");\n            return;\n        }\n        try {\n            byte[] romImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n            RandomAccessFile romFile = new RandomAccessFile(new File(romFileName), \"rw\");\n            romFile.readFully(romImage);\n            LSDJFont font = new LSDJFont();\n\n            font.setRomImage(romImage);\n            int selectedFontOffset = RomUtilities.findFontOffset(romImage) + ((numFont + 1) % 3) * LSDJFont.FONT_SIZE\n                    + LSDJFont.FONT_HEADER_SIZE;\n            font.setDataOffset(selectedFontOffset);\n            font.generateShadedAndInvertedTiles();\n\n            String correctedName = font.loadImageData(fontName, ImageIO.read(new File(imageFileName)));\n            RomUtilities.setFontName(romImage, numFont, correctedName);\n            romFile.seek(0);\n            romFile.write(romImage);\n            romFile.close();\n\n            System.out.println(\"OK!\");\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    // TODO Merge with KitEditor's own version\n    private static boolean isRomBankAKit(int bankIndex, byte[] romImage) {\n        int l_offset = bankIndex * RomUtilities.BANK_SIZE;\n        byte l_char_1 = romImage[l_offset++];\n        byte l_char_2 = romImage[l_offset];\n        return (l_char_1 == 0x60 && l_char_2 == 0x40);\n    }\n\n    // TODO Merge with KitEditor's own version\n    private static boolean isRomBankEmpty(int bankIndex, byte[] romImage) {\n        int l_offset = bankIndex * RomUtilities.BANK_SIZE;\n        byte l_char_1 = romImage[l_offset++];\n        byte l_char_2 = romImage[l_offset];\n        return (l_char_1 == -1 && l_char_2 == -1);\n    }\n    // TODO replace KitEditor's own version with that\n    private static void clearKitBank(int bankIndex, byte[] romImage) {\n        int baseOffset = bankIndex * RomUtilities.BANK_SIZE;\n        int endOfBank = (bankIndex + 1) * RomUtilities.BANK_SIZE;\n\n        Arrays.fill(romImage, baseOffset, endOfBank, (byte)0xFF);\n    }\n\n    public static void copyAllCustomizations(String originFileName, String destinationFileName)\n    {\n        try {\n            byte[] originRomFile = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n            byte[] destinationRomFile = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT];\n            RandomAccessFile originFile = new RandomAccessFile(new File(originFileName), \"r\");\n            originFile.readFully(originRomFile);\n\n            RandomAccessFile destinationFile = new RandomAccessFile(new File(destinationFileName), \"rw\");\n            destinationFile.readFully(destinationRomFile);\n\n            if (RomUtilities.getNumberOfPalettes(originRomFile) > RomUtilities.getNumberOfPalettes(destinationRomFile)) {\n                System.err.println(\"Warning: Palettes skipped due to lack of space!\");\n            }\n\n            {\n                int inBaseGfxOffset = RomUtilities.findGfxFontOffset(originRomFile);\n                int outBaseGfxOffset = RomUtilities.findGfxFontOffset(destinationRomFile);\n                System.arraycopy(originRomFile, inBaseGfxOffset, destinationRomFile, outBaseGfxOffset,\n                        (LSDJFont.GFX_TILE_COUNT * LSDJFont.FONT_TILE_SIZE));\n            }\n\n            {\n                int inBaseFontOffset = RomUtilities.findFontOffset(originRomFile);\n                int outBaseFontOffset = RomUtilities.findFontOffset(destinationRomFile);\n\n                System.arraycopy(originRomFile, inBaseFontOffset, destinationRomFile, outBaseFontOffset,\n                        (LSDJFont.FONT_SIZE + LSDJFont.FONT_HEADER_SIZE) * LSDJFont.FONT_COUNT);\n\n                int inBaseFontNameOffset = RomUtilities.findFontNameOffset(originRomFile);\n                int outBaseFontNameOffset = RomUtilities.findFontNameOffset(destinationRomFile);\n                System.arraycopy(originRomFile, inBaseFontNameOffset, destinationRomFile, outBaseFontNameOffset,\n                        LSDJFont.FONT_NAME_LENGTH * LSDJFont.FONT_COUNT);\n            }\n\n            {\n                int paletteCount = Math.min(RomUtilities.getNumberOfPalettes(originRomFile),\n                        RomUtilities.getNumberOfPalettes(destinationRomFile));\n                int inPaletteOffset = RomUtilities.findPaletteOffset(originRomFile);\n                int outPaletteOffset = RomUtilities.findPaletteOffset(destinationRomFile);\n                System.arraycopy(originRomFile, inPaletteOffset, destinationRomFile, outPaletteOffset,\n                        RomUtilities.PALETTE_SIZE * paletteCount);\n\n                int inPaletteNameOffset = RomUtilities.findPaletteNameOffset(originRomFile);\n                int outPaletteNameOffset = RomUtilities.findPaletteNameOffset(destinationRomFile);\n                System.arraycopy(originRomFile, inPaletteNameOffset, destinationRomFile, outPaletteNameOffset,\n                        RomUtilities.PALETTE_NAME_SIZE * paletteCount);\n            }\n\n            Vector<Integer> inKitsToCopy = new Vector<>();\n            for (int index = 0; index < RomUtilities.BANK_COUNT; ++index) {\n                if (isRomBankAKit(index, originRomFile)) {\n                    inKitsToCopy.add(index);\n                }\n            }\n            Vector<Integer> outAvailableKitSlots = new Vector<>();\n            for (int index = 0; index < RomUtilities.BANK_COUNT; ++index) {\n                if (isRomBankAKit(index, destinationRomFile) || isRomBankEmpty(index, destinationRomFile)) {\n                    outAvailableKitSlots.add(index);\n                }\n            }\n\n            if (outAvailableKitSlots.size() < inKitsToCopy.size()) {\n                System.err.printf(\"The destination file doesn't have enough kit slots (%d < %d). Aborting.\",\n                        outAvailableKitSlots.size(), inKitsToCopy.size());\n                return;\n            }\n\n            int numToClone = inKitsToCopy.size();\n            for (int index = 0; index < numToClone; ++index)  {\n                int inIndexOfKitToCopy = inKitsToCopy.get(index);\n                int outIndexOfKitToOverwrite = outAvailableKitSlots.get(index);\n                System.arraycopy(\n                        originRomFile, inIndexOfKitToCopy * RomUtilities.BANK_SIZE,\n                        destinationRomFile, outIndexOfKitToOverwrite * RomUtilities.BANK_SIZE,\n                        RomUtilities.BANK_SIZE\n                        );\n            }\n            // Cleaning the destination file\n            for (int index = numToClone; index < outAvailableKitSlots.size(); ++index)  {\n                clearKitBank(outAvailableKitSlots.get(index), destinationRomFile);\n            }\n\n            RomUtilities.fixChecksum(destinationRomFile);\n            destinationFile.seek(0);\n            destinationFile.write(destinationRomFile);\n            destinationFile.close();\n\n            System.out.println(\"OK!\");\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/utils/EditorPreferences.java",
    "content": "package utils;\n\nimport java.io.File;\nimport java.util.prefs.BackingStoreException;\nimport java.util.prefs.Preferences;\n\npublic class EditorPreferences {\n    private static String userDir() {\n        return System.getProperty(\"user.dir\");\n    }\n\n    public static String getKey(String name, String defaultValue) {\n        return GlobalHolder.get(Preferences.class).get(name, defaultValue);\n    }\n\n    public static void putKey(String name, String value) {\n        GlobalHolder.get(Preferences.class).put(name, value);\n    }\n\n    public static String lastPath(String extension) {\n        return getKey(\"lastPath\" + extension, userDir());\n    }\n\n    public static String lastDirectory(String extension) {\n        return new File(lastPath(extension)).getParent();\n    }\n\n    public static void setLastPath(String extension, String value) {\n        putKey(\"lastPath\" + extension, value);\n    }\n\n    public static void clearAll() {\n        try {\n            GlobalHolder.get(Preferences.class).clear();\n        } catch (BackingStoreException e) {\n            e.printStackTrace();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/utils/FileDialogLauncher.java",
    "content": "package utils;\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.io.File;\n\nclass CustomFileFilter implements java.io.FilenameFilter {\n    CustomFileFilter(String[] fileExtensions) {\n        this.fileExtensions = fileExtensions;\n    }\n\n    @Override\n    public boolean accept(File dir, String name) {\n        for (String fileExtension : fileExtensions) {\n            if (name.endsWith(fileExtension)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    String[] fileExtensions;\n}\n\npublic class FileDialogLauncher {\n    public static File load(JFrame parent, String title, String fileExtension) {\n        return open(parent, title, new String[] { fileExtension }, FileDialog.LOAD);\n    }\n\n    public static File load(JFrame parent, String title, String[] fileExtensions) {\n        return open(parent, title, fileExtensions, FileDialog.LOAD);\n    }\n\n    public static File save(JFrame parent, String title, String fileExtension) {\n        return open(parent, title, new String[] { fileExtension }, FileDialog.SAVE);\n    }\n\n    public static File save(JFrame parent, String title, String[] fileExtensions) {\n        return open(parent, title, fileExtensions, FileDialog.SAVE);\n    }\n\n    private static void setFilenameFilter(FileDialog fileDialog, String[] fileExtensions) {\n        if (System.getProperty(\"os.name\").contains(\"Windows\")) {\n            // Works on Windows only.\n            StringBuilder fileMatch = new StringBuilder(\"*.\" + fileExtensions[0]);\n            for (int i = 1; i < fileExtensions.length; ++i) {\n                fileMatch.append(\";*.\").append(fileExtensions[i]);\n            }\n            fileDialog.setFile(fileMatch.toString());\n        } else {\n            // Does not work on Windows.\n            fileDialog.setFilenameFilter(new CustomFileFilter(fileExtensions));\n        }\n    }\n\n    private static File open(JFrame parent, String title, String[] fileExtensions, int mode) {\n        FileDialog fileDialog = new FileDialog(parent, title, mode);\n        fileDialog.setDirectory(EditorPreferences.lastDirectory(fileExtensions[0]));\n        setFilenameFilter(fileDialog, fileExtensions);\n        fileDialog.setVisible(true);\n\n        String directory = fileDialog.getDirectory();\n        String fileName = fileDialog.getFile();\n        if (fileName == null) {\n            return null;\n        }\n\n        boolean filenameMatchesExtension = false;\n        String selectedExtension = null;\n        for (String fileExtension : fileExtensions) {\n            if (fileName.toLowerCase().endsWith(\".\" + fileExtension)) {\n                filenameMatchesExtension = true;\n                selectedExtension = fileExtension;\n                break;\n            }\n        }\n        if (!filenameMatchesExtension) {\n            selectedExtension = fileExtensions[0];\n            fileName += \".\" + fileExtensions[0];\n            if (mode == FileDialog.SAVE &&\n                    new File(directory + fileName).exists() &&\n                    JOptionPane.showConfirmDialog(parent,\n                            fileName + \" already exists.\\n\" +\n                                    \"Do you want to replace it?\",\n                            \"Confirm Save As\",\n                            JOptionPane.YES_NO_OPTION,\n                            JOptionPane.WARNING_MESSAGE) == JOptionPane.NO_OPTION) {\n                return null;\n            }\n        }\n        String path = directory + fileName;\n        assert selectedExtension != null;\n        EditorPreferences.setLastPath(selectedExtension, path);\n        return new File(path);\n    }\n}\n"
  },
  {
    "path": "src/main/java/utils/FileDrop.java",
    "content": "package utils;\n\nimport java.awt.datatransfer.DataFlavor;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.PrintStream;\nimport java.io.Reader;\n\n/**\n * This class makes it easy to drag and drop files from the operating\n * system to a Java program. Any <tt>java.awt.Component</tt> can be\n * dropped onto, but only <tt>javax.swing.JComponent</tt>s will indicate\n * the drop event with a changed border.\n * <p/>\n * To use this class, construct a new <tt>utils.FileDrop</tt> by passing\n * it the target component and a <tt>Listener</tt> to receive notification\n * when file(s) have been dropped. Here is an example:\n * <p/>\n * <code><pre>\n *      JPanel myPanel = new JPanel();\n *      new utils.FileDrop( myPanel, new utils.FileDrop.Listener()\n *      {   public void filesDropped( java.io.File[] files )\n *          {   \n *              // handle file drop\n *              ...\n *          }   // end filesDropped\n *      }); // end utils.FileDrop.Listener\n * </pre></code>\n * <p/>\n * You can specify the border that will appear when files are being dragged by\n * calling the constructor with a <tt>javax.swing.border.Border</tt>. Only\n * <tt>JComponent</tt>s will show any indication with a border.\n * <p/>\n * You can turn on some debugging features by passing a <tt>PrintStream</tt>\n * object (such as <tt>System.out</tt>) into the full constructor. A <tt>null</tt>\n * value will result in no extra debugging information being output.\n * <p/>\n *\n * <p>I'm releasing this code into the Public Domain. Enjoy.\n * </p>\n * <p><em>Original author: Robert Harder, rharder@usa.net</em></p>\n * <p>2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.</p>\n *\n * @author  Robert Harder\n * @author  rharder@users.sf.net\n * @version 1.0.1\n */\npublic class FileDrop\n{\n    private transient javax.swing.border.Border normalBorder;\n    private transient java.awt.dnd.DropTargetListener dropListener;\n    \n    \n    /** Discover if the running JVM is modern enough to have drag and drop. */\n    private static Boolean supportsDnD;\n    \n    // Default border color\n    private static java.awt.Color defaultBorderColor = new java.awt.Color( 0f, 0f, 1f, 0.25f );\n    \n    /**\n     * Constructs a {@link FileDrop} with a default light-blue border\n     * and, if <var>c</var> is a {@link java.awt.Container}, recursively\n     * sets all elements contained within as drop targets, though only\n     * the top level container will change borders.\n     *\n     * @param c Component on which files will be dropped.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.awt.Component c,\n    final Listener listener )\n    {   this( null,  // Logging stream\n              c,     // Drop target\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border\n              true, // Recursive\n              listener );\n    }   // end constructor\n    \n    \n    \n    \n    /**\n     * Constructor with a default border and the option to recursively set drop targets.\n     * If your component is a <tt>java.awt.Container</tt>, then each of its children\n     * components will also listen for drops, though only the parent will change borders.\n     *\n     * @param c Component on which files will be dropped.\n     * @param recursive Recursively set children as drop targets.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.awt.Component c,\n    final boolean recursive,\n    final Listener listener )\n    {   this( null,  // Logging stream\n              c,     // Drop target\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border\n              recursive, // Recursive\n              listener );\n    }   // end constructor\n    \n    \n    /**\n     * Constructor with a default border and debugging optionally turned on.\n     * With Debugging turned on, more status messages will be displayed to\n     * <tt>out</tt>. A common way to use this constructor is with\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\n     * the parameter <tt>out</tt> will result in no debugging output.\n     *\n     * @param out PrintStream to record debugging info or null for no debugging.\n     * @param out \n     * @param c Component on which files will be dropped.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.io.PrintStream out,\n    final java.awt.Component c,\n    final Listener listener )\n    {   this( out,  // Logging stream\n              c,    // Drop target\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), \n              false, // Recursive\n              listener );\n    }   // end constructor\n    \n        \n    \n    /**\n     * Constructor with a default border, debugging optionally turned on\n     * and the option to recursively set drop targets.\n     * If your component is a <tt>java.awt.Container</tt>, then each of its children\n     * components will also listen for drops, though only the parent will change borders.\n     * With Debugging turned on, more status messages will be displayed to\n     * <tt>out</tt>. A common way to use this constructor is with\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\n     * the parameter <tt>out</tt> will result in no debugging output.\n     *\n     * @param out PrintStream to record debugging info or null for no debugging.\n     * @param out \n     * @param c Component on which files will be dropped.\n     * @param recursive Recursively set children as drop targets.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.io.PrintStream out,\n    final java.awt.Component c,\n    final boolean recursive,\n    final Listener listener)\n    {   this( out,  // Logging stream\n              c,    // Drop target\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border\n              recursive, // Recursive\n              listener );\n    }   // end constructor\n    \n    \n    \n    \n    /**\n     * Constructor with a specified border \n     *\n     * @param c Component on which files will be dropped.\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.awt.Component c,\n    final javax.swing.border.Border dragBorder,\n    final Listener listener) \n    {   this(\n            null,   // Logging stream\n            c,      // Drop target\n            dragBorder, // Drag border\n            false,  // Recursive\n            listener );\n    }   // end constructor\n    \n    \n        \n    \n    /**\n     * Constructor with a specified border and the option to recursively set drop targets.\n     * If your component is a <tt>java.awt.Container</tt>, then each of its children\n     * components will also listen for drops, though only the parent will change borders.\n     *\n     * @param c Component on which files will be dropped.\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\n     * @param recursive Recursively set children as drop targets.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.awt.Component c,\n    final javax.swing.border.Border dragBorder,\n    final boolean recursive,\n    final Listener listener) \n    {   this(\n            null,\n            c,\n            dragBorder,\n            recursive,\n            listener );\n    }   // end constructor\n    \n            \n    \n    /**\n     * Constructor with a specified border and debugging optionally turned on.\n     * With Debugging turned on, more status messages will be displayed to\n     * <tt>out</tt>. A common way to use this constructor is with\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\n     * the parameter <tt>out</tt> will result in no debugging output.\n     *\n     * @param out PrintStream to record debugging info or null for no debugging.\n     * @param c Component on which files will be dropped.\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.io.PrintStream out,\n    final java.awt.Component c,\n    final javax.swing.border.Border dragBorder,\n    final Listener listener) \n    {   this(\n            out,    // Logging stream\n            c,      // Drop target\n            dragBorder, // Drag border\n            false,  // Recursive\n            listener );\n    }   // end constructor\n    \n    \n    \n    \n    \n    /**\n     * Full constructor with a specified border and debugging optionally turned on.\n     * With Debugging turned on, more status messages will be displayed to\n     * <tt>out</tt>. A common way to use this constructor is with\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\n     * the parameter <tt>out</tt> will result in no debugging output.\n     *\n     * @param out PrintStream to record debugging info or null for no debugging.\n     * @param c Component on which files will be dropped.\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\n     * @param recursive Recursively set children as drop targets.\n     * @param listener Listens for <tt>filesDropped</tt>.\n     * @since 1.0\n     */\n    public FileDrop(\n    final java.io.PrintStream out,\n    final java.awt.Component c,\n    final javax.swing.border.Border dragBorder,\n    final boolean recursive,\n    final Listener listener) \n    {   \n        \n        if( supportsDnD() )\n        {   // Make a drop listener\n            dropListener = new java.awt.dnd.DropTargetListener()\n            {   public void dragEnter( java.awt.dnd.DropTargetDragEvent evt )\n                {       log( out, \"utils.FileDrop: dragEnter event.\" );\n\n                    // Is this an acceptable drag event?\n                    if( isDragOk( out, evt ) )\n                    {\n                        // If it's a Swing component, set its border\n                        if( c instanceof javax.swing.JComponent )\n                        {   javax.swing.JComponent jc = (javax.swing.JComponent) c;\n                            normalBorder = jc.getBorder();\n                            log( out, \"utils.FileDrop: normal border saved.\" );\n                            jc.setBorder( dragBorder );\n                            log( out, \"utils.FileDrop: drag border set.\" );\n                        }   // end if: JComponent   \n\n                        // Acknowledge that it's okay to enter\n                        //evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\n                        evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY );\n                        log( out, \"utils.FileDrop: event accepted.\" );\n                    }   // end if: drag ok\n                    else \n                    {   // Reject the drag event\n                        evt.rejectDrag();\n                        log( out, \"utils.FileDrop: event rejected.\" );\n                    }   // end else: drag not ok\n                }   // end dragEnter\n\n                public void dragOver( java.awt.dnd.DropTargetDragEvent evt ) \n                {   // This is called continually as long as the mouse is\n                    // over the drag target.\n                }   // end dragOver\n\n                public void drop( java.awt.dnd.DropTargetDropEvent evt )\n                {   log( out, \"utils.FileDrop: drop event.\" );\n                    try\n                    {   // Get whatever was dropped\n                        java.awt.datatransfer.Transferable tr = evt.getTransferable();\n\n                        // Is it a file list?\n                        if (tr.isDataFlavorSupported (java.awt.datatransfer.DataFlavor.javaFileListFlavor))\n                        {\n                            // Say we'll take it.\n                            //evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\n                            evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY );\n                            log( out, \"utils.FileDrop: file list accepted.\" );\n\n                            // Get a useful list\n                            java.util.List fileList = (java.util.List) \n                                tr.getTransferData(java.awt.datatransfer.DataFlavor.javaFileListFlavor);\n                            java.util.Iterator iterator = fileList.iterator();\n\n                            // Convert list to array\n                            java.io.File[] filesTemp = new java.io.File[ fileList.size() ];\n                            fileList.toArray( filesTemp );\n                            final java.io.File[] files = filesTemp;\n\n                            // Alert listener to drop.\n                            if( listener != null )\n                                listener.filesDropped( files );\n\n                            // Mark that drop is completed.\n                            evt.getDropTargetContext().dropComplete(true);\n                            log( out, \"utils.FileDrop: drop complete.\" );\n                        }   // end if: file list\n                        else // this section will check for a reader flavor.\n                        {\n                            // Thanks, Nathan!\n                            // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\n                            DataFlavor[] flavors = tr.getTransferDataFlavors();\n                            boolean handled = false;\n                            for (int zz = 0; zz < flavors.length; zz++) {\n                                if (flavors[zz].isRepresentationClassReader()) {\n                                    // Say we'll take it.\n                                    //evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\n                                    evt.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY);\n                                    log(out, \"utils.FileDrop: reader accepted.\");\n\n                                    Reader reader = flavors[zz].getReaderForText(tr);\n\n                                    BufferedReader br = new BufferedReader(reader);\n                                    \n                                    if(listener != null)\n                                        listener.filesDropped(createFileArray(br, out));\n                                    \n                                    // Mark that drop is completed.\n                                    evt.getDropTargetContext().dropComplete(true);\n                                    log(out, \"utils.FileDrop: drop complete.\");\n                                    handled = true;\n                                    break;\n                                }\n                            }\n                            if(!handled){\n                                log( out, \"utils.FileDrop: not a file list or reader - abort.\" );\n                                evt.rejectDrop();\n                            }\n                            // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\n                        }   // end else: not a file list\n                    }   // end try\n                    catch ( java.io.IOException io) \n                    {   log( out, \"utils.FileDrop: IOException - abort:\" );\n                        io.printStackTrace( out );\n                        evt.rejectDrop();\n                    }   // end catch IOException\n                    catch (java.awt.datatransfer.UnsupportedFlavorException ufe) \n                    {   log( out, \"utils.FileDrop: UnsupportedFlavorException - abort:\" );\n                        ufe.printStackTrace( out );\n                        evt.rejectDrop();\n                    }   // end catch: UnsupportedFlavorException\n                    finally\n                    {\n                        // If it's a Swing component, reset its border\n                        if( c instanceof javax.swing.JComponent )\n                        {   javax.swing.JComponent jc = (javax.swing.JComponent) c;\n                            jc.setBorder( normalBorder );\n                            log( out, \"utils.FileDrop: normal border restored.\" );\n                        }   // end if: JComponent\n                    }   // end finally\n                }   // end drop\n\n                public void dragExit( java.awt.dnd.DropTargetEvent evt ) \n                {   log( out, \"utils.FileDrop: dragExit event.\" );\n                    // If it's a Swing component, reset its border\n                    if( c instanceof javax.swing.JComponent )\n                    {   javax.swing.JComponent jc = (javax.swing.JComponent) c;\n                        jc.setBorder( normalBorder );\n                        log( out, \"utils.FileDrop: normal border restored.\" );\n                    }   // end if: JComponent\n                }   // end dragExit\n\n                public void dropActionChanged( java.awt.dnd.DropTargetDragEvent evt ) \n                {   log( out, \"utils.FileDrop: dropActionChanged event.\" );\n                    // Is this an acceptable drag event?\n                    if( isDragOk( out, evt ) )\n                    {   //evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\n                        evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY );\n                        log( out, \"utils.FileDrop: event accepted.\" );\n                    }   // end if: drag ok\n                    else \n                    {   evt.rejectDrag();\n                        log( out, \"utils.FileDrop: event rejected.\" );\n                    }   // end else: drag not ok\n                }   // end dropActionChanged\n            }; // end DropTargetListener\n\n            // Make the component (and possibly children) drop targets\n            makeDropTarget( out, c, recursive );\n        }   // end if: supports dnd\n        else\n        {   log( out, \"utils.FileDrop: Drag and drop is not supported with this JVM\" );\n        }   // end else: does not support DnD\n    }   // end constructor\n\n    \n    private static boolean supportsDnD()\n    {   // Static Boolean\n        if( supportsDnD == null )\n        {   \n            boolean support = false;\n            try\n            {   Class arbitraryDndClass = Class.forName( \"java.awt.dnd.DnDConstants\" );\n                support = true;\n            }   // end try\n            catch( Exception e )\n            {   support = false;\n            }   // end catch\n            supportsDnD = new Boolean( support );\n        }   // end if: first time through\n        return supportsDnD.booleanValue();\n    }   // end supportsDnD\n    \n    \n     // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\n     private static String ZERO_CHAR_STRING = \"\" + (char)0;\n     private static File[] createFileArray(BufferedReader bReader, PrintStream out)\n     {\n        try { \n            java.util.List list = new java.util.ArrayList();\n            java.lang.String line = null;\n            while ((line = bReader.readLine()) != null) {\n                try {\n                    // kde seems to append a 0 char to the end of the reader\n                    if(ZERO_CHAR_STRING.equals(line)) continue; \n                    \n                    java.io.File file = new java.io.File(new java.net.URI(line));\n                    list.add(file);\n                } catch (Exception ex) {\n                    log(out, \"Error with \" + line + \": \" + ex.getMessage());\n                }\n            }\n\n            return (java.io.File[]) list.toArray(new File[list.size()]);\n        } catch (IOException ex) {\n            log(out, \"utils.FileDrop: IOException\");\n        }\n        return new File[0];\n     }\n     // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\n     \n    \n    private void makeDropTarget( final java.io.PrintStream out, final java.awt.Component c, boolean recursive )\n    {\n        // Make drop target\n        final java.awt.dnd.DropTarget dt = new java.awt.dnd.DropTarget();\n        try\n        {   dt.addDropTargetListener( dropListener );\n        }   // end try\n        catch( java.util.TooManyListenersException e )\n        {   e.printStackTrace();\n            log(out, \"utils.FileDrop: Drop will not work due to previous error. Do you have another listener attached?\" );\n        }   // end catch\n        \n        // Listen for hierarchy changes and remove the drop target when the parent gets cleared out.\n        c.addHierarchyListener( new java.awt.event.HierarchyListener()\n        {   public void hierarchyChanged( java.awt.event.HierarchyEvent evt )\n            {   log( out, \"utils.FileDrop: Hierarchy changed.\" );\n                java.awt.Component parent = c.getParent();\n                if( parent == null )\n                {   c.setDropTarget( null );\n                    log( out, \"utils.FileDrop: Drop target cleared from component.\" );\n                }   // end if: null parent\n                else\n                {   new java.awt.dnd.DropTarget(c, dropListener);\n                    log( out, \"utils.FileDrop: Drop target added to component.\" );\n                }   // end else: parent not null\n            }   // end hierarchyChanged\n        }); // end hierarchy listener\n        if( c.getParent() != null )\n            new java.awt.dnd.DropTarget(c, dropListener);\n        \n        if( recursive && (c instanceof java.awt.Container ) )\n        {   \n            // Get the container\n            java.awt.Container cont = (java.awt.Container) c;\n            \n            // Get it's components\n            java.awt.Component[] comps = cont.getComponents();\n            \n            // Set it's components as listeners also\n            for( int i = 0; i < comps.length; i++ )\n                makeDropTarget( out, comps[i], recursive );\n        }   // end if: recursively set components as listener\n    }   // end dropListener\n    \n    \n    \n    /** Determine if the dragged data is a file list. */\n    private boolean isDragOk( final java.io.PrintStream out, final java.awt.dnd.DropTargetDragEvent evt )\n    {   boolean ok = false;\n        \n        // Get data flavors being dragged\n        java.awt.datatransfer.DataFlavor[] flavors = evt.getCurrentDataFlavors();\n        \n        // See if any of the flavors are a file list\n        int i = 0;\n        while( !ok && i < flavors.length )\n        {   \n            // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\n            // Is the flavor a file list?\n            final DataFlavor curFlavor = flavors[i];\n            if( curFlavor.equals( java.awt.datatransfer.DataFlavor.javaFileListFlavor ) ||\n                curFlavor.isRepresentationClassReader()){\n                ok = true;\n            }\n            // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\n            i++;\n        }   // end while: through flavors\n        \n        // If logging is enabled, show data flavors\n        if( out != null )\n        {   if( flavors.length == 0 )\n                log( out, \"utils.FileDrop: no data flavors.\" );\n            for( i = 0; i < flavors.length; i++ )\n                log( out, flavors[i].toString() );\n        }   // end if: logging enabled\n        \n        return ok;\n    }   // end isDragOk\n    \n    \n    /** Outputs <tt>message</tt> to <tt>out</tt> if it's not null. */\n    private static void log( java.io.PrintStream out, String message )\n    {   // Log message if requested\n        if( out != null )\n            out.println( message );\n    }   // end log\n\n    \n    \n    \n    /**\n     * Removes the drag-and-drop hooks from the component and optionally\n     * from the all children. You should call this if you add and remove\n     * components after you've set up the drag-and-drop.\n     * This will recursively unregister all components contained within\n     * <var>c</var> if <var>c</var> is a {@link java.awt.Container}.\n     *\n     * @param c The component to unregister as a drop target\n     * @since 1.0\n     */\n    public static boolean remove( java.awt.Component c)\n    {   return remove( null, c, true );\n    }   // end remove\n    \n    \n    \n    /**\n     * Removes the drag-and-drop hooks from the component and optionally\n     * from the all children. You should call this if you add and remove\n     * components after you've set up the drag-and-drop.\n     *\n     * @param out Optional {@link java.io.PrintStream} for logging drag and drop messages\n     * @param c The component to unregister\n     * @param recursive Recursively unregister components within a container\n     * @since 1.0\n     */\n    public static boolean remove( java.io.PrintStream out, java.awt.Component c, boolean recursive )\n    {   // Make sure we support dnd.\n        if( supportsDnD() )\n        {   log( out, \"utils.FileDrop: Removing drag-and-drop hooks.\" );\n            c.setDropTarget( null );\n            if( recursive && ( c instanceof java.awt.Container ) )\n            {   java.awt.Component[] comps = ((java.awt.Container)c).getComponents();\n                for( int i = 0; i < comps.length; i++ )\n                    remove( out, comps[i], recursive );\n                return true;\n            }   // end if: recursive\n            else return false;\n        }   // end if: supports DnD\n        else return false;\n    }   // end remove\n    \n    \n\n    \n/* ********  I N N E R   I N T E R F A C E   L I S T E N E R  ******** */    \n    \n    \n    /**\n     * Implement this inner interface to listen for when files are dropped. For example\n     * your class declaration may begin like this:\n     * <code><pre>\n     *      public class MyClass implements utils.FileDrop.Listener\n     *      ...\n     *      public void filesDropped( java.io.File[] files )\n     *      {\n     *          ...\n     *      }   // end filesDropped\n     *      ...\n     * </pre></code>\n     *\n     * @since 1.1\n     */\n    public static interface Listener {\n       \n        /**\n         * This method is called when files have been successfully dropped.\n         *\n         * @param files An array of <tt>File</tt>s that were dropped.\n         * @since 1.0\n         */\n        public abstract void filesDropped( java.io.File[] files );\n        \n        \n    }   // end inner-interface Listener\n    \n    \n/* ********  I N N E R   C L A S S  ******** */    \n    \n    \n    /**\n     * This is the event that is passed to the\n     * {@link FileDropListener#filesDropped filesDropped(...)} method in\n     * your {@link FileDropListener} when files are dropped onto\n     * a registered drop target.\n     *\n     * <p>I'm releasing this code into the Public Domain. Enjoy.</p>\n     * \n     * @author  Robert Harder\n     * @author  rob@iharder.net\n     * @version 1.2\n     */\n    public static class Event extends java.util.EventObject {\n\n        private java.io.File[] files;\n\n        /**\n         * Constructs an {@link Event} with the array\n         * of files that were dropped and the\n         * {@link FileDrop} that initiated the event.\n         *\n         * @param files The array of files that were dropped\n         * @source The event source\n         * @since 1.1\n         */\n        public Event( java.io.File[] files, Object source ) {\n            super( source );\n            this.files = files;\n        }   // end constructor\n\n        /**\n         * Returns an array of files that were dropped on a\n         * registered drop target.\n         *\n         * @return array of files that were dropped\n         * @since 1.1\n         */\n        public java.io.File[] getFiles() {\n            return files;\n        }   // end getFiles\n    \n    }   // end inner class Event\n    \n    \n    \n/* ********  I N N E R   C L A S S  ******** */\n    \n\n    /**\n     * At last an easy way to encapsulate your custom objects for dragging and dropping\n     * in your Java programs!\n     * When you need to create a {@link java.awt.datatransfer.Transferable} object,\n     * use this class to wrap your object.\n     * For example:\n     * <pre><code>\n     *      ...\n     *      MyCoolClass myObj = new MyCoolClass();\n     *      Transferable xfer = new TransferableObject( myObj );\n     *      ...\n     * </code></pre>\n     * Or if you need to know when the data was actually dropped, like when you're\n     * moving data out of a list, say, you can use the {@link TransferableObject.Fetcher}\n     * inner class to return your object Just in Time.\n     * For example:\n     * <pre><code>\n     *      ...\n     *      final MyCoolClass myObj = new MyCoolClass();\n     *\n     *      TransferableObject.Fetcher fetcher = new TransferableObject.Fetcher()\n     *      {   public Object getObject(){ return myObj; }\n     *      }; // end fetcher\n     *\n     *      Transferable xfer = new TransferableObject( fetcher );\n     *      ...\n     * </code></pre>\n     *\n     * The {@link java.awt.datatransfer.DataFlavor} associated with \n     * {@link TransferableObject} has the representation class\n     * <tt>net.iharder.dnd.TransferableObject.class</tt> and MIME type\n     * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\n     * This data flavor is accessible via the static\n     * {@link #DATA_FLAVOR} property.\n     *\n     *\n     * <p>I'm releasing this code into the Public Domain. Enjoy.</p>\n     * \n     * @author  Robert Harder\n     * @author  rob@iharder.net\n     * @version 1.2\n     */\n    public static class TransferableObject implements java.awt.datatransfer.Transferable\n    {\n        /**\n         * The MIME type for {@link #DATA_FLAVOR} is \n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\n         *\n         * @since 1.1\n         */\n        public final static String MIME_TYPE = \"application/x-net.iharder.dnd.TransferableObject\";\n\n\n        /**\n         * The default {@link java.awt.datatransfer.DataFlavor} for\n         * {@link TransferableObject} has the representation class\n         * <tt>net.iharder.dnd.TransferableObject.class</tt>\n         * and the MIME type \n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\n         *\n         * @since 1.1\n         */\n        public final static java.awt.datatransfer.DataFlavor DATA_FLAVOR = \n            new java.awt.datatransfer.DataFlavor( FileDrop.TransferableObject.class, MIME_TYPE );\n\n\n        private Fetcher fetcher;\n        private Object data;\n\n        private java.awt.datatransfer.DataFlavor customFlavor; \n\n\n\n        /**\n         * Creates a new {@link TransferableObject} that wraps <var>data</var>.\n         * Along with the {@link #DATA_FLAVOR} associated with this class,\n         * this creates a custom data flavor with a representation class \n         * determined from <code>data.getClass()</code> and the MIME type\n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\n         *\n         * @param data The data to transfer\n         * @since 1.1\n         */\n        public TransferableObject( Object data )\n        {   this.data = data;\n            this.customFlavor = new java.awt.datatransfer.DataFlavor( data.getClass(), MIME_TYPE );\n        }   // end constructor\n\n\n\n        /**\n         * Creates a new {@link TransferableObject} that will return the\n         * object that is returned by <var>fetcher</var>.\n         * No custom data flavor is set other than the default\n         * {@link #DATA_FLAVOR}.\n         *\n         * @see Fetcher\n         * @param fetcher The {@link Fetcher} that will return the data object\n         * @since 1.1\n         */\n        public TransferableObject( Fetcher fetcher )\n        {   this.fetcher = fetcher;\n        }   // end constructor\n\n\n\n        /**\n         * Creates a new {@link TransferableObject} that will return the\n         * object that is returned by <var>fetcher</var>.\n         * Along with the {@link #DATA_FLAVOR} associated with this class,\n         * this creates a custom data flavor with a representation class <var>dataClass</var>\n         * and the MIME type\n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\n         *\n         * @see Fetcher\n         * @param dataClass The {@link java.lang.Class} to use in the custom data flavor\n         * @param fetcher The {@link Fetcher} that will return the data object\n         * @since 1.1\n         */\n        public TransferableObject( Class dataClass, Fetcher fetcher )\n        {   this.fetcher = fetcher;\n            this.customFlavor = new java.awt.datatransfer.DataFlavor( dataClass, MIME_TYPE );\n        }   // end constructor\n\n        /**\n         * Returns the custom {@link java.awt.datatransfer.DataFlavor} associated\n         * with the encapsulated object or <tt>null</tt> if the {@link Fetcher}\n         * constructor was used without passing a {@link java.lang.Class}.\n         *\n         * @return The custom data flavor for the encapsulated object\n         * @since 1.1\n         */\n        public java.awt.datatransfer.DataFlavor getCustomDataFlavor()\n        {   return customFlavor;\n        }   // end getCustomDataFlavor\n\n\n    /* ********  T R A N S F E R A B L E   M E T H O D S  ******** */    \n\n\n        /**\n         * Returns a two- or three-element array containing first\n         * the custom data flavor, if one was created in the constructors,\n         * second the default {@link #DATA_FLAVOR} associated with\n         * {@link TransferableObject}, and third the\n         * {@link java.awt.datatransfer.DataFlavor.stringFlavor}.\n         *\n         * @return An array of supported data flavors\n         * @since 1.1\n         */\n        public java.awt.datatransfer.DataFlavor[] getTransferDataFlavors() \n        {   \n            if( customFlavor != null )\n                return new java.awt.datatransfer.DataFlavor[]\n                {   customFlavor,\n                    DATA_FLAVOR,\n                    java.awt.datatransfer.DataFlavor.stringFlavor\n                };  // end flavors array\n            else\n                return new java.awt.datatransfer.DataFlavor[]\n                {   DATA_FLAVOR,\n                    java.awt.datatransfer.DataFlavor.stringFlavor\n                };  // end flavors array\n        }   // end getTransferDataFlavors\n\n\n\n        /**\n         * Returns the data encapsulated in this {@link TransferableObject}.\n         * If the {@link Fetcher} constructor was used, then this is when\n         * the {@link Fetcher#getObject getObject()} method will be called.\n         * If the requested data flavor is not supported, then the\n         * {@link Fetcher#getObject getObject()} method will not be called.\n         *\n         * @param flavor The data flavor for the data to return\n         * @return The dropped data\n         * @since 1.1\n         */\n        public Object getTransferData( java.awt.datatransfer.DataFlavor flavor )\n        throws java.awt.datatransfer.UnsupportedFlavorException, java.io.IOException \n        {   \n            // Native object\n            if( flavor.equals( DATA_FLAVOR ) )\n                return fetcher == null ? data : fetcher.getObject();\n\n            // String\n            if( flavor.equals( java.awt.datatransfer.DataFlavor.stringFlavor ) )\n                return fetcher == null ? data.toString() : fetcher.getObject().toString();\n\n            // We can't do anything else\n            throw new java.awt.datatransfer.UnsupportedFlavorException(flavor);\n        }   // end getTransferData\n\n\n\n\n        /**\n         * Returns <tt>true</tt> if <var>flavor</var> is one of the supported\n         * flavors. Flavors are supported using the <code>equals(...)</code> method.\n         *\n         * @param flavor The data flavor to check\n         * @return Whether or not the flavor is supported\n         * @since 1.1\n         */\n        public boolean isDataFlavorSupported( java.awt.datatransfer.DataFlavor flavor ) \n        {\n            // Native object\n            if( flavor.equals( DATA_FLAVOR ) )\n                return true;\n\n            // String\n            if( flavor.equals( java.awt.datatransfer.DataFlavor.stringFlavor ) )\n                return true;\n\n            // We can't do anything else\n            return false;\n        }   // end isDataFlavorSupported\n\n\n    /* ********  I N N E R   I N T E R F A C E   F E T C H E R  ******** */    \n\n        /**\n         * Instead of passing your data directly to the {@link TransferableObject}\n         * constructor, you may want to know exactly when your data was received\n         * in case you need to remove it from its source (or do anyting else to it).\n         * When the {@link #getTransferData getTransferData(...)} method is called\n         * on the {@link TransferableObject}, the {@link Fetcher}'s\n         * {@link #getObject getObject()} method will be called.\n         *\n         * @author Robert Harder\n         * @copyright 2001\n         * @version 1.1\n         * @since 1.1\n         */\n        public static interface Fetcher\n        {\n            /**\n             * Return the object being encapsulated in the\n             * {@link TransferableObject}.\n             *\n             * @return The dropped object\n             * @since 1.1\n             */\n            public abstract Object getObject();\n        }   // end inner interface Fetcher\n\n\n\n    }   // end class TransferableObject\n\n    \n    \n    \n    \n}   // end class utils.FileDrop\n"
  },
  {
    "path": "src/main/java/utils/FontIO.java",
    "content": "package utils;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\nimport structures.LSDJFont;\n\npublic class FontIO {\n\n    static void loadFnt(File file, byte[] array) throws IOException {\n        loadFnt(file, array, 0);\n    }\n\n    public static String loadFnt(File file, byte[] array, int arrayOffset) throws IOException {\n        StringBuilder name = new StringBuilder();\n        int bytesPerTile = 16;\n        int fontSize = LSDJFont.TILE_COUNT * bytesPerTile;\n        java.io.RandomAccessFile f = new java.io.RandomAccessFile(file, \"r\");\n\n        for (int i = 0; i < LSDJFont.FONT_NAME_LENGTH; ++i) {\n            name.append((char) f.read());\n        }\n\n        for (int i = 0; i < fontSize; ++i) {\n            array[i + arrayOffset] = (byte) f.read();\n        }\n\n        f.close();\n        return name.toString();\n    }\n\n    static void saveFnt(File file, String fontName, byte[] array) throws IOException {\n        saveFnt(file, fontName, array, 0);\n    }\n\n    public static void saveFnt(File file, String fontName, byte[] array, int arrayOffset) throws IOException {\n        FileOutputStream f = new java.io.FileOutputStream(file);\n        f.write(fontName.charAt(0));\n        f.write(fontName.charAt(1));\n        f.write(fontName.charAt(2));\n        f.write(fontName.charAt(3));\n        int bytesPerTile = 16;\n        int fontSize = LSDJFont.TILE_COUNT * bytesPerTile;\n        for (int i = 0; i < fontSize; ++i) {\n            f.write(array[i + arrayOffset]);\n        }\n        f.close();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/utils/GlobalHolder.java",
    "content": "package utils;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Singletons is an useful template but it does have one big issue: it doesnn't\n * comply with the Single Responsibility Principle as it both holds an instance\n * and manages its lifetime.\n * <p>\n * This class only offers a way to store and access a global instance as long as\n * the using code manages the lifetime of said global.\n * Allows for using a kind-of namespace and class override,\n * providing the overriding class is a child of the given object.\n *\n * @author Florian Dormont\n */\npublic class GlobalHolder {\n    static private Map<String, Object> globals = null;\n\n    private static void lazyInstantiation() {\n        if (globals == null)\n            globals = new HashMap<>();\n    }\n\n    public static void set(Object object) {\n        lazyInstantiation();\n        globals.put(object.getClass().getCanonicalName(), object);\n    }\n\n    public static <C> void set(C object, Class<C> cls) {\n        lazyInstantiation();\n        globals.put(cls.getCanonicalName(), object);\n    }\n\n    public static <C> void set(C object, Class<C> cls, String namespace) {\n        lazyInstantiation();\n        globals.put(namespace + \"::\" + cls.getCanonicalName(), object);\n    }\n\n    public static void set(Object object, String namespace) {\n        lazyInstantiation();\n        globals.put(namespace + \"::\" + object.getClass().getCanonicalName(), object);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <C> C get(Class<C> cls) {\n        lazyInstantiation();\n        return (C) globals.get(cls.getCanonicalName());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <C> C get(Class<C> cls, String namespace) {\n        lazyInstantiation();\n        return (C) globals.get(namespace + \"::\" + cls.getCanonicalName());\n    }\n\n    public static <C> C release(Class<C> cls) {\n        lazyInstantiation();\n        C c = get(cls);\n        globals.put(c.getClass().getCanonicalName(), null);\n        return c;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/utils/RomUtilities.java",
    "content": "package utils;\n\nimport structures.LSDJFont;\n\npublic class RomUtilities {\n    public static final int BANK_COUNT = 64;\n    public static final int BANK_SIZE = 0x4000;\n\n    public static final int COLOR_SET_SIZE = 4 * 2;  // one color set contains 4 colors\n    public static final int NUM_COLOR_SETS = 5;  // one palette contains 5 color sets\n    public static final int PALETTE_SIZE = COLOR_SET_SIZE * NUM_COLOR_SETS;\n    public static final int PALETTE_NAME_SIZE = 5;\n\n    private static int findGrayscalePaletteNames(byte[] romImage)\n    {\n        for (int i = 0x4000 * 27;i < 0x4000 * 28; ++i) {\n            if (romImage[i] != 0 &&\n                    romImage[i + 1] != 0 &&\n                    romImage[i + 2] != 0 &&\n                    romImage[i + 3] != 0 &&\n                    romImage[i + 4] == 0 &&\n                    romImage[i + 5] != 0 &&\n                    romImage[i + 6] != 0 &&\n                    romImage[i + 7] != 0 &&\n                    romImage[i + 8] != 0 &&\n                    romImage[i + 9] == 0 &&\n                    romImage[i + 10] != 0 &&\n                    romImage[i + 11] != 0 &&\n                    romImage[i + 12] != 0 &&\n                    romImage[i + 13] != 0 &&\n                    romImage[i + 14] == 0)\n            {\n                return i + 15;\n            }\n        }\n        return -1;\n    }\n\n    private static int findScreenBackgroundData(byte[] romImage)\n    {\n        int numPalettes = getNumberOfPalettes(romImage);\n        if (numPalettes == -1)\n        {\n            return -1;\n        }\n        for (int i = 0x4000; i < 0x8000; ++i)\n        {\n            if (romImage[i] == 0 &&\n                    romImage[i + 1] == 0 &&\n                    romImage[i + 2] == 0 &&\n                    romImage[i + 3] == 0 &&\n                    romImage[i + 4] == 0 &&\n                    romImage[i + 5] == 0 &&\n                    romImage[i + 6] == 0 &&\n                    romImage[i + 7] == 0 &&\n                    romImage[i + 8] == 0 &&\n                    romImage[i + 9] == 0 &&\n                    romImage[i + 10] == 0 &&\n                    romImage[i + 11] == 0 &&\n                    romImage[i + 12] == 0 &&\n                    romImage[i + 13] == 0 &&\n                    romImage[i + 14] == 0 &&\n                    romImage[i + 15] == 0 &&\n                    romImage[i + 16] == 0 &&\n                    romImage[i + 17] == 72 &&\n                    romImage[i + 18] == 72 &&\n                    romImage[i + 19] == 72)\n            {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    public static int getNumberOfPalettes(byte[] romImage)\n    {\n        int baseOffset = findGrayscalePaletteNames(romImage);\n        if (baseOffset == -1)\n        {\n            return -1;\n        }\n\n        int numPalettes = 0;\n        for (int j = baseOffset + 4; romImage[j] == 0; j +=5)\n        {\n            ++numPalettes;\n        }\n        return numPalettes/2;\n    }\n\n    public static int findPaletteOffset(byte[] romImage) {\n        // Finds the palette location by searching for the screen\n        // backgrounds, which are defined directly after the palettes\n        // in bank 1.\n        int baseOffset = findScreenBackgroundData(romImage);\n        if (baseOffset == -1)\n        {\n            return -1;\n        }\n        return baseOffset - getNumberOfPalettes(romImage) * PALETTE_SIZE;\n    }\n\n    public static int findPaletteNameOffset(byte[] romImage) {\n        // Palette names are in bank 27.\n        int baseOffset = findGrayscalePaletteNames(romImage);\n        if (baseOffset == -1)\n        {\n            return -1;\n        }\n\n        return baseOffset + 5 * getNumberOfPalettes(romImage);\n    }\n\n    // Returns address of first graphics character.\n    public static int findGfxFontOffset(byte[] romImage) {\n        for (int i = 30 * 0x4000; i < 31 * 0x4000; ++i) {\n            if (romImage[i] == 1 && romImage[i + 1] == 46 && romImage[i + 2] == 0 && romImage[i + 3] == 1) {\n                return i + 2 + 8 * 16;\n            }\n        }\n        return -1;\n    }\n\n    public static int findFontOffset(byte[] romImage) {\n        int gfxOffset = findGfxFontOffset(romImage);\n        int gfxCharacterCount = 46;\n        int gfxCharacterSize = 16;\n        return gfxOffset == -1 ? -1 : gfxOffset + gfxCharacterCount * gfxCharacterSize;\n    }\n\n    public static int findFontNameOffset(byte[] romImage) {\n        // Palette names are in bank 27.\n        int baseOffset = findGrayscalePaletteNames(romImage);\n        if (baseOffset == -1)\n        {\n            return -1;\n        }\n        return baseOffset - 15;\n    }\n\n    public static String getFontName(byte[] romImage, int font) {\n        int fontNameSize = 5;\n        int nameOffset = findFontNameOffset(romImage);\n        StringBuilder s = new StringBuilder();\n        for (int i = 0; i < LSDJFont.FONT_NAME_LENGTH; i++) {\n            s.append((char) romImage[nameOffset + font * fontNameSize + i]);\n        }\n        return s.toString();\n    }\n\n    public static void setFontName(byte[] romImage, int fontIndex, String fontName) {\n        StringBuilder fontNameBuilder = new StringBuilder(fontName);\n        while (fontNameBuilder.length() < 4) {\n            fontNameBuilder.append(\" \");\n        }\n        fontName = fontNameBuilder.toString();\n        int fontNameSize = 5;\n        int nameOffset = findFontNameOffset(romImage);\n        for (int i = 0; i < LSDJFont.FONT_NAME_LENGTH; i++) {\n            romImage[nameOffset + fontIndex * fontNameSize + i] = (byte) fontName.charAt(i);\n        }\n    }\n\n    public static void fixChecksum(byte[] romImage) {\n        int checksum014D = 0;\n        for (int i = 0x134; i < 0x14D; ++i) {\n            checksum014D = checksum014D - romImage[i] - 1;\n        }\n        romImage[0x14D] = (byte) (checksum014D & 0xFF);\n\n        int checksum014E = 0;\n        for (int i = 0; i < romImage.length; ++i) {\n            if (i == 0x14E || i == 0x14F) {\n                continue;\n            }\n            checksum014E += romImage[i] & 0xFF;\n        }\n\n        romImage[0x14E] = (byte) ((checksum014E & 0xFF00) >> 8);\n        romImage[0x14F] = (byte) (checksum014E & 0x00FF);\n    }\n\n    public static boolean validatePaletteData(byte[] romImage) {\n        return getNumberOfPalettes(romImage) > 0 &&\n                findPaletteNameOffset(romImage) > 0 &&\n                findPaletteOffset(romImage) > 0;\n    }\n}\n"
  },
  {
    "path": "src/main/java/utils/StretchIcon.java",
    "content": "package utils;\n\nimport java.awt.Component;\nimport java.awt.Container;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.Image;\nimport java.awt.Insets;\nimport java.awt.RenderingHints;\nimport java.awt.image.BufferedImage;\nimport java.awt.image.ImageObserver;\nimport java.net.URL;\n\nimport javax.swing.ImageIcon;\n\n/**\n * An <CODE>Icon</CODE> that scales its image to fill the component area, excluding any border or insets, optionally maintaining the image's\n * aspect ratio by padding and centering the scaled image horizontally or vertically.\n * <P>\n * The class is a drop-in replacement for <CODE>ImageIcon</CODE>, except that the no-argument constructor is not supported.\n * <P>\n * As the size of the Icon is determined by the size of the component in which it is displayed, <CODE>utils.StretchIcon</CODE> must only be used\n * in conjunction with a component and layout that does not depend on the size of the component's Icon.\n *\n * @version 1.1 01/15/2016\n * @author Darryl\n */\npublic class StretchIcon extends ImageIcon\n{\n    /**\n     *\n     */\n    private static final long serialVersionUID = 1L;\n    /**\n     * Determines whether the aspect ratio of the image is maintained. Set to <code>false</code> to allow th image to distort to fill the\n     * component.\n     */\n    protected boolean         proportionate    = true;\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from an array of bytes.\n     *\n     * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG\n     *\n     * @see ImageIcon#ImageIcon(byte[])\n     */\n    public StretchIcon(byte[] imageData)\n    {\n        super(imageData);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from an array of bytes with the specified behavior.\n     *\n     * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(byte[])\n     */\n    public StretchIcon(byte[] imageData, boolean proportionate)\n    {\n        super(imageData);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from an array of bytes.\n     *\n     * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG\n     * @param description a brief textual description of the image\n     *\n     * @see ImageIcon#ImageIcon(byte[], java.lang.String)\n     */\n    public StretchIcon(byte[] imageData, String description)\n    {\n        super(imageData, description);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from an array of bytes with the specified behavior.\n     *\n     * @see ImageIcon#ImageIcon(byte[])\n     * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG\n     * @param description a brief textual description of the image\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(byte[], java.lang.String)\n     */\n    public StretchIcon(byte[] imageData, String description, boolean proportionate)\n    {\n        super(imageData, description);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the image.\n     *\n     * @param image the image\n     *\n     * @see ImageIcon#ImageIcon(java.awt.Image)\n     */\n    public StretchIcon(Image image)\n    {\n        super(image);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the image with the specified behavior.\n     *\n     * @param image the image\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(java.awt.Image)\n     */\n    public StretchIcon(Image image, boolean proportionate)\n    {\n        super(image);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the image.\n     *\n     * @param image the image\n     * @param description a brief textual description of the image\n     *\n     * @see ImageIcon#ImageIcon(java.awt.Image, java.lang.String)\n     */\n    public StretchIcon(Image image, String description)\n    {\n        super(image, description);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the image with the specified behavior.\n     *\n     * @param image the image\n     * @param description a brief textual description of the image\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(java.awt.Image, java.lang.String)\n     */\n    public StretchIcon(Image image, String description, boolean proportionate)\n    {\n        super(image, description);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified file.\n     *\n     * @param filename a String specifying a filename or path\n     *\n     * @see ImageIcon#ImageIcon(java.lang.String)\n     */\n    public StretchIcon(String filename)\n    {\n        super(filename);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified file with the specified behavior.\n     *\n     * @param filename a String specifying a filename or path\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(java.lang.String)\n     */\n    public StretchIcon(String filename, boolean proportionate)\n    {\n        super(filename);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified file.\n     *\n     * @param filename a String specifying a filename or path\n     * @param description a brief textual description of the image\n     *\n     * @see ImageIcon#ImageIcon(java.lang.String, java.lang.String)\n     */\n    public StretchIcon(String filename, String description)\n    {\n        super(filename, description);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified file with the specified behavior.\n     *\n     * @param filename a String specifying a filename or path\n     * @param description a brief textual description of the image\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(java.awt.Image, java.lang.String)\n     */\n    public StretchIcon(String filename, String description, boolean proportionate)\n    {\n        super(filename, description);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified URL.\n     *\n     * @param location the URL for the image\n     *\n     * @see ImageIcon#ImageIcon(java.net.URL)\n     */\n    public StretchIcon(URL location)\n    {\n        super(location);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified URL with the specified behavior.\n     *\n     * @param location the URL for the image\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(java.net.URL)\n     */\n    public StretchIcon(URL location, boolean proportionate)\n    {\n        super(location);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified URL.\n     *\n     * @param location the URL for the image\n     * @param description a brief textual description of the image\n     *\n     * @see ImageIcon#ImageIcon(java.net.URL, java.lang.String)\n     */\n    public StretchIcon(URL location, String description)\n    {\n        super(location, description);\n    }\n\n    /**\n     * Creates a <CODE>utils.StretchIcon</CODE> from the specified URL with the specified behavior.\n     *\n     * @param location the URL for the image\n     * @param description a brief textual description of the image\n     * @param proportionate <code>true</code> to retain the image's aspect ratio, <code>false</code> to allow distortion of the image to\n     *            fill the component.\n     *\n     * @see ImageIcon#ImageIcon(java.net.URL, java.lang.String)\n     */\n    public StretchIcon(URL location, String description, boolean proportionate)\n    {\n        super(location, description);\n        this.proportionate = proportionate;\n    }\n\n    /**\n     * Paints the icon. The image is reduced or magnified to fit the component to which it is painted.\n     * <P>\n     * If the proportion has not been specified, or has been specified as <code>true</code>, the aspect ratio of the image will be preserved\n     * by padding and centering the image horizontally or vertically. Otherwise the image may be distorted to fill the component it is\n     * painted to.\n     * <P>\n     * If this icon has no image observer,this method uses the <code>c</code> component as the observer.\n     *\n     * @param c the component to which the Icon is painted. This is used as the observer if this icon has no image observer\n     * @param g the graphics context\n     * @param x not used.\n     * @param y not used.\n     *\n     * @see ImageIcon#paintIcon(java.awt.Component, java.awt.Graphics, int, int)\n     */\n    @Override\n    public synchronized void paintIcon(Component c, Graphics g, int x, int y)\n    {\n        Image image = getImage();\n        if (image == null)\n        {\n            return;\n        }\n        Insets insets = ((Container) c).getInsets();\n        x = insets.left;\n        y = insets.top;\n\n        int w = c.getWidth() - x - insets.right;\n        int h = c.getHeight() - y - insets.bottom;\n\n        if (proportionate)\n        {\n            int iw = image.getWidth(c);\n            int ih = image.getHeight(c);\n\n            if ((iw * h) < (ih * w))\n            {\n                iw = (h * iw) / ih;\n                x += (w - iw) / 2;\n                w = iw;\n            }\n            else\n            {\n                ih = (w * ih) / iw;\n                y += (h - ih) / 2;\n                h = ih;\n            }\n        }\n        ImageObserver io = getImageObserver();\n        g.drawImage(image, x, y, w, h, io == null ? c : io);\n    }\n\n    /**\n     * Overridden to return 0. The size of this Icon is determined by the size of the component.\n     *\n     * @return 0\n     */\n    @Override\n    public int getIconWidth()\n    {\n        return 0;\n    }\n\n    /**\n     * Overridden to return 0. The size of this Icon is determined by the size of the component.\n     *\n     * @return 0\n     */\n    @Override\n    public int getIconHeight()\n    {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/main/resources/META-INF/MANIFEST.MF",
    "content": "Manifest-Version: 1.0\nMain-Class: lsdpatch.LSDPatcher\n\n"
  },
  {
    "path": "src/test/java/Document/DocumentTest.java",
    "content": "package Document;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\nclass DocumentTest {\n\n    @Test\n    void savFile() {\n        Document document = new Document();\n        Assertions.assertNotNull(document.savFile());\n    }\n\n    @Test\n    void setSavFile() throws IOException {\n        Document document = new Document();\n        LSDSavFile savFile = document.savFile();\n        Assertions.assertNotNull(savFile);\n        Assertions.assertFalse(document.isSavDirty());\n\n        LSDSavFile newSavFile = new LSDSavFile();\n        document.setSavFile(newSavFile);\n        Assertions.assertFalse(document.isSavDirty());\n\n        File tempFile = File.createTempFile(\"lsdpatcher\", \".sav\");\n        tempFile.deleteOnExit();\n        FileOutputStream fos = new FileOutputStream(tempFile);\n        for (int i = 0; i < 0x8000 * 4; ++i) {\n            fos.write(1);\n        }\n        fos.close();\n        newSavFile.loadFromSav(tempFile.getAbsolutePath());\n        document.setSavFile(newSavFile);\n        Assertions.assertTrue(newSavFile.equals(document.savFile()));\n        Assertions.assertTrue(document.isSavDirty());\n\n        try {\n            document.loadSavFile(\"invalid_path\");\n            Assertions.fail(\"loadSavFile did not throw\");\n        } catch (FileNotFoundException ignored) {\n        }\n\n        document.setSavFile(null);\n        Assertions.assertNull(document.savFile());\n        Assertions.assertFalse(document.isSavDirty());\n    }\n}"
  },
  {
    "path": "src/test/java/Document/LSDSavFileTest.java",
    "content": "package Document;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.Objects;\n\nclass LSDSavFileTest {\n    private LSDSavFile savFile;\n\n    @BeforeEach\n    void createLsdSavFile() {\n        savFile = new LSDSavFile();\n        Arrays.fill(savFile.workRam, (byte)-1); // Resets block allocation table.\n        savFile.workRam[0] = 0; // Satisfies 64 kb SRAM check.\n    }\n\n    @Test\n    @DisplayName(\"Add songs until out of blocks, validate all\")\n    void isValid_addSongsUntilOutOfBlocks() {\n        ClassLoader classLoader = getClass().getClassLoader();\n        File file = new File(Objects.requireNonNull(classLoader.getResource(\"triangle_waves.lsdprj\")).getFile());\n        int addedSongs = 0;\n        try {\n            while (true) {\n                savFile.addSongFromFile(file.getAbsolutePath(), null);\n                ++addedSongs;\n            }\n        } catch (Exception e) {\n            Assertions.assertEquals(e.getMessage(), \"Out of blocks!\");\n        }\n        Assertions.assertEquals(addedSongs, 19);\n        for (int song = 0; song < addedSongs; ++song) {\n            Assertions.assertTrue(savFile.isValid(song));\n        }\n    }\n\n    @Test\n    void testClone() throws CloneNotSupportedException {\n        LSDSavFile savFile = new LSDSavFile();\n        LSDSavFile clone = savFile.clone();\n        Assertions.assertNotNull(clone);\n        Assertions.assertNotSame(savFile, clone);\n    }\n\n    @Test\n    void saveAs() throws Exception {\n        LSDSavFile savFile = new LSDSavFile();\n        File file = File.createTempFile(\"lsdpatcher\", \".sav\");\n        file.deleteOnExit();\n        savFile.saveAs(file.getAbsolutePath());\n    }\n}"
  },
  {
    "path": "src/test/java/kitEditor/SampleTest.java",
    "content": "package kitEditor;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport javax.sound.sampled.UnsupportedAudioFileException;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URL;\n\nclass SampleTest {\n\n    @Test\n    void createFromWav() throws IOException, UnsupportedAudioFileException {\n        ClassLoader classLoader = getClass().getClassLoader();\n        URL url = classLoader.getResource(\"sine1s44khz.wav\");\n        assert url != null;\n        File file = new File(url.getFile());\n        Sample sample = Sample.createFromWav(file, false, false, 0, 0, 0);\n        Assertions.assertNotNull(sample);\n        Assertions.assertEquals(\"sine1s44khz\", sample.getName());\n        Assertions.assertEquals(11467, sample.lengthInSamples());\n        Assertions.assertEquals(5728, sample.lengthInBytes());\n\n        int sum = 0;\n        int min = Integer.MAX_VALUE;\n        int max = Integer.MIN_VALUE;\n        for (int i = 0; i < sample.lengthInSamples(); ++i) {\n            int s = sample.read();\n            sum += s;\n            min = Math.min(s, min);\n            max = Math.max(s, max);\n        }\n        int avg = sum / sample.lengthInSamples();\n        Assertions.assertEquals(0, avg);\n        Assertions.assertEquals(-Short.MAX_VALUE, min);\n        Assertions.assertEquals(Short.MAX_VALUE, max);\n    }\n\n    @Test\n    void decreaseVolume() throws IOException, UnsupportedAudioFileException {\n        ClassLoader classLoader = getClass().getClassLoader();\n        URL url = classLoader.getResource(\"sine1s44khz.wav\");\n        assert url != null;\n        File file = new File(url.getFile());\n\n        Sample sample = Sample.createFromWav(file, false, false, -20, 0, 0);\n\n        int sum = 0;\n        int min = Integer.MAX_VALUE;\n        int max = Integer.MIN_VALUE;\n        for (int i = 0; i < sample.lengthInSamples(); ++i) {\n            int s = sample.read();\n            sum += s;\n            min = Math.min(s, min);\n            max = Math.max(s, max);\n        }\n        int avg = sum / sample.lengthInSamples();\n        Assertions.assertEquals(0, avg);\n        Assertions.assertEquals(Short.MIN_VALUE / 10, min);\n        Assertions.assertEquals(Short.MAX_VALUE / 10, max);\n    }\n\n    @Test\n    void trim() throws IOException, UnsupportedAudioFileException {\n        ClassLoader classLoader = getClass().getClassLoader();\n        URL url = classLoader.getResource(\"sine1s44khz.wav\");\n        assert url != null;\n        File file = new File(url.getFile());\n\n        Sample sample = Sample.createFromWav(file, false, false, 0, 0, 0);\n        Assertions.assertEquals(11467, sample.untrimmedLengthInSamples());\n        Assertions.assertEquals(11467, sample.lengthInSamples());\n\n        sample = Sample.createFromWav(file, false, false, 0, 1, 0);\n        Assertions.assertEquals(11467, sample.untrimmedLengthInSamples());\n        Assertions.assertEquals(11435, sample.lengthInSamples());\n\n        sample = Sample.createFromWav(file, false, false, 0, 10000, 0);\n        Assertions.assertEquals(11467, sample.untrimmedLengthInSamples());\n        Assertions.assertEquals(32, sample.lengthInSamples());\n    }\n\n    @Test\n    void pitch() throws IOException, UnsupportedAudioFileException {\n        ClassLoader classLoader = getClass().getClassLoader();\n        URL url = classLoader.getResource(\"sine1s44khz.wav\");\n        assert url != null;\n        File file = new File(url.getFile());\n\n        Sample sample = Sample.createFromWav(file, false, false, 0, 0, 0);\n        Assertions.assertEquals(11467, sample.lengthInSamples());\n\n        // octave down\n        sample = Sample.createFromWav(file, false, false, 0, 0, -12);\n        Assertions.assertEquals(11467 * 2 + 1, sample.lengthInSamples());\n\n        // octave up\n        sample = Sample.createFromWav(file, false, false, 0, 0, 12);\n        Assertions.assertEquals(11467 / 2, sample.lengthInSamples());\n    }\n}"
  }
]