/* * This file is part of libsidplayfp, a SID player engine. * * Copyright 2011-2015 Leandro Nini * Copyright 2007-2010 Antti Lankila * Copyright 2000 Simon White * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "SidTuneBase.h" #include #include #include #include #include #include #include #include "SmartPtr.h" #include "SidTuneTools.h" #include "SidTuneInfoImpl.h" #include "sidendian.h" #include "sidmemory.h" #include "stringutils.h" #include "MUS.h" #include "p00.h" #include "prg.h" #include "PSID.h" namespace libsidplayfp { // Error and status message strings. const char ERR_EMPTY[] = "SIDTUNE ERROR: No data to load"; const char ERR_UNRECOGNIZED_FORMAT[] = "SIDTUNE ERROR: Could not determine file format"; const char ERR_NOT_ENOUGH_MEMORY[] = "SIDTUNE ERROR: Not enough free memory"; const char ERR_CANT_LOAD_FILE[] = "SIDTUNE ERROR: Could not load input file"; const char ERR_CANT_OPEN_FILE[] = "SIDTUNE ERROR: Could not open file for binary input"; const char ERR_FILE_TOO_LONG[] = "SIDTUNE ERROR: Input data too long"; const char ERR_DATA_TOO_LONG[] = "SIDTUNE ERROR: Size of music data exceeds C64 memory"; const char ERR_BAD_ADDR[] = "SIDTUNE ERROR: Bad address data"; const char ERR_BAD_RELOC[] = "SIDTUNE ERROR: Bad reloc data"; const char ERR_CORRUPT[] = "SIDTUNE ERROR: File is incomplete or corrupt"; const char SidTuneBase::ERR_TRUNCATED[] = "SIDTUNE ERROR: File is most likely truncated"; const char SidTuneBase::ERR_INVALID[] = "SIDTUNE ERROR: File contains invalid data"; /** * Petscii to Ascii conversion table (0x01 = no output). */ const char CHR_tab[256] = { 0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x0d,0x01,0x01, 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, 0x20,0x21,0x01,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f, 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x3b,0x3c,0x3d,0x3e,0x3f, 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f, 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0x5b,0x24,0x5d,0x20,0x20, // alternative: CHR$(92=0x5c) => ISO Latin-1(0xa3) 0x2d,0x23,0x7c,0x2d,0x2d,0x2d,0x2d,0x7c,0x7c,0x5c,0x5c,0x2f,0x5c,0x5c,0x2f,0x2f, 0x5c,0x23,0x5f,0x23,0x7c,0x2f,0x58,0x4f,0x23,0x7c,0x23,0x2b,0x7c,0x7c,0x26,0x5c, // 0x80-0xFF 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, 0x20,0x7c,0x23,0x2d,0x2d,0x7c,0x23,0x7c,0x23,0x2f,0x7c,0x7c,0x2f,0x5c,0x5c,0x2d, 0x2f,0x2d,0x2d,0x7c,0x7c,0x7c,0x7c,0x2d,0x2d,0x2d,0x2f,0x5c,0x5c,0x2f,0x2f,0x23, 0x2d,0x23,0x7c,0x2d,0x2d,0x2d,0x2d,0x7c,0x7c,0x5c,0x5c,0x2f,0x5c,0x5c,0x2f,0x2f, 0x5c,0x23,0x5f,0x23,0x7c,0x2f,0x58,0x4f,0x23,0x7c,0x23,0x2b,0x7c,0x7c,0x26,0x5c, 0x20,0x7c,0x23,0x2d,0x2d,0x7c,0x23,0x7c,0x23,0x2f,0x7c,0x7c,0x2f,0x5c,0x5c,0x2d, 0x2f,0x2d,0x2d,0x7c,0x7c,0x7c,0x7c,0x2d,0x2d,0x2d,0x2f,0x5c,0x5c,0x2f,0x2f,0x23 }; /// The Commodore 64 memory size const uint_least32_t MAX_MEMORY = 65536; /// C64KB + LOAD + PSID const uint_least32_t MAX_FILELEN = MAX_MEMORY + 2 + 0x7C; /// Minimum load address for real c64 only tunes const uint_least16_t SIDTUNE_R64_MIN_LOAD_ADDR = 0x07e8; SidTuneBase* SidTuneBase::load(const char* fileName, const char **fileNameExt, bool separatorIsSlash) { if (fileName == nullptr) return nullptr; #if !defined(SIDTUNE_NO_STDIN_LOADER) // Filename "-" is used as a synonym for standard input. if (strcmp(fileName, "-") == 0) return getFromStdIn(); #endif return getFromFiles(fileName, fileNameExt, separatorIsSlash); } SidTuneBase* SidTuneBase::read(const uint_least8_t* sourceBuffer, uint_least32_t bufferLen) { return getFromBuffer(sourceBuffer, bufferLen); } const SidTuneInfo* SidTuneBase::getInfo() const { return info.get(); } const SidTuneInfo* SidTuneBase::getInfo(unsigned int songNum) { selectSong(songNum); return info.get(); } unsigned int SidTuneBase::selectSong(unsigned int selectedSong) { // First, check whether selected song is valid. if (selectedSong > info->m_songs || selectedSong > MAX_SONGS) { return info->m_currentSong; } // Determine and set starting song number. const unsigned int song = (selectedSong == 0) ? info->m_startSong : selectedSong; // Copy any song-specific variable information // such a speed/clock setting to the info structure. info->m_currentSong = song; // Retrieve song speed definition. switch (info->m_compatibility) { case SidTuneInfo::COMPATIBILITY_R64: info->m_songSpeed = SidTuneInfo::SPEED_CIA_1A; break; case SidTuneInfo::COMPATIBILITY_PSID: // This does not take into account the PlaySID bug upon evaluating the // SPEED field. It would most likely break compatibility to lots of // sidtunes, which have been converted from .SID format and vice versa. // The .SID format does the bit-wise/song-wise evaluation of the SPEED // value correctly, like it is described in the PlaySID documentation. info->m_songSpeed = songSpeed[(song - 1) & 31]; break; default: info->m_songSpeed = songSpeed[song - 1]; break; } info->m_clockSpeed = clockSpeed[song - 1]; return info->m_currentSong; } // ------------------------------------------------- private member functions void SidTuneBase::placeSidTuneInC64mem(sidmemory& mem) { // The Basic ROM sets these values on loading a file. // Program end address const uint_least16_t start = info->m_loadAddr; const uint_least16_t end = start + info->m_c64dataLen; mem.writeMemWord(0x2d, end); // Variables start mem.writeMemWord(0x2f, end); // Arrays start mem.writeMemWord(0x31, end); // Strings start mem.writeMemWord(0xac, start); mem.writeMemWord(0xae, end); // Copy data from cache to the correct destination. mem.fillRam(info->m_loadAddr, &cache[fileOffset], info->m_c64dataLen); } void SidTuneBase::loadFile(const char* fileName, buffer_t& bufferRef) { std::ifstream inFile(fileName, std::ifstream::binary); if (!inFile.is_open()) { throw loadError(ERR_CANT_OPEN_FILE); } inFile.seekg(0, inFile.end); const int fileLen = inFile.tellg(); if (fileLen <= 0) { throw loadError(ERR_EMPTY); } inFile.seekg(0, inFile.beg); buffer_t fileBuf; fileBuf.reserve(fileLen); try { fileBuf.assign(std::istreambuf_iterator(inFile), std::istreambuf_iterator()); } catch (std::exception &ex) { throw loadError(ex.what()); } if (inFile.bad()) { throw loadError(ERR_CANT_LOAD_FILE); } inFile.close(); bufferRef.swap(fileBuf); } SidTuneBase::SidTuneBase() : info(new SidTuneInfoImpl()), fileOffset(0) { // Initialize the object with some safe defaults. for (unsigned int si = 0; si < MAX_SONGS; si++) { songSpeed[si] = info->m_songSpeed; clockSpeed[si] = info->m_clockSpeed; } } #if !defined(SIDTUNE_NO_STDIN_LOADER) SidTuneBase* SidTuneBase::getFromStdIn() { buffer_t fileBuf; // We only read as much as fits in the buffer. // This way we avoid choking on huge data. char datb; while (std::cin.get(datb) && fileBuf.size() < MAX_FILELEN) { fileBuf.push_back((uint_least8_t)datb); } return getFromBuffer(&fileBuf.front(), fileBuf.size()); } #endif SidTuneBase* SidTuneBase::getFromBuffer(const uint_least8_t* const buffer, uint_least32_t bufferLen) { if (buffer == nullptr || bufferLen == 0) { throw loadError(ERR_EMPTY); } if (bufferLen > MAX_FILELEN) { throw loadError(ERR_FILE_TOO_LONG); } buffer_t buf1(buffer, buffer + bufferLen); // Here test for the possible single file formats. std::unique_ptr s(PSID::load(buf1)); if (s.get() == nullptr) { buffer_t buf2; // empty s.reset(MUS::load(buf1, buf2, 0, true)); } if (s.get() != nullptr) { s->acceptSidTune("-", "-", buf1, false); return s.release(); } throw loadError(ERR_UNRECOGNIZED_FORMAT); } void SidTuneBase::acceptSidTune(const char* dataFileName, const char* infoFileName, buffer_t& buf, bool isSlashedFileName) { // Make a copy of the data file name and path, if available. if (dataFileName != nullptr) { const size_t fileNamePos = isSlashedFileName ? SidTuneTools::slashedFileNameWithoutPath(dataFileName) : SidTuneTools::fileNameWithoutPath(dataFileName); info->m_path = std::string(dataFileName, fileNamePos); info->m_dataFileName = std::string(dataFileName + fileNamePos); } // Make a copy of the info file name, if available. if (infoFileName != nullptr) { const size_t fileNamePos = isSlashedFileName ? SidTuneTools::slashedFileNameWithoutPath(infoFileName) : SidTuneTools::fileNameWithoutPath(infoFileName); info->m_infoFileName = std::string(infoFileName + fileNamePos); } // Fix bad sidtune set up. if (info->m_songs > MAX_SONGS) { info->m_songs = MAX_SONGS; } else if (info->m_songs == 0) { info->m_songs = 1; } if (info->m_startSong == 0 || info->m_startSong > info->m_songs) { info->m_startSong = 1; } info->m_dataFileLen = buf.size(); info->m_c64dataLen = buf.size() - fileOffset; // Calculate any remaining addresses and then // confirm all the file details are correct resolveAddrs(&buf[fileOffset]); if (checkRelocInfo() == false) { throw loadError(ERR_BAD_RELOC); } if (checkCompatibility() == false) { throw loadError(ERR_BAD_ADDR); } if (info->m_dataFileLen >= 2) { // We only detect an offset of two. Some position independent // sidtunes contain a load address of 0xE000, but are loaded // to 0x0FFE and call player at 0x1000. info->m_fixLoad = (endian_little16(&buf[fileOffset])==(info->m_loadAddr+2)); } // Check the size of the data. if (info->m_c64dataLen > MAX_MEMORY) { throw loadError(ERR_DATA_TOO_LONG); } else if (info->m_c64dataLen == 0) { throw loadError(ERR_EMPTY); } cache.swap(buf); } void SidTuneBase::createNewFileName(std::string& destString, const char* sourceName, const char* sourceExt) { destString.assign(sourceName); destString.erase(destString.find_last_of('.')); destString.append(sourceExt); } // Initializing the object based upon what we find in the specified file. SidTuneBase* SidTuneBase::getFromFiles(const char* fileName, const char **fileNameExtensions, bool separatorIsSlash) { buffer_t fileBuf1; loadFile(fileName, fileBuf1); // File loaded. Now check if it is in a valid single-file-format. std::unique_ptr s(PSID::load(fileBuf1)); if (s.get() == nullptr) { buffer_t fileBuf2; // Try some native C64 file formats s.reset(MUS::load(fileBuf1, fileBuf2, 0, true)); if (s.get() != nullptr) { // Try to find second file. std::string fileName2; int n = 0; while (fileNameExtensions[n] != nullptr) { createNewFileName(fileName2, fileName, fileNameExtensions[n]); // 1st data file was loaded into "fileBuf1", // so we load the 2nd one into "fileBuf2". // Do not load the first file again if names are equal. if (!stringutils::equal(fileName, fileName2.data(), fileName2.size())) { try { loadFile(fileName2.c_str(), fileBuf2); // Check if tunes in wrong order and therefore swap them here if (stringutils::equal(fileNameExtensions[n], ".mus")) { std::unique_ptr s2(MUS::load(fileBuf2, fileBuf1, 0, true)); if (s2.get() != nullptr) { s2->acceptSidTune(fileName2.c_str(), fileName, fileBuf2, separatorIsSlash); return s2.release(); } } else { std::unique_ptr s2(MUS::load(fileBuf1, fileBuf2, 0, true)); if (s2.get() != nullptr) { s2->acceptSidTune(fileName, fileName2.c_str(), fileBuf1, separatorIsSlash); return s2.release(); } } // The first tune loaded ok, so ignore errors on the // second tune, may find an ok one later } catch (loadError const &) {} } n++; } s->acceptSidTune(fileName, nullptr, fileBuf1, separatorIsSlash); return s.release(); } } if (s.get() == nullptr) s.reset(p00::load(fileName, fileBuf1)); if (s.get() == nullptr) s.reset(prg::load(fileName, fileBuf1)); if (s.get() != nullptr) { s->acceptSidTune(fileName, nullptr, fileBuf1, separatorIsSlash); return s.release(); } throw loadError(ERR_UNRECOGNIZED_FORMAT); } void SidTuneBase::convertOldStyleSpeedToTables(uint_least32_t speed, SidTuneInfo::clock_t clock) { // Create the speed/clock setting tables. // // This routine implements the PSIDv2NG compliant speed conversion. All tunes // above 32 use the same song speed as tune 32 // NOTE: The cast here is used to avoid undefined references // as the std::min function takes its parameters by reference const unsigned int toDo = std::min(info->m_songs, static_cast(MAX_SONGS)); for (unsigned int s = 0; s < toDo; s++) { clockSpeed[s] = clock; songSpeed[s] = (speed & 1) ? SidTuneInfo::SPEED_CIA_1A : SidTuneInfo::SPEED_VBI; if (s < 31) { speed >>= 1; } } } bool SidTuneBase::checkRelocInfo() { // Fix relocation information if (info->m_relocStartPage == 0xFF) { info->m_relocPages = 0; return true; } else if (info->m_relocPages == 0) { info->m_relocStartPage = 0; return true; } // Calculate start/end page const uint_least8_t startp = info->m_relocStartPage; const uint_least8_t endp = (startp + info->m_relocPages - 1) & 0xff; if (endp < startp) { return false; } { // Check against load range const uint_least8_t startlp = (uint_least8_t) (info->m_loadAddr >> 8); const uint_least8_t endlp = startlp + (uint_least8_t) ((info->m_c64dataLen - 1) >> 8); if (((startp <= startlp) && (endp >= startlp)) || ((startp <= endlp) && (endp >= endlp))) { return false; } } // Check that the relocation information does not use the following // memory areas: 0x0000-0x03FF, 0xA000-0xBFFF and 0xD000-0xFFFF if ((startp < 0x04) || ((0xa0 <= startp) && (startp <= 0xbf)) || (startp >= 0xd0) || ((0xa0 <= endp) && (endp <= 0xbf)) || (endp >= 0xd0)) { return false; } return true; } void SidTuneBase::resolveAddrs(const uint_least8_t *c64data) { // Originally used as a first attempt at an RSID // style format. Now reserved for future use if (info->m_playAddr == 0xffff) { info->m_playAddr = 0; } // loadAddr = 0 means, the address is stored in front of the C64 data. if (info->m_loadAddr == 0) { if (info->m_c64dataLen < 2) { throw loadError(ERR_CORRUPT); } info->m_loadAddr = endian_16(*(c64data+1), *c64data); fileOffset += 2; info->m_c64dataLen -= 2; } if (info->m_compatibility == SidTuneInfo::COMPATIBILITY_BASIC) { if (info->m_initAddr != 0) { throw loadError(ERR_BAD_ADDR); } } else if (info->m_initAddr == 0) { info->m_initAddr = info->m_loadAddr; } } bool SidTuneBase::checkCompatibility() { if (info->m_compatibility == SidTuneInfo::COMPATIBILITY_R64) { // Check valid init address switch (info->m_initAddr >> 12) { case 0x0A: case 0x0B: case 0x0D: case 0x0E: case 0x0F: return false; default: if ((info->m_initAddr < info->m_loadAddr) || (info->m_initAddr > (info->m_loadAddr + info->m_c64dataLen - 1))) { return false; } } // Check tune is loadable on a real C64 if (info->m_loadAddr < SIDTUNE_R64_MIN_LOAD_ADDR) { return false; } } return true; } std::string SidTuneBase::petsciiToAscii(SmartPtr_sidtt& spPet) { std::string buffer; char c; do { c = CHR_tab[*spPet]; // ASCII CHR$ conversion if ((c >= 0x20) && (buffer.length() <= 31)) buffer.push_back(c); // copy to info string // If character is 0x9d (left arrow key) then move back. if ((*spPet == 0x9d) && (!buffer.empty())) { buffer.resize(buffer.size() - 1); } spPet++; } while (!((c == 0x0D) || (c == 0x00) || spPet.fail())); return buffer; } }