/* * SampleFormats.cpp * ----------------- * Purpose: Code for loading various more or less common sample and instrument formats. * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Sndfile.h" #include "mod_specifications.h" #ifdef MODPLUG_TRACKER #include "../mptrack/Moddoc.h" #include "../mptrack/TrackerSettings.h" #include "Dlsbank.h" #endif //MODPLUG_TRACKER #include "../soundlib/AudioCriticalSection.h" #ifndef MODPLUG_NO_FILESAVE #include "../common/mptFileIO.h" #endif #include "../common/misc_util.h" #include "Tagging.h" #include "ITTools.h" #include "XMTools.h" #include "S3MTools.h" #include "WAVTools.h" #include "../common/version.h" #include "Loaders.h" #include "ChunkReader.h" #include "modsmp_ctrl.h" #include "../soundbase/SampleFormatConverters.h" #include "../soundbase/SampleFormatCopy.h" #include "../soundlib/ModSampleCopy.h" #include OPENMPT_NAMESPACE_BEGIN bool CSoundFile::ReadSampleFromFile(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize, bool includeInstrumentFormats) { if(!nSample || nSample >= MAX_SAMPLES) return false; if(!ReadWAVSample(nSample, file, mayNormalize) && !(includeInstrumentFormats && ReadXISample(nSample, file)) && !(includeInstrumentFormats && ReadITISample(nSample, file)) && !ReadW64Sample(nSample, file) && !ReadCAFSample(nSample, file) && !ReadAIFFSample(nSample, file, mayNormalize) && !ReadITSSample(nSample, file) && !(includeInstrumentFormats && ReadPATSample(nSample, file)) && !ReadIFFSample(nSample, file) && !ReadS3ISample(nSample, file) && !ReadSBISample(nSample, file) && !ReadAUSample(nSample, file, mayNormalize) && !ReadFLACSample(nSample, file) && !ReadOpusSample(nSample, file) && !ReadVorbisSample(nSample, file) && !ReadMP3Sample(nSample, file, false) && !ReadMediaFoundationSample(nSample, file) ) { return false; } if(nSample > GetNumSamples()) { m_nSamples = nSample; } if(Samples[nSample].uFlags[CHN_ADLIB]) { InitOPL(); } return true; } bool CSoundFile::ReadInstrumentFromFile(INSTRUMENTINDEX nInstr, FileReader &file, bool mayNormalize) { if ((!nInstr) || (nInstr >= MAX_INSTRUMENTS)) return false; if(!ReadITIInstrument(nInstr, file) && !ReadXIInstrument(nInstr, file) && !ReadPATInstrument(nInstr, file) && !ReadSFZInstrument(nInstr, file) // Generic read && !ReadSampleAsInstrument(nInstr, file, mayNormalize)) { bool ok = false; #ifdef MODPLUG_TRACKER CDLSBank bank; if(bank.Open(file)) { ok = bank.ExtractInstrument(*this, nInstr, 0, 0); } #endif // MODPLUG_TRACKER if(!ok) return false; } if(nInstr > GetNumInstruments()) m_nInstruments = nInstr; return true; } bool CSoundFile::ReadSampleAsInstrument(INSTRUMENTINDEX nInstr, FileReader &file, bool mayNormalize) { // Scanning free sample SAMPLEINDEX nSample = GetNextFreeSample(nInstr); // may also return samples which are only referenced by the current instrument if(nSample == SAMPLEINDEX_INVALID) { return false; } // Loading Instrument ModInstrument *pIns = new (std::nothrow) ModInstrument(nSample); if(pIns == nullptr) { return false; } if(!ReadSampleFromFile(nSample, file, mayNormalize, false)) { delete pIns; return false; } // Remove all samples which are only referenced by the old instrument, except for the one we just loaded our new sample into. RemoveInstrumentSamples(nInstr, nSample); // Replace the instrument DestroyInstrument(nInstr, doNoDeleteAssociatedSamples); Instruments[nInstr] = pIns; #if defined(MPT_ENABLE_FILEIO) && defined(MPT_EXTERNAL_SAMPLES) SetSamplePath(nSample, file.GetFileName()); #endif return true; } bool CSoundFile::DestroyInstrument(INSTRUMENTINDEX nInstr, deleteInstrumentSamples removeSamples) { if(nInstr == 0 || nInstr >= MAX_INSTRUMENTS || !Instruments[nInstr]) return true; if(removeSamples == deleteAssociatedSamples) { RemoveInstrumentSamples(nInstr); } CriticalSection cs; ModInstrument *pIns = Instruments[nInstr]; Instruments[nInstr] = nullptr; for(auto &chn : m_PlayState.Chn) { if(chn.pModInstrument == pIns) chn.pModInstrument = nullptr; } delete pIns; return true; } // Remove all unused samples from the given nInstr and keep keepSample if provided bool CSoundFile::RemoveInstrumentSamples(INSTRUMENTINDEX nInstr, SAMPLEINDEX keepSample) { if(Instruments[nInstr] == nullptr) { return false; } std::vector keepSamples(GetNumSamples() + 1, true); // Check which samples are used by the instrument we are going to nuke. auto referencedSamples = Instruments[nInstr]->GetSamples(); for(auto sample : referencedSamples) { if(sample <= GetNumSamples()) { keepSamples[sample] = false; } } // If we want to keep a specific sample, do so. if(keepSample != SAMPLEINDEX_INVALID) { if(keepSample <= GetNumSamples()) { keepSamples[keepSample] = true; } } // Check if any of those samples are referenced by other instruments as well, in which case we want to keep them of course. for(INSTRUMENTINDEX nIns = 1; nIns <= GetNumInstruments(); nIns++) if (Instruments[nIns] != nullptr && nIns != nInstr) { Instruments[nIns]->GetSamples(keepSamples); } // Now nuke the selected samples. RemoveSelectedSamples(keepSamples); return true; } //////////////////////////////////////////////////////////////////////////////// // // I/O From another song // bool CSoundFile::ReadInstrumentFromSong(INSTRUMENTINDEX targetInstr, const CSoundFile &srcSong, INSTRUMENTINDEX sourceInstr) { if ((!sourceInstr) || (sourceInstr > srcSong.GetNumInstruments()) || (targetInstr >= MAX_INSTRUMENTS) || (!srcSong.Instruments[sourceInstr])) { return false; } if (m_nInstruments < targetInstr) m_nInstruments = targetInstr; ModInstrument *pIns = new (std::nothrow) ModInstrument(); if(pIns == nullptr) { return false; } DestroyInstrument(targetInstr, deleteAssociatedSamples); Instruments[targetInstr] = pIns; *pIns = *srcSong.Instruments[sourceInstr]; std::vector sourceSample; // Sample index in source song std::vector targetSample; // Sample index in target song SAMPLEINDEX targetIndex = 0; // Next index for inserting sample for(auto &sample : pIns->Keyboard) { const SAMPLEINDEX sourceIndex = sample; if(sourceIndex > 0 && sourceIndex <= srcSong.GetNumSamples()) { const auto entry = std::find(sourceSample.cbegin(), sourceSample.cend(), sourceIndex); if(entry == sourceSample.end()) { // Didn't consider this sample yet, so add it to our map. targetIndex = GetNextFreeSample(targetInstr, targetIndex + 1); if(targetIndex <= GetModSpecifications().samplesMax) { sourceSample.push_back(sourceIndex); targetSample.push_back(targetIndex); sample = targetIndex; } else { sample = 0; } } else { // Sample reference has already been created, so only need to update the sample map. sample = *(entry - sourceSample.begin() + targetSample.begin()); } } else { // Invalid or no source sample sample = 0; } } #ifdef MODPLUG_TRACKER if(!strcmp(pIns->filename, "") && srcSong.GetpModDoc() != nullptr && &srcSong != this) { mpt::String::Copy(pIns->filename, srcSong.GetpModDoc()->GetPathNameMpt().GetFullFileName().ToLocale()); } #endif pIns->Convert(srcSong.GetType(), GetType()); // Copy all referenced samples over for(size_t i = 0; i < targetSample.size(); i++) { ReadSampleFromSong(targetSample[i], srcSong, sourceSample[i]); } return true; } bool CSoundFile::ReadSampleFromSong(SAMPLEINDEX targetSample, const CSoundFile &srcSong, SAMPLEINDEX sourceSample) { if(!sourceSample || sourceSample > srcSong.GetNumSamples() || (targetSample >= GetModSpecifications().samplesMax && targetSample > GetNumSamples())) { return false; } DestroySampleThreadsafe(targetSample); const ModSample &sourceSmp = srcSong.GetSample(sourceSample); ModSample &targetSmp = GetSample(targetSample); if(GetNumSamples() < targetSample) m_nSamples = targetSample; targetSmp = sourceSmp; strcpy(m_szNames[targetSample], srcSong.m_szNames[sourceSample]); if(sourceSmp.HasSampleData()) { targetSmp.pData.pSample = nullptr; // Don't want to delete the original sample! if(targetSmp.AllocateSample()) { SmpLength nSize = sourceSmp.GetSampleSizeInBytes(); memcpy(targetSmp.sampleb(), sourceSmp.sampleb(), nSize); targetSmp.PrecomputeLoops(*this, false); } // Remember on-disk path (for MPTM files), but don't implicitely enable on-disk storage // (we really don't want this for e.g. duplicating samples or splitting stereo samples) #ifdef MPT_EXTERNAL_SAMPLES SetSamplePath(targetSample, srcSong.GetSamplePath(sourceSample)); #endif targetSmp.uFlags.reset(SMP_KEEPONDISK); } #ifdef MODPLUG_TRACKER if(!strcmp(targetSmp.filename, "") && srcSong.GetpModDoc() != nullptr && &srcSong != this) { mpt::String::Copy(targetSmp.filename, mpt::ToCharset(GetCharsetInternal(), srcSong.GetpModDoc()->GetTitle())); } #endif if(targetSmp.uFlags[CHN_ADLIB] && !SupportsOPL()) { AddToLog("OPL instruments are not supported by this format."); } targetSmp.Convert(srcSong.GetType(), GetType()); if(targetSmp.uFlags[CHN_ADLIB]) { InitOPL(); } return true; } //////////////////////////////////////////////////////////////////////// // IMA ADPCM Support for WAV files static bool IMAADPCMUnpack16(int16 *target, SmpLength sampleLen, FileReader file, uint16 blockAlign, uint32 numChannels) { static const int32 IMAIndexTab[8] = { -1, -1, -1, -1, 2, 4, 6, 8 }; static const int32 IMAUnpackTable[90] = { 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767, 0 }; if(target == nullptr || blockAlign < 4u * numChannels) return false; SmpLength samplePos = 0; sampleLen *= numChannels; while(file.CanRead(4u * numChannels) && samplePos < sampleLen) { FileReader block = file.ReadChunk(blockAlign); FileReader::PinnedRawDataView blockView = block.GetPinnedRawDataView(); const mpt::byte *data = blockView.data(); const uint32 blockSize = static_cast(blockView.size()); for(uint32 chn = 0; chn < numChannels; chn++) { // Block header int32 value = block.ReadInt16LE(); int32 nIndex = block.ReadUint8(); Limit(nIndex, 0, 89); block.Skip(1); SmpLength smpPos = samplePos + chn; uint32 dataPos = (numChannels + chn) * 4; // Block data while(smpPos <= (sampleLen - 8) && dataPos <= (blockSize - 4)) { for(uint32 i = 0; i < 8; i++) { uint8 delta = mpt::byte_cast(data[dataPos]); if(i & 1) { delta >>= 4; dataPos++; } else { delta &= 0x0F; } int32 v = IMAUnpackTable[nIndex] >> 3; if (delta & 1) v += IMAUnpackTable[nIndex] >> 2; if (delta & 2) v += IMAUnpackTable[nIndex] >> 1; if (delta & 4) v += IMAUnpackTable[nIndex]; if (delta & 8) value -= v; else value += v; nIndex += IMAIndexTab[delta & 7]; Limit(nIndex, 0, 88); Limit(value, -32768, 32767); target[smpPos] = static_cast(value); smpPos += numChannels; } dataPos += (numChannels - 1) * 4u; } } samplePos += ((blockSize - (numChannels * 4u)) * 2u); } return true; } //////////////////////////////////////////////////////////////////////////////// // WAV Open bool CSoundFile::ReadWAVSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize, FileReader *wsmpChunk) { WAVReader wavFile(file); if(!wavFile.IsValid() || wavFile.GetNumChannels() == 0 || wavFile.GetNumChannels() > 2 || (wavFile.GetBitsPerSample() == 0 && wavFile.GetSampleFormat() != WAVFormatChunk::fmtMP3) || (wavFile.GetBitsPerSample() > 64) || (wavFile.GetSampleFormat() != WAVFormatChunk::fmtPCM && wavFile.GetSampleFormat() != WAVFormatChunk::fmtFloat && wavFile.GetSampleFormat() != WAVFormatChunk::fmtIMA_ADPCM && wavFile.GetSampleFormat() != WAVFormatChunk::fmtMP3 && wavFile.GetSampleFormat() != WAVFormatChunk::fmtALaw && wavFile.GetSampleFormat() != WAVFormatChunk::fmtULaw)) { return false; } DestroySampleThreadsafe(nSample); strcpy(m_szNames[nSample], ""); ModSample &sample = Samples[nSample]; sample.Initialize(); sample.nLength = wavFile.GetSampleLength(); sample.nC5Speed = wavFile.GetSampleRate(); wavFile.ApplySampleSettings(sample, GetCharsetInternal(), m_szNames[nSample]); FileReader sampleChunk = wavFile.GetSampleData(); SampleIO sampleIO( SampleIO::_8bit, (wavFile.GetNumChannels() > 1) ? SampleIO::stereoInterleaved : SampleIO::mono, SampleIO::littleEndian, SampleIO::signedPCM); if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtIMA_ADPCM && wavFile.GetNumChannels() <= 2) { // IMA ADPCM 4:1 LimitMax(sample.nLength, MAX_SAMPLE_LENGTH); sample.uFlags.set(CHN_16BIT); sample.uFlags.set(CHN_STEREO, wavFile.GetNumChannels() == 2); if(!sample.AllocateSample()) { return false; } IMAADPCMUnpack16(sample.sample16(), sample.nLength, sampleChunk, wavFile.GetBlockAlign(), wavFile.GetNumChannels()); sample.PrecomputeLoops(*this, false); } else if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtMP3) { // MP3 in WAV bool loadedMP3 = ReadMP3Sample(nSample, sampleChunk, false, true) || ReadMediaFoundationSample(nSample, sampleChunk, true); if(!loadedMP3) { return false; } } else if(!wavFile.IsExtensibleFormat() && wavFile.MayBeCoolEdit16_8() && wavFile.GetSampleFormat() == WAVFormatChunk::fmtPCM && wavFile.GetBitsPerSample() == 32 && wavFile.GetBlockAlign() == wavFile.GetNumChannels() * 4) { // Syntrillium Cool Edit hack to store IEEE 32bit floating point // Format is described as 32bit integer PCM contained in 32bit blocks and an WAVEFORMATEX extension size of 2 which contains a single 16 bit little endian value of 1. // (This is parsed in WAVTools.cpp and returned via MayBeCoolEdit16_8()). // The data actually stored in this case is little endian 32bit floating point PCM with 2**15 full scale. // Cool Edit calls this format "16.8 float". sampleIO |= SampleIO::_32bit; sampleIO |= SampleIO::floatPCM15; sampleIO.ReadSample(sample, sampleChunk); } else if(!wavFile.IsExtensibleFormat() && wavFile.GetSampleFormat() == WAVFormatChunk::fmtPCM && wavFile.GetBitsPerSample() == 24 && wavFile.GetBlockAlign() == wavFile.GetNumChannels() * 4) { // Syntrillium Cool Edit hack to store IEEE 32bit floating point // Format is described as 24bit integer PCM contained in 32bit blocks. // The data actually stored in this case is little endian 32bit floating point PCM with 2**23 full scale. // Cool Edit calls this format "24.0 float". sampleIO |= SampleIO::_32bit; sampleIO |= SampleIO::floatPCM23; sampleIO.ReadSample(sample, sampleChunk); } else if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtALaw || wavFile.GetSampleFormat() == WAVFormatChunk::fmtULaw) { // a-law / u-law sampleIO |= SampleIO::_16bit; sampleIO |= wavFile.GetSampleFormat() == WAVFormatChunk::fmtALaw ? SampleIO::aLaw : SampleIO::uLaw; sampleIO.ReadSample(sample, sampleChunk); } else { // PCM / Float SampleIO::Bitdepth bitDepth; switch((wavFile.GetBitsPerSample() - 1) / 8u) { default: case 0: bitDepth = SampleIO::_8bit; break; case 1: bitDepth = SampleIO::_16bit; break; case 2: bitDepth = SampleIO::_24bit; break; case 3: bitDepth = SampleIO::_32bit; break; case 7: bitDepth = SampleIO::_64bit; break; } sampleIO |= bitDepth; if(wavFile.GetBitsPerSample() <= 8) sampleIO |= SampleIO::unsignedPCM; if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtFloat) sampleIO |= SampleIO::floatPCM; if(mayNormalize) sampleIO.MayNormalize(); sampleIO.ReadSample(sample, sampleChunk); } if(wsmpChunk != nullptr) { // DLS WSMP chunk *wsmpChunk = wavFile.GetWsmpChunk(); } sample.Convert(MOD_TYPE_IT, GetType()); sample.PrecomputeLoops(*this, false); return true; } /////////////////////////////////////////////////////////////// // Save WAV #ifndef MODPLUG_NO_FILESAVE bool CSoundFile::SaveWAVSample(SAMPLEINDEX nSample, std::ostream &f) const { WAVWriter file(&f); if(!file.IsValid()) { return false; } const ModSample &sample = Samples[nSample]; file.WriteFormat(sample.GetSampleRate(GetType()), sample.GetElementarySampleSize() * 8, sample.GetNumChannels(), WAVFormatChunk::fmtPCM); // Write sample data file.StartChunk(RIFFChunk::iddata); file.Skip(SampleIO( sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, sample.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved : SampleIO::mono, SampleIO::littleEndian, sample.uFlags[CHN_16BIT] ? SampleIO::signedPCM : SampleIO::unsignedPCM) .WriteSample(f, sample)); file.WriteLoopInformation(sample); file.WriteExtraInformation(sample, GetType()); if(sample.HasCustomCuePoints()) { file.WriteCueInformation(sample); } FileTags tags; tags.SetEncoder(); tags.title = mpt::ToUnicode(GetCharsetInternal(), m_szNames[nSample]); file.WriteMetatags(tags); return true; } #endif // MODPLUG_NO_FILESAVE ///////////////// // Sony Wave64 // struct Wave64FileHeader { GUIDms GuidRIFF; uint64le FileSize; GUIDms GuidWAVE; }; MPT_BINARY_STRUCT(Wave64FileHeader, 40) struct Wave64ChunkHeader { GUIDms GuidChunk; uint64le Size; }; MPT_BINARY_STRUCT(Wave64ChunkHeader, 24) struct Wave64Chunk { Wave64ChunkHeader header; FileReader::off_t GetLength() const { uint64 length = header.Size; if(length < sizeof(Wave64ChunkHeader)) { length = 0; } else { length -= sizeof(Wave64ChunkHeader); } return mpt::saturate_cast(length); } mpt::UUID GetID() const { return mpt::UUID(header.GuidChunk); } }; MPT_BINARY_STRUCT(Wave64Chunk, 24) static void Wave64TagFromLISTINFO(mpt::ustring & dst, uint16 codePage, const ChunkReader::ChunkList & infoChunk, RIFFChunk::ChunkIdentifiers id) { if(!infoChunk.ChunkExists(id)) { return; } FileReader textChunk = infoChunk.GetChunk(id); if(!textChunk.IsValid()) { return; } std::string str; textChunk.ReadString(str, textChunk.GetLength()); str = mpt::String::Replace(str, std::string("\r\n"), std::string("\n")); str = mpt::String::Replace(str, std::string("\r"), std::string("\n")); dst = mpt::ToUnicode(codePage, mpt::CharsetWindows1252, str); } bool CSoundFile::ReadW64Sample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) { file.Rewind(); constexpr mpt::UUID guidRIFF = "66666972-912E-11CF-A5D6-28DB04C10000"_uuid; constexpr mpt::UUID guidWAVE = "65766177-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; constexpr mpt::UUID guidLIST = "7473696C-912F-11CF-A5D6-28DB04C10000"_uuid; constexpr mpt::UUID guidFMT = "20746D66-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; //constexpr mpt::UUID guidFACT = "74636166-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; constexpr mpt::UUID guidDATA = "61746164-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; //constexpr mpt::UUID guidLEVL = "6C76656C-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; //constexpr mpt::UUID guidJUNK = "6b6E756A-ACF3-11D3-8CD1-00C04f8EDB8A"_uuid; //constexpr mpt::UUID guidBEXT = "74786562-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; //constexpr mpt::UUID guiMARKER = "ABF76256-392D-11D2-86C7-00C04F8EDB8A"_uuid; //constexpr mpt::UUID guiSUMMARYLIST = "925F94BC-525A-11D2-86DC-00C04F8EDB8A"_uuid; constexpr mpt::UUID guidCSET = "54455343-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; Wave64FileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(mpt::UUID(fileHeader.GuidRIFF) != guidRIFF) { return false; } if(mpt::UUID(fileHeader.GuidWAVE) != guidWAVE) { return false; } if(fileHeader.FileSize != file.GetLength()) { return false; } ChunkReader chunkFile = file; auto chunkList = chunkFile.ReadChunks(8); if(!chunkList.ChunkExists(guidFMT)) { return false; } FileReader formatChunk = chunkList.GetChunk(guidFMT); WAVFormatChunk format; if(!formatChunk.ReadStruct(format)) { return false; } uint16 sampleFormat = format.format; if(format.format == WAVFormatChunk::fmtExtensible) { WAVFormatChunkExtension formatExt; if(!formatChunk.ReadStruct(formatExt)) { return false; } sampleFormat = static_cast(mpt::UUID(formatExt.subFormat).GetData1()); } if(format.sampleRate == 0) { return false; } if(format.numChannels == 0) { return false; } if(format.numChannels > 2) { return false; } if(sampleFormat != WAVFormatChunk::fmtPCM && sampleFormat != WAVFormatChunk::fmtFloat) { return false; } if(sampleFormat == WAVFormatChunk::fmtFloat && format.bitsPerSample != 32 && format.bitsPerSample != 64) { return false; } if(sampleFormat == WAVFormatChunk::fmtPCM && format.bitsPerSample > 64) { return false; } SampleIO::Bitdepth bitDepth; switch((format.bitsPerSample - 1) / 8u) { default: case 0: bitDepth = SampleIO::_8bit ; break; case 1: bitDepth = SampleIO::_16bit; break; case 2: bitDepth = SampleIO::_24bit; break; case 3: bitDepth = SampleIO::_32bit; break; case 7: bitDepth = SampleIO::_64bit; break; } SampleIO sampleIO( bitDepth, (format.numChannels > 1) ? SampleIO::stereoInterleaved : SampleIO::mono, SampleIO::littleEndian, (sampleFormat == WAVFormatChunk::fmtFloat) ? SampleIO::floatPCM : SampleIO::signedPCM); if(format.bitsPerSample <= 8) { sampleIO |= SampleIO::unsignedPCM; } if(mayNormalize) { sampleIO.MayNormalize(); } FileTags tags; uint16 codePage = 28591; // mpt::CharsetISO8859_1 FileReader csetChunk = chunkList.GetChunk(guidCSET); if(csetChunk.IsValid()) { if(csetChunk.CanRead(2)) { codePage = csetChunk.ReadUint16LE(); } } if(chunkList.ChunkExists(guidLIST)) { ChunkReader listChunk = chunkList.GetChunk(guidLIST); if(listChunk.ReadMagic("INFO")) { auto infoChunk = listChunk.ReadChunks(2); Wave64TagFromLISTINFO(tags.title, codePage, infoChunk, RIFFChunk::idINAM); Wave64TagFromLISTINFO(tags.encoder, codePage, infoChunk, RIFFChunk::idISFT); //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idICOP); Wave64TagFromLISTINFO(tags.artist, codePage, infoChunk, RIFFChunk::idIART); Wave64TagFromLISTINFO(tags.album, codePage, infoChunk, RIFFChunk::idIPRD); Wave64TagFromLISTINFO(tags.comments, codePage, infoChunk, RIFFChunk::idICMT); //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idIENG); //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idISBJ); Wave64TagFromLISTINFO(tags.genre, codePage, infoChunk, RIFFChunk::idIGNR); //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idICRD); Wave64TagFromLISTINFO(tags.year, codePage, infoChunk, RIFFChunk::idYEAR); Wave64TagFromLISTINFO(tags.trackno, codePage, infoChunk, RIFFChunk::idTRCK); Wave64TagFromLISTINFO(tags.url, codePage, infoChunk, RIFFChunk::idTURL); //Wave64TagFromLISTINFO(tags.bpm, codePage, infoChunk, void); } } if(!chunkList.ChunkExists(guidDATA)) { return false; } FileReader audioData = chunkList.GetChunk(guidDATA); SmpLength length = mpt::saturate_cast(audioData.GetLength() / (sampleIO.GetEncodedBitsPerSample()/8)); ModSample &mptSample = Samples[nSample]; DestroySampleThreadsafe(nSample); mptSample.Initialize(); mptSample.nLength = length; mptSample.nC5Speed = format.sampleRate; sampleIO.ReadSample(mptSample, audioData); mpt::String::Copy(m_szNames[nSample], mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags))); mptSample.Convert(MOD_TYPE_IT, GetType()); mptSample.PrecomputeLoops(*this, false); return true; } #ifndef MODPLUG_NO_FILESAVE /////////////////////////////////////////////////////////////// // Save RAW bool CSoundFile::SaveRAWSample(SAMPLEINDEX nSample, std::ostream &f) const { const ModSample &sample = Samples[nSample]; SampleIO( sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, sample.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved : SampleIO::mono, SampleIO::littleEndian, SampleIO::signedPCM) .WriteSample(f, sample); return true; } #endif // MODPLUG_NO_FILESAVE ///////////////////////////////////////////////////////////// // GUS Patches struct GF1PatchFileHeader { char magic[8]; // "GF1PATCH" char version[4]; // "100", or "110" char id[10]; // "ID#000002" char copyright[60]; // Copyright uint8le numInstr; // Number of instruments in patch uint8le voices; // Number of voices, usually 14 uint8le channels; // Number of wav channels that can be played concurently to the patch uint16le numSamples; // Total number of waveforms for all the .PAT uint16le volume; // Master volume uint32le dataSize; char reserved2[36]; }; MPT_BINARY_STRUCT(GF1PatchFileHeader, 129) struct GF1Instrument { uint16le id; // Instrument id: 0-65535 char name[16]; // Name of instrument. Gravis doesn't seem to use it uint32le size; // Number of bytes for the instrument with header. (To skip to next instrument) uint8 layers; // Number of layers in instrument: 1-4 char reserved[40]; }; MPT_BINARY_STRUCT(GF1Instrument, 63) struct GF1SampleHeader { char name[7]; // null terminated string. name of the wave. uint8le fractions; // Start loop point fraction in 4 bits + End loop point fraction in the 4 other bits. uint32le length; // total size of wavesample. limited to 65535 now by the drivers, not the card. uint32le loopstart; // start loop position in the wavesample uint32le loopend; // end loop position in the wavesample uint16le freq; // Rate at which the wavesample has been sampled uint32le low_freq, high_freq, root_freq; // check note.h for the correspondance. int16le finetune; // fine tune. -512 to +512, EXCLUDING 0 cause it is a multiplier. 512 is one octave off, and 1 is a neutral value uint8le balance; // Balance: 0-15. 0=full left, 15 = full right uint8le env_rate[6]; // attack rates uint8le env_volume[6]; // attack volumes uint8le tremolo_sweep, tremolo_rate, tremolo_depth; uint8le vibrato_sweep, vibrato_rate, vibrato_depth; uint8le flags; int16le scale_frequency; // Note uint16le scale_factor; // 0...2048 (1024 is normal) or 0...2 char reserved[36]; }; MPT_BINARY_STRUCT(GF1SampleHeader, 96) // -- GF1 Envelopes -- // // It can be represented like this (the envelope is totally bogus, it is // just to show the concept): // // | // | /----` | | // | /------/ `\ | | | | | // | / \ | | | | | // | / \ | | | | | // |/ \ | | | | | // ---------------------------- | | | | | | // <---> attack rate 0 0 1 2 3 4 5 amplitudes // <----> attack rate 1 // <> attack rate 2 // <--> attack rate 3 // <> attack rate 4 // <-----> attack rate 5 // // -- GF1 Flags -- // // bit 0: 8/16 bit // bit 1: Signed/Unsigned // bit 2: off/on looping // bit 3: off/on bidirectionnal looping // bit 4: off/on backward looping // bit 5: off/on sustaining (3rd point in env.) // bit 6: off/on envelopes // bit 7: off/on clamped release (6th point, env) struct GF1Layer { uint8le previous; // If !=0 the wavesample to use is from the previous layer. The waveheader is still needed uint8le id; // Layer id: 0-3 uint32le size; // data size in bytes in the layer, without the header. to skip to next layer for example: uint8le samples; // number of wavesamples char reserved[40]; }; MPT_BINARY_STRUCT(GF1Layer, 47) static double PatchFreqToNote(uint32 nFreq) { return std::log(nFreq / 2044.0) * (12.0 * 1.44269504088896340736); // 1.0/std::log(2.0) } static int32 PatchFreqToNoteInt(uint32 nFreq) { return mpt::saturate_round(PatchFreqToNote(nFreq)); } static void PatchToSample(CSoundFile *that, SAMPLEINDEX nSample, GF1SampleHeader &sampleHeader, FileReader &file) { ModSample &sample = that->GetSample(nSample); file.ReadStruct(sampleHeader); sample.Initialize(); if(sampleHeader.flags & 4) sample.uFlags.set(CHN_LOOP); if(sampleHeader.flags & 8) sample.uFlags.set(CHN_PINGPONGLOOP); if(sampleHeader.flags & 16) sample.uFlags.set(CHN_REVERSE); sample.nLength = sampleHeader.length; sample.nLoopStart = sampleHeader.loopstart; sample.nLoopEnd = sampleHeader.loopend; sample.nC5Speed = sampleHeader.freq; sample.nPan = (sampleHeader.balance * 256 + 8) / 15; if(sample.nPan > 256) sample.nPan = 128; else sample.uFlags.set(CHN_PANNING); sample.nVibType = VIB_SINE; sample.nVibSweep = sampleHeader.vibrato_sweep; sample.nVibDepth = sampleHeader.vibrato_depth; sample.nVibRate = sampleHeader.vibrato_rate / 4; if(sampleHeader.scale_factor) { sample.Transpose((84.0 - PatchFreqToNote(sampleHeader.root_freq)) / 12.0); } SampleIO sampleIO( SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, (sampleHeader.flags & 2) ? SampleIO::unsignedPCM : SampleIO::signedPCM); if(sampleHeader.flags & 1) { sampleIO |= SampleIO::_16bit; sample.nLength /= 2; sample.nLoopStart /= 2; sample.nLoopEnd /= 2; } sampleIO.ReadSample(sample, file); sample.Convert(MOD_TYPE_IT, that->GetType()); sample.PrecomputeLoops(*that, false); mpt::String::Read(that->m_szNames[nSample], sampleHeader.name); } bool CSoundFile::ReadPATSample(SAMPLEINDEX nSample, FileReader &file) { file.Rewind(); GF1PatchFileHeader fileHeader; GF1Instrument instrHeader; // We only support one instrument GF1Layer layerHeader; if(!file.ReadStruct(fileHeader) || memcmp(fileHeader.magic, "GF1PATCH", 8) || (memcmp(fileHeader.version, "110\0", 4) && memcmp(fileHeader.version, "100\0", 4)) || memcmp(fileHeader.id, "ID#000002\0", 10) || !fileHeader.numInstr || !fileHeader.numSamples || !file.ReadStruct(instrHeader) //|| !instrHeader.layers // DOO.PAT has 0 layers || !file.ReadStruct(layerHeader) || !layerHeader.samples) { return false; } DestroySampleThreadsafe(nSample); GF1SampleHeader sampleHeader; PatchToSample(this, nSample, sampleHeader, file); if(instrHeader.name[0] > ' ') { mpt::String::Read(m_szNames[nSample], instrHeader.name); } return true; } // PAT Instrument bool CSoundFile::ReadPATInstrument(INSTRUMENTINDEX nInstr, FileReader &file) { file.Rewind(); GF1PatchFileHeader fileHeader; GF1Instrument instrHeader; // We only support one instrument GF1Layer layerHeader; if(!file.ReadStruct(fileHeader) || memcmp(fileHeader.magic, "GF1PATCH", 8) || (memcmp(fileHeader.version, "110\0", 4) && memcmp(fileHeader.version, "100\0", 4)) || memcmp(fileHeader.id, "ID#000002\0", 10) || !fileHeader.numInstr || !fileHeader.numSamples || !file.ReadStruct(instrHeader) //|| !instrHeader.layers // DOO.PAT has 0 layers || !file.ReadStruct(layerHeader) || !layerHeader.samples) { return false; } ModInstrument *pIns = new (std::nothrow) ModInstrument(); if(pIns == nullptr) { return false; } DestroyInstrument(nInstr, deleteAssociatedSamples); if (nInstr > m_nInstruments) m_nInstruments = nInstr; Instruments[nInstr] = pIns; mpt::String::Read(pIns->name, instrHeader.name); pIns->nFadeOut = 2048; if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) { pIns->nNNA = NNA_NOTEOFF; pIns->nDNA = DNA_NOTEFADE; } SAMPLEINDEX nextSample = 0; int32 nMinSmpNote = 0xFF; SAMPLEINDEX nMinSmp = 0; for(uint8 smp = 0; smp < layerHeader.samples; smp++) { // Find a free sample nextSample = GetNextFreeSample(nInstr, nextSample + 1); if(nextSample == SAMPLEINDEX_INVALID) break; if(m_nSamples < nextSample) m_nSamples = nextSample; if(!nMinSmp) nMinSmp = nextSample; // Load it GF1SampleHeader sampleHeader; PatchToSample(this, nextSample, sampleHeader, file); int32 nMinNote = (sampleHeader.low_freq > 100) ? PatchFreqToNoteInt(sampleHeader.low_freq) : 0; int32 nMaxNote = (sampleHeader.high_freq > 100) ? PatchFreqToNoteInt(sampleHeader.high_freq) : NOTE_MAX; int32 nBaseNote = (sampleHeader.root_freq > 100) ? PatchFreqToNoteInt(sampleHeader.root_freq) : -1; if(!sampleHeader.scale_factor && layerHeader.samples == 1) { nMinNote = 0; nMaxNote = NOTE_MAX; } // Fill Note Map for(int32 k = 0; k < NOTE_MAX; k++) { if(k == nBaseNote || (!pIns->Keyboard[k] && k >= nMinNote && k <= nMaxNote)) { if(!sampleHeader.scale_factor) pIns->NoteMap[k] = NOTE_MIDDLEC; pIns->Keyboard[k] = nextSample; if(k < nMinSmpNote) { nMinSmpNote = k; nMinSmp = nextSample; } } } } if(nMinSmp) { // Fill note map and missing samples for(uint8 k = 0; k < NOTE_MAX; k++) { if(!pIns->NoteMap[k]) pIns->NoteMap[k] = k + 1; if(!pIns->Keyboard[k]) { pIns->Keyboard[k] = nMinSmp; } else { nMinSmp = pIns->Keyboard[k]; } } } pIns->Sanitize(MOD_TYPE_IT); pIns->Convert(MOD_TYPE_IT, GetType()); return true; } ///////////////////////////////////////////////////////////// // S3I Samples bool CSoundFile::ReadS3ISample(SAMPLEINDEX nSample, FileReader &file) { file.Rewind(); S3MSampleHeader sampleHeader; if(!file.ReadStruct(sampleHeader) || (sampleHeader.sampleType != S3MSampleHeader::typePCM && sampleHeader.sampleType != S3MSampleHeader::typeAdMel) || (memcmp(sampleHeader.magic, "SCRS", 4) && memcmp(sampleHeader.magic, "SCRI", 4)) || !file.Seek((sampleHeader.dataPointer[1] << 4) | (sampleHeader.dataPointer[2] << 12) | (sampleHeader.dataPointer[0] << 20))) { return false; } DestroySampleThreadsafe(nSample); ModSample &sample = Samples[nSample]; sampleHeader.ConvertToMPT(sample); mpt::String::Read(m_szNames[nSample], sampleHeader.name); if(sampleHeader.sampleType < S3MSampleHeader::typeAdMel) sampleHeader.GetSampleFormat(false).ReadSample(sample, file); else if(SupportsOPL()) InitOPL(); else AddToLog("OPL instruments are not supported by this format."); sample.Convert(MOD_TYPE_S3M, GetType()); sample.PrecomputeLoops(*this, false); return true; } #ifndef MODPLUG_NO_FILESAVE bool CSoundFile::SaveS3ISample(SAMPLEINDEX smp, std::ostream &f) const { const ModSample &sample = Samples[smp]; S3MSampleHeader sampleHeader; MemsetZero(sampleHeader); SmpLength length = sampleHeader.ConvertToS3M(sample); mpt::String::Write(sampleHeader.name, m_szNames[smp]); mpt::String::Write(sampleHeader.reserved2, mpt::ToCharset(mpt::CharsetUTF8, Version::Current().GetOpenMPTVersionString())); if(length) sampleHeader.dataPointer[1] = sizeof(S3MSampleHeader) >> 4; mpt::IO::Write(f, sampleHeader); if(length) sampleHeader.GetSampleFormat(false).WriteSample(f, sample, length); return true; } #endif // MODPLUG_NO_FILESAVE ///////////////////////////////////////////////////////////// // SBI OPL patch files bool CSoundFile::ReadSBISample(SAMPLEINDEX sample, FileReader &file) { file.Rewind(); if(!file.ReadMagic("SBI\x1A") || !file.CanRead(32 + sizeof(OPLPatch)) || file.CanRead(64)) // Arbitrary threshold to reject files that are unlikely to be SBI files return false; if(!SupportsOPL()) { AddToLog("OPL instruments are not supported by this format."); return true; } DestroySampleThreadsafe(sample); InitOPL(); ModSample &mptSmp = Samples[sample]; mptSmp.Initialize(MOD_TYPE_S3M); file.ReadString(m_szNames[sample], 32); OPLPatch patch; file.ReadArray(patch); mptSmp.SetAdlib(true, patch); mptSmp.Convert(MOD_TYPE_S3M, GetType()); return true; } ///////////////////////////////////////////////////////////// // XI Instruments bool CSoundFile::ReadXIInstrument(INSTRUMENTINDEX nInstr, FileReader &file) { file.Rewind(); XIInstrumentHeader fileHeader; if(!file.ReadStruct(fileHeader) || memcmp(fileHeader.signature, "Extended Instrument: ", 21) || fileHeader.version != XIInstrumentHeader::fileVersion || fileHeader.eof != 0x1A) { return false; } ModInstrument *pIns = new (std::nothrow) ModInstrument(); if(pIns == nullptr) { return false; } DestroyInstrument(nInstr, deleteAssociatedSamples); if(nInstr > m_nInstruments) { m_nInstruments = nInstr; } Instruments[nInstr] = pIns; fileHeader.ConvertToMPT(*pIns); // Translate sample map and find available sample slots std::vector sampleMap(fileHeader.numSamples); SAMPLEINDEX maxSmp = 0; for(size_t i = 0 + 12; i < 96 + 12; i++) { if(pIns->Keyboard[i] >= fileHeader.numSamples) { continue; } if(sampleMap[pIns->Keyboard[i]] == 0) { // Find slot for this sample maxSmp = GetNextFreeSample(nInstr, maxSmp + 1); if(maxSmp != SAMPLEINDEX_INVALID) { sampleMap[pIns->Keyboard[i]] = maxSmp; } } pIns->Keyboard[i] = sampleMap[pIns->Keyboard[i]]; } if(m_nSamples < maxSmp) { m_nSamples = maxSmp; } std::vector sampleFlags(fileHeader.numSamples); // Read sample headers for(SAMPLEINDEX i = 0; i < fileHeader.numSamples; i++) { XMSample sampleHeader; if(!file.ReadStruct(sampleHeader) || !sampleMap[i]) { continue; } ModSample &mptSample = Samples[sampleMap[i]]; sampleHeader.ConvertToMPT(mptSample); fileHeader.instrument.ApplyAutoVibratoToMPT(mptSample); mptSample.Convert(MOD_TYPE_XM, GetType()); if(GetType() != MOD_TYPE_XM && fileHeader.numSamples == 1) { // No need to pan that single sample, thank you... mptSample.uFlags &= ~CHN_PANNING; } mpt::String::Read(mptSample.filename, sampleHeader.name); mpt::String::Read(m_szNames[sampleMap[i]], sampleHeader.name); sampleFlags[i] = sampleHeader.GetSampleFormat(); } // Read sample data for(SAMPLEINDEX i = 0; i < fileHeader.numSamples; i++) { if(sampleMap[i]) { sampleFlags[i].ReadSample(Samples[sampleMap[i]], file); Samples[sampleMap[i]].PrecomputeLoops(*this, false); } } // Read MPT crap ReadExtendedInstrumentProperties(pIns, file); pIns->Convert(MOD_TYPE_XM, GetType()); pIns->Sanitize(GetType()); return true; } #ifndef MODPLUG_NO_FILESAVE bool CSoundFile::SaveXIInstrument(INSTRUMENTINDEX nInstr, std::ostream &f) const { ModInstrument *pIns = Instruments[nInstr]; if(pIns == nullptr) { return false; } // Create file header XIInstrumentHeader header; header.ConvertToXM(*pIns, false); const std::vector samples = header.instrument.GetSampleList(*pIns, false); if(samples.size() > 0 && samples[0] <= GetNumSamples()) { // Copy over auto-vibrato settings of first sample header.instrument.ApplyAutoVibratoToXM(Samples[samples[0]], GetType()); } mpt::IO::Write(f, header); std::vector sampleFlags(samples.size()); // XI Sample Headers for(SAMPLEINDEX i = 0; i < samples.size(); i++) { XMSample xmSample; if(samples[i] <= GetNumSamples()) { xmSample.ConvertToXM(Samples[samples[i]], GetType(), false); } else { MemsetZero(xmSample); } sampleFlags[i] = xmSample.GetSampleFormat(); mpt::String::Write(xmSample.name, m_szNames[samples[i]]); mpt::IO::Write(f, xmSample); } // XI Sample Data for(SAMPLEINDEX i = 0; i < samples.size(); i++) { if(samples[i] <= GetNumSamples()) { sampleFlags[i].WriteSample(f, Samples[samples[i]]); } } // Write 'MPTX' extension tag mpt::IO::WriteText(f, "XTPM"); WriteInstrumentHeaderStructOrField(pIns, f); // Write full extended header. return true; } #endif // MODPLUG_NO_FILESAVE // Read first sample from XI file into a sample slot bool CSoundFile::ReadXISample(SAMPLEINDEX nSample, FileReader &file) { file.Rewind(); XIInstrumentHeader fileHeader; if(!file.ReadStruct(fileHeader) || !file.CanRead(sizeof(XMSample)) || memcmp(fileHeader.signature, "Extended Instrument: ", 21) || fileHeader.version != XIInstrumentHeader::fileVersion || fileHeader.eof != 0x1A || fileHeader.numSamples == 0) { return false; } if(m_nSamples < nSample) { m_nSamples = nSample; } uint16 numSamples = fileHeader.numSamples; FileReader::off_t samplePos = sizeof(XIInstrumentHeader) + numSamples * sizeof(XMSample); // Preferrably read the middle-C sample auto sample = fileHeader.instrument.sampleMap[48]; if(sample >= fileHeader.numSamples) sample = 0; XMSample sampleHeader; while(sample--) { file.ReadStruct(sampleHeader); samplePos += sampleHeader.length; } file.ReadStruct(sampleHeader); // Gotta skip 'em all! file.Seek(samplePos); DestroySampleThreadsafe(nSample); ModSample &mptSample = Samples[nSample]; sampleHeader.ConvertToMPT(mptSample); if(GetType() != MOD_TYPE_XM) { // No need to pan that single sample, thank you... mptSample.uFlags.reset(CHN_PANNING); } fileHeader.instrument.ApplyAutoVibratoToMPT(mptSample); mptSample.Convert(MOD_TYPE_XM, GetType()); mpt::String::Read(mptSample.filename, sampleHeader.name); mpt::String::Read(m_szNames[nSample], sampleHeader.name); // Read sample data sampleHeader.GetSampleFormat().ReadSample(Samples[nSample], file); Samples[nSample].PrecomputeLoops(*this, false); return true; } ///////////////////////////////////////////////////////////////////////////////////////// // SFZ Instrument #ifdef MPT_EXTERNAL_SAMPLES struct SFZControl { std::string defaultPath; int8 octaveOffset = 0, noteOffset = 0; void Parse(const std::string &key, const std::string &value) { if(key == "default_path") defaultPath = value; else if(key == "octave_offset") octaveOffset = ConvertStrTo(value); else if(key == "note_offset") noteOffset = ConvertStrTo(value); } }; struct SFZEnvelope { float startLevel = 0, delay = 0, attack = 0, hold = 0, decay = 0, sustainLevel = 100, release = 0, depth = 0; void Parse(std::string key, const std::string &value) { key.erase(0, key.find('_') + 1); float v = ConvertStrTo(value); if(key == "depth") Limit(v, -12000.0f, 12000.0f); else if(key == "start" || key == "sustain") Limit(v, -100.0f, 100.0f); else Limit(v, 0.0f, 100.0f); if(key == "start") startLevel = v; else if(key == "delay") delay = v; else if(key == "attack") attack = v; else if(key == "hold") hold = v; else if(key == "decay") decay = v; else if(key == "sustain") sustainLevel = v; else if(key == "release") release = v; else if(key == "depth") depth = v; } static EnvelopeNode::tick_t ToTicks(float duration, float tickDuration) { return std::max(EnvelopeNode::tick_t(1), mpt::saturate_round(duration / tickDuration)); } EnvelopeNode::value_t ToValue(float value, EnvelopeType envType) const { value *= (ENVELOPE_MAX / 100.0f); if(envType == ENV_PITCH) { value *= depth / 3200.0f; value += ENVELOPE_MID; } Limit(value, ENVELOPE_MIN, ENVELOPE_MAX); return mpt::saturate_round(value); } void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile, EnvelopeType envType) const { auto &env = ins->GetEnvelope(envType); float tickDuration = sndFile.m_PlayState.m_nSamplesPerTick / static_cast(sndFile.GetSampleRate()); if(tickDuration <= 0) return; env.clear(); if(envType != ENV_VOLUME && attack == 0 && delay == 0 && hold == 0 && decay == 0 && sustainLevel == 100 && release == 0 && depth == 0) { env.dwFlags.reset(ENV_SUSTAIN | ENV_ENABLED); return; } if(attack > 0 || delay > 0) { env.push_back(0, ToValue(startLevel, envType)); if(delay > 0) env.push_back(ToTicks(delay, tickDuration), env.back().value); env.push_back(env.back().tick + ToTicks(attack, tickDuration), ToValue(100, envType)); } if(hold > 0) { if(env.empty()) env.push_back(0, ToValue(100, envType)); env.push_back(env.back().tick + ToTicks(hold, tickDuration), env.back().value); } if(env.empty()) env.push_back(0, ToValue(100, envType)); auto sustain = ToValue(sustainLevel, envType); if(env.back().value != sustain) env.push_back(env.back().tick + ToTicks(decay, tickDuration), sustain); env.nSustainStart = env.nSustainEnd = static_cast(env.size() - 1); if(sustainLevel != 0) { if(envType == ENV_VOLUME && env.nSustainEnd > 0) env.nReleaseNode = env.nSustainEnd; env.push_back(env.back().tick + ToTicks(release, tickDuration), ToValue(0, envType)); env.dwFlags.set(ENV_SUSTAIN); } env.dwFlags.set(ENV_ENABLED); } }; struct SFZRegion { enum class LoopMode { kUnspecified, kContinuous, kOneShot, kSustain, kNoLoop }; enum class LoopType { kUnspecified, kForward, kBackward, kAlternate, }; std::string filename, name; SFZEnvelope ampEnv, pitchEnv, filterEnv; SmpLength loopStart = 0, loopEnd = 0; SmpLength end = MAX_SAMPLE_LENGTH, offset = 0; double loopCrossfade = 0.0; LoopMode loopMode = LoopMode::kUnspecified; LoopType loopType = LoopType::kUnspecified; int32 cutoff = 0; // in Hz int32 filterRandom = 0; // 0...9600 cents int16 volume = 0; // -144dB...+6dB int16 pitchBend = 200; // -9600...9600 cents float pitchLfoFade = 0; // 0...100 seconds int16 pitchLfoDepth = 0; // -1200...12000 uint8 pitchLfoFreq = 0; // 0...20 Hz int8 panning = -128; // -100...+100 int8 transpose = 0; int8 finetune = 0; uint8 keyLo = 0, keyHi = 127, keyRoot = 60; uint8 resonance = 0; // 0...40dB InstrFilterMode filterType = FLTMODE_UNCHANGED; uint8 polyphony = 255; bool useSampleKeyRoot = false; bool invertPhase = false; template static void Read(const std::string &valueStr, T &value, Tc valueMin = std::numeric_limits::min(), Tc valueMax = std::numeric_limits::max()) { double valueF = ConvertStrTo(valueStr); MPT_CONSTANT_IF(std::numeric_limits::is_integer) { valueF = mpt::round(valueF); } Limit(valueF, static_cast(valueMin), static_cast(valueMax)); value = static_cast(valueF); } static uint8 ReadKey(const std::string &value, const SFZControl &control) { if(value.empty()) return 0; int key = 0; if(value[0] >= '0' && value[0] <= '9') { // MIDI key key = ConvertStrTo(value); } else if(value.length() < 2) { return 0; } else { // Scientific pitch static const int8 keys[] = { 9, 11, 0, 2, 4, 5, 7 }; STATIC_ASSERT(CountOf(keys) == 'g' - 'a' + 1); auto keyC = value[0]; if(keyC >= 'A' && keyC <= 'G') key = keys[keyC - 'A']; if(keyC >= 'a' && keyC <= 'g') key = keys[keyC - 'a']; else return 0; uint8 octaveOffset = 1; if(value[1] == '#') { key++; octaveOffset = 2; } else if(value[1] == 'b' || value[1] == 'B') { key--; octaveOffset = 2; } if(octaveOffset >= value.length()) return 0; int8 octave = ConvertStrTo(value.c_str() + octaveOffset); key += (octave + 1) * 12; } key += control.octaveOffset * 12 + control.noteOffset; return static_cast(Clamp(key, 0, 127)); } void Parse(const std::string &key, const std::string &value, const SFZControl &control) { if(key == "sample") filename = control.defaultPath + value; else if(key == "region_label") name = value; else if(key == "lokey") keyLo = ReadKey(value, control); else if(key == "hikey") keyHi = ReadKey(value, control); else if(key == "pitch_keycenter") { keyRoot = ReadKey(value, control); useSampleKeyRoot = (value == "sample"); } else if(key == "key") { keyLo = keyHi = keyRoot = ReadKey(value, control); useSampleKeyRoot = false; } else if(key == "bend_up" || key == "bendup") Read(value, pitchBend, -9600, 9600); else if(key == "pitchlfo_fade") Read(value, pitchLfoFade, 0.0f, 100.0f); else if(key == "pitchlfo_depth") Read(value, pitchLfoDepth, -12000, 12000); else if(key == "pitchlfo_freq") Read(value, pitchLfoFreq, 0, 20); else if(key == "volume") Read(value, volume, -144, 6); else if(key == "pan") Read(value, panning, -100, 100); else if(key == "transpose") Read(value, transpose, -127, 127); else if(key == "tune") Read(value, finetune, -100, 100); else if(key == "end") Read(value, end, SmpLength(0), MAX_SAMPLE_LENGTH); else if(key == "offset") Read(value, offset, SmpLength(0), MAX_SAMPLE_LENGTH); else if(key == "loop_start" || key == "loopstart") Read(value, loopStart, SmpLength(0), MAX_SAMPLE_LENGTH); else if(key == "loop_end" || key == "loopend") Read(value, loopEnd, SmpLength(0), MAX_SAMPLE_LENGTH); else if(key == "loop_crossfade") Read(value, loopCrossfade, 0.0, DBL_MAX); else if(key == "loop_mode" || key == "loopmode") { if(value == "loop_continuous") loopMode = LoopMode::kContinuous; else if(value == "one_shot") loopMode = LoopMode::kOneShot; else if(value == "loop_sustain") loopMode = LoopMode::kSustain; else if(value == "no_loop") loopMode = LoopMode::kNoLoop; } else if(key == "loop_type" || key == "looptype") { if(value == "forward") loopType = LoopType::kForward; else if(value == "backward") loopType = LoopType::kBackward; else if(value == "alternate") loopType = LoopType::kAlternate; } else if(key == "cutoff") Read(value, cutoff, 0, 96000); else if(key == "fil_random") Read(value, filterRandom, 0, 9600); else if(key == "resonance") Read(value, resonance, 0u, 40u); else if(key == "polyphony") Read(value, polyphony, 0u, 255u); else if(key == "phase") invertPhase = (value == "invert"); else if(key == "fil_type" || key == "filtype") { if(value == "lpf_1p" || value == "lpf_2p" || value == "lpf_4p" || value == "lpf_6p") filterType = FLTMODE_LOWPASS; else if(value == "hpf_1p" || value == "hpf_2p" || value == "hpf_4p" || value == "hpf_6p") filterType = FLTMODE_HIGHPASS; // Alternatives: bpf_2p, brf_2p } else if(key.substr(0, 6) == "ampeg_") ampEnv.Parse(key, value); else if(key.substr(0, 6) == "fileg_") filterEnv.Parse(key, value); else if(key.substr(0, 8) == "pitcheg_") pitchEnv.Parse(key, value); } }; bool CSoundFile::ReadSFZInstrument(INSTRUMENTINDEX nInstr, FileReader &file) { file.Rewind(); enum { kNone, kGlobal, kMaster, kGroup, kRegion, kControl, kCurve, kUnknown } section = kNone; SFZControl control; SFZRegion group, master, globals; std::vector regions; std::map macros; std::string s; while(file.ReadLine(s, 1024)) { // First, terminate line at the start of a comment block auto commentPos = s.find("//"); if(commentPos != std::string::npos) { s.resize(commentPos); } // Now, read the tokens. // This format is so funky that no general tokenizer approach seems to work here... // Consider this jolly good example found at https://stackoverflow.com/questions/5923895/tokenizing-a-custom-text-file-format-file-using-c-sharp // sample=piano C3.wav key=48 ampeg_release=0.7 // a comment here // key = 49 sample = piano Db3.wav // // group=1 // key = 48 // sample = piano D3.ogg // The original sfz specification claims that spaces around = are not allowed, but a quick look into the real world tells us otherwise. while(!s.empty()) { s.erase(0, s.find_first_not_of(" \t")); // Replace macros for(const auto &m : macros) { auto &oldStr = m.first; auto &newStr = m.second; std::string::size_type pos = 0; while((pos = s.find(oldStr, pos)) != std::string::npos) { s.replace(pos, oldStr.length(), newStr); pos += newStr.length(); } } if(s.empty()) { break; } std::string::size_type charsRead = 0; if(s[0] == '<' && (charsRead = s.find('>')) != std::string::npos) { // Section header std::string sec = s.substr(1, charsRead - 1); section = kUnknown; if(sec == "global") { section = kGlobal; // Reset global parameters globals = SFZRegion(); } else if(sec == "master") { section = kMaster; // Reset master parameters master = globals; } else if(sec == "group") { section = kGroup; // Reset group parameters group = master; } else if(sec == "region") { section = kRegion; regions.push_back(group); } else if(sec == "control") { section = kControl; } else if(sec == "curve") { section = kCurve; } charsRead++; } else if(s.substr(0, 8) == "#define " || s.substr(0, 8) == "#define\t") { // Macro definition auto keyStart = s.find_first_not_of(" \t", 8); auto keyEnd = s.find_first_of(" \t", keyStart); auto valueStart = s.find_first_not_of(" \t", keyEnd); std::string key = s.substr(keyStart, keyEnd - keyStart); if(valueStart != std::string::npos && key.length() > 1 && key[0] == '$') { charsRead = s.find_first_of(" \t", valueStart); macros[key] = s.substr(valueStart, charsRead - valueStart); } } else if(s.substr(0, 9) == "#include " || s.substr(0, 9) == "#include\t") { AddToLog(LogWarning, U_("#include directive is not supported.")); auto fileStart = s.find("\"", 9); // Yes, there can be arbitrary characters before the opening quote, at least that's how sforzando does it. auto fileEnd = s.find("\"", fileStart + 1); if(fileStart != std::string::npos && fileEnd != std::string::npos) { charsRead = fileEnd + 1; } else { return false; } } else if(section == kNone) { // Garbage before any section, probably not an sfz file return false; } else if(s.find('=') != std::string::npos) { // Read key=value pair auto keyEnd = s.find_first_of(" \t="); auto valueStart = s.find_first_not_of(" \t=", keyEnd); std::string key = mpt::ToLowerCaseAscii(s.substr(0, keyEnd)); if(key == "sample" || key == "default_path" || key.substr(0, 8) == "label_cc" || key.substr(0, 12) == "region_label") { // Sample / CC name may contain spaces... charsRead = s.find_first_of("=\t<", valueStart); if(charsRead != std::string::npos && s[charsRead] == '=') { // Backtrack to end of key while(charsRead > valueStart && s[charsRead] == ' ') charsRead--; // Backtrack to start of key while(charsRead > valueStart && s[charsRead] != ' ') charsRead--; } } else { charsRead = s.find_first_of(" \t<", valueStart); } std::string value = s.substr(valueStart, charsRead - valueStart); switch(section) { case kGlobal: globals.Parse(key, value, control); MPT_FALLTHROUGH; case kMaster: master.Parse(key, value, control); MPT_FALLTHROUGH; case kGroup: group.Parse(key, value, control); break; case kRegion: regions.back().Parse(key, value, control); break; case kControl: control.Parse(key, value); break; } } else { // Garbage, probably not an sfz file MPT_ASSERT(false); return false; } // Remove the token(s) we just read s.erase(0, charsRead); } } if(regions.empty()) { return false; } ModInstrument *pIns = new (std::nothrow) ModInstrument(); if(pIns == nullptr) { return false; } RecalculateSamplesPerTick(); DestroyInstrument(nInstr, deleteAssociatedSamples); if(nInstr > m_nInstruments) m_nInstruments = nInstr; Instruments[nInstr] = pIns; SAMPLEINDEX prevSmp = 0; for(auto ®ion : regions) { uint8 keyLo = region.keyLo, keyHi = region.keyHi; if(keyLo > keyHi) continue; Clamp(keyLo, 0, NOTE_MAX - NOTE_MIN); Clamp(keyHi, 0, NOTE_MAX - NOTE_MIN); SAMPLEINDEX smp = GetNextFreeSample(nInstr, prevSmp + 1); if(smp == SAMPLEINDEX_INVALID) break; prevSmp = smp; ModSample &sample = Samples[smp]; mpt::PathString filename = mpt::PathString::FromUTF8(region.filename); if(!filename.empty()) { if(region.filename.find(':') == std::string::npos) { filename = file.GetFileName().GetPath() + filename; } SetSamplePath(smp, filename); InputFile f(filename); FileReader smpFile = GetFileReader(f); if(!ReadSampleFromFile(smp, smpFile, false)) { AddToLog(LogWarning, U_("Unable to load sample: ") + filename.ToUnicode()); prevSmp--; continue; } if(!region.name.empty()) { mpt::String::Copy(m_szNames[smp], mpt::ToCharset(GetCharsetInternal(), mpt::CharsetUTF8, region.name)); } if(!m_szNames[smp][0]) { mpt::String::Copy(m_szNames[smp], filename.GetFileName().ToLocale()); } } sample.uFlags.set(SMP_KEEPONDISK, sample.HasSampleData()); if(region.useSampleKeyRoot) { if(sample.rootNote != NOTE_NONE) region.keyRoot = sample.rootNote - NOTE_MIN; else region.keyRoot = 60; } const auto origSampleRate = sample.GetSampleRate(GetType()); int8 transp = region.transpose + (60 - region.keyRoot); for(uint8 i = keyLo; i <= keyHi; i++) { pIns->Keyboard[i] = smp; if(GetType() != MOD_TYPE_XM) pIns->NoteMap[i] = NOTE_MIN + i + transp; } if(GetType() == MOD_TYPE_XM) sample.Transpose(transp / 12.0); pIns->nFilterMode = region.filterType; if(region.cutoff != 0) pIns->SetCutoff(FrequencyToCutOff(region.cutoff), true); if(region.resonance != 0) pIns->SetResonance(mpt::saturate_cast(Util::muldivr(region.resonance, 128, 24)), true); pIns->nCutSwing = mpt::saturate_cast(Util::muldivr(region.filterRandom, m_SongFlags[SONG_EXFILTERRANGE] ? 20 : 24, 1200)); pIns->midiPWD = static_cast(region.pitchBend / 100); pIns->nNNA = NNA_NOTEOFF; if(region.polyphony == 1) { pIns->nDNA = DNA_NOTECUT; pIns->nDCT = DCT_SAMPLE; } region.ampEnv.ConvertToMPT(pIns, *this, ENV_VOLUME); region.pitchEnv.ConvertToMPT(pIns, *this, ENV_PITCH); //region.filterEnv.ConvertToMPT(pIns, *this, ENV_PITCH); sample.rootNote = region.keyRoot + NOTE_MIN; sample.nGlobalVol = mpt::saturate_round(64 * std::pow(10.0, region.volume / 20.0)); if(region.panning != -128) { sample.nPan = static_cast(Util::muldivr_unsigned(region.panning + 100, 256, 200)); sample.uFlags.set(CHN_PANNING); } sample.Transpose(region.finetune / 1200.0); if(region.pitchLfoDepth && region.pitchLfoFreq) { sample.nVibSweep = 255; if(region.pitchLfoFade > 0) sample.nVibSweep = mpt::saturate_round(255 / region.pitchLfoFade); sample.nVibDepth = static_cast(Util::muldivr(region.pitchLfoDepth, 32, 100)); sample.nVibRate = region.pitchLfoFreq * 4; } if(region.loopMode != SFZRegion::LoopMode::kUnspecified) { switch(region.loopMode) { case SFZRegion::LoopMode::kContinuous: case SFZRegion::LoopMode::kOneShot: sample.uFlags.set(CHN_LOOP); break; case SFZRegion::LoopMode::kSustain: sample.uFlags.set(CHN_SUSTAINLOOP); break; case SFZRegion::LoopMode::kNoLoop: sample.uFlags.reset(CHN_LOOP | CHN_SUSTAINLOOP); } } if(region.loopEnd > region.loopStart) { // Loop may also be defined in file, in which case loopStart and loopEnd are unset. if(region.loopMode == SFZRegion::LoopMode::kSustain) { sample.nSustainStart = region.loopStart; sample.nSustainEnd = region.loopEnd + 1; } else if(region.loopMode == SFZRegion::LoopMode::kContinuous || region.loopMode == SFZRegion::LoopMode::kOneShot) { sample.nLoopStart = region.loopStart; sample.nLoopEnd = region.loopEnd + 1; } } else if(sample.nLoopEnd <= sample.nLoopStart && region.loopMode != SFZRegion::LoopMode::kUnspecified && region.loopMode != SFZRegion::LoopMode::kNoLoop) { sample.nLoopEnd = sample.nLength; } switch(region.loopType) { case SFZRegion::LoopType::kUnspecified: break; case SFZRegion::LoopType::kForward: sample.uFlags.reset(CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN | CHN_REVERSE); break; case SFZRegion::LoopType::kBackward: sample.uFlags.set(CHN_REVERSE); break; case SFZRegion::LoopType::kAlternate: sample.uFlags.set(CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN); break; default: break; } if(sample.nSustainEnd <= sample.nSustainStart && sample.nLoopEnd > sample.nLoopStart && region.loopMode == SFZRegion::LoopMode::kSustain) { // Turn normal loop (imported from sample) into sustain loop std::swap(sample.nSustainStart, sample.nLoopStart); std::swap(sample.nSustainEnd, sample.nLoopEnd); sample.uFlags.set(CHN_SUSTAINLOOP); sample.uFlags.set(CHN_PINGPONGSUSTAIN, sample.uFlags[CHN_PINGPONGLOOP]); sample.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP); } // Loop cross-fade SmpLength fadeSamples = mpt::saturate_round(region.loopCrossfade * origSampleRate); LimitMax(fadeSamples, sample.uFlags[CHN_SUSTAINLOOP] ? sample.nSustainStart : sample.nLoopStart); if(fadeSamples > 0) { ctrlSmp::XFadeSample(sample, fadeSamples, 50000, true, sample.uFlags[CHN_SUSTAINLOOP], *this); sample.uFlags.set(SMP_MODIFIED); } // Sample offset if(region.offset && region.offset < sample.nLength) { auto offset = region.offset * sample.GetBytesPerSample(); memmove(sample.sampleb(), sample.sampleb() + offset, sample.nLength * sample.GetBytesPerSample() - offset); if(region.end > region.offset) region.end -= region.offset; sample.nLength -= region.offset; sample.nLoopStart -= region.offset; sample.nLoopEnd -= region.offset; sample.uFlags.set(SMP_MODIFIED); } LimitMax(sample.nLength, region.end); if(region.invertPhase) { ctrlSmp::InvertSample(sample, 0, sample.nLength, *this); sample.uFlags.set(SMP_MODIFIED); } sample.PrecomputeLoops(*this, false); sample.Convert(MOD_TYPE_MPT, GetType()); } pIns->Sanitize(MOD_TYPE_MPT); pIns->Convert(MOD_TYPE_MPT, GetType()); return true; } #ifndef MODPLUG_NO_FILESAVE static double SFZLinear2dB(double volume) { return (volume > 0.0 ? 20.0 * std::log10(volume) : -144.0); } bool CSoundFile::SaveSFZInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool useFLACsamples) const { #ifdef MODPLUG_TRACKER const mpt::FlushMode flushMode = mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave); #else const mpt::FlushMode flushMode = mpt::FlushMode::Full; #endif const ModInstrument *ins = Instruments[nInstr]; if(ins == nullptr) { return false; } const mpt::PathString sampleBaseName = filename.GetFileName(); const mpt::PathString sampleDirName = sampleBaseName + P_("/"); const mpt::PathString sampleBasePath = filename.GetPath() + sampleDirName; if(!sampleBasePath.IsDirectory() && !::CreateDirectory(sampleBasePath.AsNative().c_str(), nullptr)) { return false; } if(strcmp(ins->name, "")) { f << "// Name: " << mpt::ToCharset(mpt::CharsetUTF8, GetCharsetInternal(), ins->name) << "\n"; } f << "// Created with " << mpt::ToCharset(mpt::CharsetUTF8, Version::Current().GetOpenMPTVersionString()) << "\n\n"; f << "\ndefault_path=" << sampleDirName.ToUTF8() << "\n\n"; f << ""; f << "\nbend_up=" << ins->midiPWD * 100; if(ins->IsCutoffEnabled()) { f << "\ncutoff=" << CSoundFile::CutOffToFrequency(ins->GetCutoff()); } if(ins->IsResonanceEnabled()) { f << "\nresonance=" << Util::muldivr_unsigned(ins->GetResonance(), 24, 128); } if(ins->IsCutoffEnabled() || ins->IsResonanceEnabled()) { f << "\nfil_type=" << (ins->nFilterMode == FLTMODE_HIGHPASS ? "hpf_2p" : "lpf_2p"); } if(ins->dwFlags[INS_SETPANNING]) { f << "\npan=" << (Util::muldivr_unsigned(ins->nPan, 200, 256) - 100); } if(ins->nGlobalVol != 64) { f << "\nvolume=" << SFZLinear2dB(ins->nGlobalVol / 64.0); } size_t numSamples = 0; for(size_t i = 0; i < mpt::size(ins->Keyboard); i++) { if(ins->Keyboard[i] < 1 || ins->Keyboard[i] > GetNumSamples()) continue; numSamples++; size_t endOfRegion = i + 1; while(endOfRegion < mpt::size(ins->Keyboard)) { if(ins->Keyboard[endOfRegion] != ins->Keyboard[i] || ins->NoteMap[endOfRegion] != (ins->NoteMap[i] + endOfRegion - i)) break; endOfRegion++; } endOfRegion--; mpt::PathString sampleName = sampleBasePath + sampleBaseName + P_(" ") + mpt::PathString::FromUnicode(mpt::ufmt::val(numSamples)); if(useFLACsamples) sampleName += P_(".flac"); else sampleName += P_(".wav"); try { mpt::SafeOutputFile fSmp(sampleName, std::ios::binary, flushMode); if(fSmp) { //fSmp.exceptions(fSmp.exceptions() | std::ios::badbit | std::ios::failbit); if(useFLACsamples) SaveFLACSample(ins->Keyboard[i], fSmp); else SaveWAVSample(ins->Keyboard[i], fSmp); } } catch(const std::exception &) { AddToLog(LogError, MPT_USTRING("Unable to save sample: ") + sampleName.ToUnicode()); } const ModSample &sample = Samples[ins->Keyboard[i]]; f << "\n\n"; if(strcmp(m_szNames[ins->Keyboard[i]], "")) { f << "\nregion_label=" << mpt::ToCharset(mpt::CharsetUTF8, GetCharsetInternal(), m_szNames[ins->Keyboard[i]]); } f << "\nsample=" << sampleName.GetFullFileName().ToUTF8(); f << "\nlokey=" << i; f << "\nhikey=" << endOfRegion; if(sample.rootNote != NOTE_NONE) { f << "\npitch_keycenter=" << sample.rootNote - NOTE_MIN; } else { f << "\npitch_keycenter=" << NOTE_MIDDLEC + i - ins->NoteMap[i]; } if(sample.uFlags[CHN_PANNING]) { f << "\npan=" << (Util::muldivr_unsigned(sample.nPan, 200, 256) - 100); } if(sample.nGlobalVol != 64) { f << "\nvolume=" << SFZLinear2dB((ins->nGlobalVol * sample.nGlobalVol) / 4096.0); } const char *loopMode; SmpLength loopStart = 0, loopEnd = 0; bool loopType = false; if(sample.uFlags[CHN_SUSTAINLOOP]) { loopMode = "loop_sustain"; loopStart = sample.nSustainStart; loopEnd = sample.nSustainEnd; loopType = sample.uFlags[CHN_PINGPONGSUSTAIN]; } else if(sample.uFlags[CHN_LOOP]) { loopMode = "loop_continuous"; loopStart = sample.nLoopStart; loopEnd = sample.nLoopEnd; loopType = sample.uFlags[CHN_SUSTAINLOOP]; // TODO backward loop (supported by engine but no UI) } else { loopMode = "no_loop"; } f << "\nloop_mode=" << loopMode; if(loopStart < loopEnd) { f << "\nloop_start=" << loopStart; f << "\nloop_end=" << loopEnd; f << "\nloop_type=" << (loopType ? "alternate" : "forward"); } if(sample.uFlags.test_all(CHN_SUSTAINLOOP | CHN_LOOP)) { f << "\n// Warning: Only sustain loop was exported!"; } i = endOfRegion; } return true; } #endif // MODPLUG_NO_FILESAVE #else bool CSoundFile::ReadSFZInstrument(INSTRUMENTINDEX, FileReader &) { return false; } #endif // MPT_EXTERNAL_SAMPLES /////////////// // Apple CAF // struct CAFFileHeader { uint32be mFileType; uint16be mFileVersion; uint16be mFileFlags; }; MPT_BINARY_STRUCT(CAFFileHeader, 8) struct CAFChunkHeader { uint32be mChunkType; int64be mChunkSize; }; MPT_BINARY_STRUCT(CAFChunkHeader, 12) struct CAFChunk { enum ChunkIdentifiers { iddesc = MagicBE("desc"), iddata = MagicBE("data"), idstrg = MagicBE("strg"), idinfo = MagicBE("info") }; CAFChunkHeader header; FileReader::off_t GetLength() const { int64 length = header.mChunkSize; if(length == -1) { length = std::numeric_limits::max(); // spec } if(length < 0) { length = std::numeric_limits::max(); // heuristic } return mpt::saturate_cast(length); } ChunkIdentifiers GetID() const { return static_cast(header.mChunkType.get()); } }; MPT_BINARY_STRUCT(CAFChunk, 12) enum { CAFkAudioFormatLinearPCM = MagicBE("lpcm"), CAFkAudioFormatAppleIMA4 = MagicBE("ima4"), CAFkAudioFormatMPEG4AAC = MagicBE("aac "), CAFkAudioFormatMACE3 = MagicBE("MAC3"), CAFkAudioFormatMACE6 = MagicBE("MAC6"), CAFkAudioFormatULaw = MagicBE("ulaw"), CAFkAudioFormatALaw = MagicBE("alaw"), CAFkAudioFormatMPEGLayer1 = MagicBE(".mp1"), CAFkAudioFormatMPEGLayer2 = MagicBE(".mp2"), CAFkAudioFormatMPEGLayer3 = MagicBE(".mp3"), CAFkAudioFormatAppleLossless = MagicBE("alac") }; enum { CAFkCAFLinearPCMFormatFlagIsFloat = (1L << 0), CAFkCAFLinearPCMFormatFlagIsLittleEndian = (1L << 1) }; struct CAFAudioFormat { float64be mSampleRate; uint32be mFormatID; uint32be mFormatFlags; uint32be mBytesPerPacket; uint32be mFramesPerPacket; uint32be mChannelsPerFrame; uint32be mBitsPerChannel; }; MPT_BINARY_STRUCT(CAFAudioFormat, 32) static void CAFSetTagFromInfoKey(mpt::ustring & dst, const std::map & infoMap, const std::string & key) { auto item = infoMap.find(key); if(item == infoMap.end()) { return; } if(item->second.empty()) { return; } dst = mpt::ToUnicode(mpt::CharsetUTF8, item->second); } bool CSoundFile::ReadCAFSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) { file.Rewind(); CAFFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(fileHeader.mFileType != MagicBE("caff")) { return false; } if(fileHeader.mFileVersion != 1) { return false; } ChunkReader chunkFile = file; auto chunkList = chunkFile.ReadChunks(0); CAFAudioFormat audioFormat; if(!chunkList.GetChunk(CAFChunk::iddesc).ReadStruct(audioFormat)) { return false; } if(audioFormat.mSampleRate <= 0.0) { return false; } if(audioFormat.mChannelsPerFrame == 0) { return false; } if(audioFormat.mChannelsPerFrame > 2) { return false; } if(!Util::TypeCanHoldValue(mpt::saturate_round(audioFormat.mSampleRate))) { return false; } uint32 sampleRate = static_cast(mpt::saturate_round(audioFormat.mSampleRate)); if(sampleRate <= 0) { return false; } SampleIO sampleIO; if(audioFormat.mFormatID == CAFkAudioFormatLinearPCM) { if(audioFormat.mFramesPerPacket != 1) { return false; } if(audioFormat.mBytesPerPacket == 0) { return false; } if(audioFormat.mBitsPerChannel == 0) { return false; } if(audioFormat.mFormatFlags & CAFkCAFLinearPCMFormatFlagIsFloat) { if(audioFormat.mBitsPerChannel != 32 && audioFormat.mBitsPerChannel != 64) { return false; } if(audioFormat.mBytesPerPacket != audioFormat.mChannelsPerFrame * audioFormat.mBitsPerChannel/8) { return false; } } if(audioFormat.mBytesPerPacket % audioFormat.mChannelsPerFrame != 0) { return false; } if(audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 1 && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 2 && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 3 && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 4 && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 8 ) { return false; } SampleIO::Channels channels = (audioFormat.mChannelsPerFrame == 2) ? SampleIO::stereoInterleaved : SampleIO::mono; SampleIO::Endianness endianness = (audioFormat.mFormatFlags & CAFkCAFLinearPCMFormatFlagIsLittleEndian) ? SampleIO::littleEndian : SampleIO::bigEndian; SampleIO::Encoding encoding = (audioFormat.mFormatFlags & CAFkCAFLinearPCMFormatFlagIsFloat) ? SampleIO::floatPCM : SampleIO::signedPCM; SampleIO::Bitdepth bitdepth = static_cast((audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame) * 8); sampleIO = SampleIO(bitdepth, channels, endianness, encoding); } else { return false; } if(mayNormalize) { sampleIO.MayNormalize(); } /* std::map stringMap; // UTF-8 if(chunkList.ChunkExists(CAFChunk::idstrg)) { FileReader stringsChunk = chunkList.GetChunk(CAFChunk::idstrg); uint32 numEntries = stringsChunk.ReadUint32BE(); if(stringsChunk.Skip(12 * numEntries)) { FileReader stringData = stringsChunk.ReadChunk(stringsChunk.BytesLeft()); stringsChunk.Seek(4); for(uint32 entry = 0; entry < numEntries && stringsChunk.CanRead(12); entry++) { uint32 stringID = stringsChunk.ReadUint32BE(); int64 offset = stringsChunk.ReadIntBE(); if(offset >= 0 && Util::TypeCanHoldValue(offset)) { stringData.Seek(mpt::saturate_cast(offset)); std::string str; if(stringData.ReadNullString(str)) { stringMap[stringID] = str; } } } } } */ std::map infoMap; // UTF-8 if(chunkList.ChunkExists(CAFChunk::idinfo)) { FileReader informationChunk = chunkList.GetChunk(CAFChunk::idinfo); uint32 numEntries = informationChunk.ReadUint32BE(); for(uint32 entry = 0; entry < numEntries && informationChunk.CanRead(2); entry++) { std::string key; std::string value; if(!informationChunk.ReadNullString(key)) { break; } if(!informationChunk.ReadNullString(value)) { break; } if(!key.empty() && !value.empty()) { infoMap[key] = value; } } } FileTags tags; CAFSetTagFromInfoKey(tags.bpm, infoMap, "tempo"); //CAFSetTagFromInfoKey(void, infoMap, "key signature"); //CAFSetTagFromInfoKey(void, infoMap, "time signature"); CAFSetTagFromInfoKey(tags.artist, infoMap, "artist"); CAFSetTagFromInfoKey(tags.album, infoMap, "album"); CAFSetTagFromInfoKey(tags.trackno, infoMap, "track number"); CAFSetTagFromInfoKey(tags.year, infoMap, "year"); //CAFSetTagFromInfoKey(void, infoMap, "composer"); //CAFSetTagFromInfoKey(void, infoMap, "lyricist"); CAFSetTagFromInfoKey(tags.genre, infoMap, "genre"); CAFSetTagFromInfoKey(tags.title, infoMap, "title"); //CAFSetTagFromInfoKey(void, infoMap, "recorded date"); CAFSetTagFromInfoKey(tags.comments, infoMap, "comments"); //CAFSetTagFromInfoKey(void, infoMap, "copyright"); //CAFSetTagFromInfoKey(void, infoMap, "source encoder"); CAFSetTagFromInfoKey(tags.encoder, infoMap, "encoding application"); //CAFSetTagFromInfoKey(void, infoMap, "nominal bit rate"); //CAFSetTagFromInfoKey(void, infoMap, "channel layout"); //CAFSetTagFromInfoKey(tags.url, infoMap, void); if(!chunkList.ChunkExists(CAFChunk::iddata)) { return false; } FileReader dataChunk = chunkList.GetChunk(CAFChunk::iddata); dataChunk.Skip(4); // edit count FileReader audioData = dataChunk.ReadChunk(dataChunk.BytesLeft()); SmpLength length = mpt::saturate_cast((audioData.GetLength() / audioFormat.mBytesPerPacket) * audioFormat.mFramesPerPacket); ModSample &mptSample = Samples[nSample]; DestroySampleThreadsafe(nSample); mptSample.Initialize(); mptSample.nLength = length; mptSample.nC5Speed = sampleRate; sampleIO.ReadSample(mptSample, audioData); mpt::String::Copy(m_szNames[nSample], mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags))); mptSample.Convert(MOD_TYPE_IT, GetType()); mptSample.PrecomputeLoops(*this, false); return true; } ///////////////////////////////////////////////////////////////////////////////////////// // AIFF File I/O // AIFF header struct AIFFHeader { char magic[4]; // FORM uint32be length; // Size of the file, not including magic and length char type[4]; // AIFF or AIFC }; MPT_BINARY_STRUCT(AIFFHeader, 12) // General IFF Chunk header struct AIFFChunk { // 32-Bit chunk identifiers enum ChunkIdentifiers { idCOMM = MagicBE("COMM"), idSSND = MagicBE("SSND"), idINST = MagicBE("INST"), idMARK = MagicBE("MARK"), idNAME = MagicBE("NAME"), }; uint32be id; // See ChunkIdentifiers uint32be length; // Chunk size without header size_t GetLength() const { return length; } ChunkIdentifiers GetID() const { return static_cast(id.get()); } }; MPT_BINARY_STRUCT(AIFFChunk, 8) // "Common" chunk (in AIFC, a compression ID and compression name follows this header, but apart from that it's identical) struct AIFFCommonChunk { uint16be numChannels; uint32be numSampleFrames; uint16be sampleSize; uint8be sampleRate[10]; // Sample rate in 80-Bit floating point // Convert sample rate to integer uint32 GetSampleRate() const { uint32 mantissa = (sampleRate[2] << 24) | (sampleRate[3] << 16) | (sampleRate[4] << 8) | (sampleRate[5] << 0); uint32 last = 0; uint8 exp = 30 - sampleRate[1]; while(exp--) { last = mantissa; mantissa >>= 1; } if(last & 1) mantissa++; return mantissa; } }; MPT_BINARY_STRUCT(AIFFCommonChunk, 18) // Sound chunk struct AIFFSoundChunk { uint32be offset; uint32be blockSize; }; MPT_BINARY_STRUCT(AIFFSoundChunk, 8) // Marker struct AIFFMarker { uint16be id; uint32be position; // Position in sample uint8be nameLength; // Not counting eventually existing padding byte in name string }; MPT_BINARY_STRUCT(AIFFMarker, 7) // Instrument loop struct AIFFInstrumentLoop { enum PlayModes { noLoop = 0, loopNormal = 1, loopBidi = 2, }; uint16be playMode; uint16be beginLoop; // Marker index uint16be endLoop; // Marker index }; MPT_BINARY_STRUCT(AIFFInstrumentLoop, 6) struct AIFFInstrumentChunk { uint8be baseNote; uint8be detune; uint8be lowNote; uint8be highNote; uint8be lowVelocity; uint8be highVelocity; uint16be gain; AIFFInstrumentLoop sustainLoop; AIFFInstrumentLoop releaseLoop; }; MPT_BINARY_STRUCT(AIFFInstrumentChunk, 20) bool CSoundFile::ReadAIFFSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) { file.Rewind(); ChunkReader chunkFile(file); // Verify header AIFFHeader fileHeader; if(!chunkFile.ReadStruct(fileHeader) || memcmp(fileHeader.magic, "FORM", 4) || (memcmp(fileHeader.type, "AIFF", 4) && memcmp(fileHeader.type, "AIFC", 4))) { return false; } auto chunks = chunkFile.ReadChunks(2); // Read COMM chunk FileReader commChunk(chunks.GetChunk(AIFFChunk::idCOMM)); AIFFCommonChunk sampleInfo; if(!commChunk.ReadStruct(sampleInfo)) { return false; } // Is this a proper sample? if(sampleInfo.numSampleFrames == 0 || sampleInfo.numChannels < 1 || sampleInfo.numChannels > 2 || sampleInfo.sampleSize < 1 || sampleInfo.sampleSize > 64) { return false; } // Read compression type in AIFF-C files. uint8 compression[4] = { 'N', 'O', 'N', 'E' }; SampleIO::Endianness endian = SampleIO::bigEndian; if(!memcmp(fileHeader.type, "AIFC", 4)) { if(!commChunk.ReadArray(compression)) { return false; } if(!memcmp(compression, "twos", 4)) { endian = SampleIO::littleEndian; } } // Read SSND chunk FileReader soundChunk(chunks.GetChunk(AIFFChunk::idSSND)); AIFFSoundChunk sampleHeader; if(!soundChunk.ReadStruct(sampleHeader) || !soundChunk.CanRead(sampleHeader.offset)) { return false; } SampleIO::Bitdepth bitDepth; switch((sampleInfo.sampleSize - 1) / 8) { default: case 0: bitDepth = SampleIO::_8bit; break; case 1: bitDepth = SampleIO::_16bit; break; case 2: bitDepth = SampleIO::_24bit; break; case 3: bitDepth = SampleIO::_32bit; break; case 7: bitDepth = SampleIO::_64bit; break; } SampleIO sampleIO(bitDepth, (sampleInfo.numChannels == 2) ? SampleIO::stereoInterleaved : SampleIO::mono, endian, SampleIO::signedPCM); if(!memcmp(compression, "fl32", 4) || !memcmp(compression, "FL32", 4) || !memcmp(compression, "fl64", 4)) { sampleIO |= SampleIO::floatPCM; } else if(!memcmp(compression, "alaw", 4) || !memcmp(compression, "ALAW", 4)) { sampleIO |= SampleIO::aLaw; sampleIO |= SampleIO::_16bit; } else if(!memcmp(compression, "ulaw", 4) || !memcmp(compression, "ULAW", 4)) { sampleIO |= SampleIO::uLaw; sampleIO |= SampleIO::_16bit; } if(mayNormalize) { sampleIO.MayNormalize(); } soundChunk.Skip(sampleHeader.offset); ModSample &mptSample = Samples[nSample]; DestroySampleThreadsafe(nSample); mptSample.Initialize(); mptSample.nLength = sampleInfo.numSampleFrames; mptSample.nC5Speed = sampleInfo.GetSampleRate(); sampleIO.ReadSample(mptSample, soundChunk); // Read MARK and INST chunk to extract sample loops FileReader markerChunk(chunks.GetChunk(AIFFChunk::idMARK)); AIFFInstrumentChunk instrHeader; if(markerChunk.IsValid() && chunks.GetChunk(AIFFChunk::idINST).ReadStruct(instrHeader)) { uint16 numMarkers = markerChunk.ReadUint16BE(); std::vector markers; markers.reserve(numMarkers); for(size_t i = 0; i < numMarkers; i++) { AIFFMarker marker; if(!markerChunk.ReadStruct(marker)) { break; } markers.push_back(marker); markerChunk.Skip(marker.nameLength + ((marker.nameLength % 2u) == 0 ? 1 : 0)); } if(instrHeader.sustainLoop.playMode != AIFFInstrumentLoop::noLoop) { mptSample.uFlags.set(CHN_SUSTAINLOOP); mptSample.uFlags.set(CHN_PINGPONGSUSTAIN, instrHeader.sustainLoop.playMode == AIFFInstrumentLoop::loopBidi); } if(instrHeader.releaseLoop.playMode != AIFFInstrumentLoop::noLoop) { mptSample.uFlags.set(CHN_LOOP); mptSample.uFlags.set(CHN_PINGPONGLOOP, instrHeader.releaseLoop.playMode == AIFFInstrumentLoop::loopBidi); } // Read markers for(const auto &m : markers) { if(m.id == instrHeader.sustainLoop.beginLoop) mptSample.nSustainStart = m.position; if(m.id == instrHeader.sustainLoop.endLoop) mptSample.nSustainEnd = m.position; if(m.id == instrHeader.releaseLoop.beginLoop) mptSample.nLoopStart = m.position; if(m.id == instrHeader.releaseLoop.endLoop) mptSample.nLoopEnd = m.position; } mptSample.SanitizeLoops(); } // Extract sample name FileReader nameChunk(chunks.GetChunk(AIFFChunk::idNAME)); if(nameChunk.IsValid()) { nameChunk.ReadString(m_szNames[nSample], nameChunk.GetLength()); } else { strcpy(m_szNames[nSample], ""); } mptSample.Convert(MOD_TYPE_IT, GetType()); mptSample.PrecomputeLoops(*this, false); return true; } static bool AUIsAnnotationLineWithField(const std::string & line) { std::size_t pos = line.find('='); if(pos == std::string::npos) { return false; } if(pos == 0) { return false; } std::string field = line.substr(0, pos); bool invalidChars = false; for(auto c : field) { if(!IsInRange(c, 'a', 'z') && !IsInRange(c, 'A', 'Z') && !IsInRange(c, '0', '9') && c != '-' && c != '_') { invalidChars = true; } } if(invalidChars) { return false; } return true; } static std::string AUTrimFieldFromAnnotationLine(const std::string & line) { if(!AUIsAnnotationLineWithField(line)) { return line; } std::size_t pos = line.find('='); return line.substr(pos + 1); } static std::string AUGetAnnotationFieldFromLine(const std::string & line) { if(!AUIsAnnotationLineWithField(line)) { return std::string(); } std::size_t pos = line.find('='); return line.substr(0, pos); } bool CSoundFile::ReadAUSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) { file.Rewind(); // Verify header if(!file.ReadMagic(".snd")) return false; uint32 dataOffset = file.ReadUint32BE(); // must be divisible by 8 according to spec, however, there are files that ignore this requirement uint32 dataSize = file.ReadUint32BE(); uint32 encoding = file.ReadUint32BE(); uint32 sampleRate = file.ReadUint32BE(); uint32 channels = file.ReadUint32BE(); // According to spec, a minimum 8 byte annotation field after the header fields is required, // however, there are files in the wild that violate this requirement. // Thus, check for 24 instead of 32 here. if(dataOffset < 24) // data offset points inside header { return false; } if(channels < 1 || channels > 2) return false; SampleIO sampleIO(SampleIO::_8bit, channels == 1 ? SampleIO::mono : SampleIO::stereoInterleaved, SampleIO::bigEndian, SampleIO::signedPCM); switch(encoding) { case 1: sampleIO |= SampleIO::_16bit; // u-law sampleIO |= SampleIO::uLaw; break; case 2: break; // 8-bit linear PCM case 3: sampleIO |= SampleIO::_16bit; break; // 16-bit linear PCM case 4: sampleIO |= SampleIO::_24bit; break; // 24-bit linear PCM case 5: sampleIO |= SampleIO::_32bit; break; // 32-bit linear PCM case 6: sampleIO |= SampleIO::_32bit; // 32-bit IEEE floating point sampleIO |= SampleIO::floatPCM; break; case 7: sampleIO |= SampleIO::_64bit; // 64-bit IEEE floating point sampleIO |= SampleIO::floatPCM; break; case 27: sampleIO |= SampleIO::_16bit; // a-law sampleIO |= SampleIO::aLaw; break; default: return false; } if(!file.LengthIsAtLeast(dataOffset)) { return false; } FileTags tags; // This reads annotation metadata as written by OpenMPT, sox, ffmpeg. // Additionally, we fall back to just reading the whole field as a single comment. file.Seek(24); std::vector annotationData; file.ReadVector(annotationData, dataOffset - 24); std::string annotation(annotationData.begin(), annotationData.end()); annotation = mpt::String::RTrim(annotation, std::string(1, '\0')); std::size_t term = annotation.find(std::string(1, '\0')); if(term != std::string::npos) { // only up to first \0 byte annotation = annotation.substr(0, term); } annotation = mpt::String::Replace(annotation, "\r\n", "\n"); annotation = mpt::String::Replace(annotation, "\r", "\n"); mpt::Charset charset = mpt::IsUTF8(annotation) ? mpt::CharsetUTF8 : mpt::CharsetISO8859_1; std::vector lines = mpt::String::Split(annotation, "\n"); bool has_fields = false; for(const auto & line : lines) { if(AUIsAnnotationLineWithField(line)) { has_fields = true; } } if(has_fields) { std::map> lines_per_field; std::string last_field = "comment"; for(const auto & line : lines) { if(AUIsAnnotationLineWithField(line)) { last_field = mpt::ToLowerCaseAscii(mpt::String::Trim(AUGetAnnotationFieldFromLine(line))); } lines_per_field[last_field].push_back(AUTrimFieldFromAnnotationLine(line)); } tags.title = mpt::ToUnicode(charset, mpt::String::Combine(lines_per_field["title" ], std::string("\n"))); tags.artist = mpt::ToUnicode(charset, mpt::String::Combine(lines_per_field["artist" ], std::string("\n"))); tags.album = mpt::ToUnicode(charset, mpt::String::Combine(lines_per_field["album" ], std::string("\n"))); tags.trackno = mpt::ToUnicode(charset, mpt::String::Combine(lines_per_field["track" ], std::string("\n"))); tags.genre = mpt::ToUnicode(charset, mpt::String::Combine(lines_per_field["genre" ], std::string("\n"))); tags.comments = mpt::ToUnicode(charset, mpt::String::Combine(lines_per_field["comment"], std::string("\n"))); } else { // Most applications tend to write their own name here, // thus there is little use in interpreting the string as a title. annotation = mpt::String::RTrim(annotation, std::string("\r\n")); tags.comments = mpt::ToUnicode(charset, annotation); } file.Seek(dataOffset); ModSample &mptSample = Samples[nSample]; DestroySampleThreadsafe(nSample); mptSample.Initialize(); SmpLength length = mpt::saturate_cast(file.BytesLeft()); if(dataSize != 0xFFFFFFFF) LimitMax(length, dataSize); mptSample.nLength = (length * 8u) / (sampleIO.GetEncodedBitsPerSample() * channels); mptSample.nC5Speed = sampleRate; mpt::String::Copy(m_szNames[nSample], mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags))); if(mayNormalize) { sampleIO.MayNormalize(); } sampleIO.ReadSample(mptSample, file); mptSample.Convert(MOD_TYPE_IT, GetType()); mptSample.PrecomputeLoops(*this, false); return true; } ///////////////////////////////////////////////////////////////////////////////////////// // ITS Samples bool CSoundFile::ReadITSSample(SAMPLEINDEX nSample, FileReader &file, bool rewind) { if(rewind) { file.Rewind(); } ITSample sampleHeader; if(!file.ReadStruct(sampleHeader) || memcmp(sampleHeader.id, "IMPS", 4)) { return false; } DestroySampleThreadsafe(nSample); ModSample &sample = Samples[nSample]; file.Seek(sampleHeader.ConvertToMPT(sample)); mpt::String::Read(m_szNames[nSample], sampleHeader.name); if(sample.uFlags[CHN_ADLIB]) { OPLPatch patch; file.ReadArray(patch); sample.SetAdlib(true, patch); InitOPL(); if(!SupportsOPL()) { AddToLog("OPL instruments are not supported by this format."); } } else if(!sample.uFlags[SMP_KEEPONDISK]) { sampleHeader.GetSampleFormat().ReadSample(sample, file); } else { // External sample size_t strLen; file.ReadVarInt(strLen); #ifdef MPT_EXTERNAL_SAMPLES std::string filenameU8; file.ReadString(filenameU8, strLen); mpt::PathString filename = mpt::PathString::FromUTF8(filenameU8); if(!filename.empty()) { if(!file.GetFileName().empty()) { filename = filename.RelativePathToAbsolute(file.GetFileName().GetPath()); } if(!LoadExternalSample(nSample, filename)) { AddToLog(LogWarning, U_("Unable to load sample: ") + filename.ToUnicode()); } } else { sample.uFlags.reset(SMP_KEEPONDISK); } #else file.Skip(strLen); #endif // MPT_EXTERNAL_SAMPLES } sample.Convert(MOD_TYPE_IT, GetType()); sample.PrecomputeLoops(*this, false); return true; } bool CSoundFile::ReadITISample(SAMPLEINDEX nSample, FileReader &file) { ITInstrument instrumentHeader; file.Rewind(); if(!file.ReadStruct(instrumentHeader) || memcmp(instrumentHeader.id, "IMPI", 4)) { return false; } file.Rewind(); ModInstrument dummy; ITInstrToMPT(file, dummy, instrumentHeader.trkvers); // Old SchismTracker versions set nos=0 const SAMPLEINDEX nsamples = std::max(static_cast(instrumentHeader.nos), *std::max_element(std::begin(dummy.Keyboard), std::end(dummy.Keyboard))); if(!nsamples) return false; // Preferrably read the middle-C sample auto sample = dummy.Keyboard[NOTE_MIDDLEC - NOTE_MIN]; if(sample > 0) sample--; else sample = 0; file.Seek(file.GetPosition() + sample * sizeof(ITSample)); return ReadITSSample(nSample, file, false); } bool CSoundFile::ReadITIInstrument(INSTRUMENTINDEX nInstr, FileReader &file) { ITInstrument instrumentHeader; SAMPLEINDEX smp = 0; file.Rewind(); if(!file.ReadStruct(instrumentHeader) || memcmp(instrumentHeader.id, "IMPI", 4)) { return false; } if(nInstr > GetNumInstruments()) m_nInstruments = nInstr; ModInstrument *pIns = new (std::nothrow) ModInstrument(); if(pIns == nullptr) { return false; } DestroyInstrument(nInstr, deleteAssociatedSamples); Instruments[nInstr] = pIns; file.Rewind(); ITInstrToMPT(file, *pIns, instrumentHeader.trkvers); // Old SchismTracker versions set nos=0 const SAMPLEINDEX nsamples = std::max(static_cast(instrumentHeader.nos), *std::max_element(std::begin(pIns->Keyboard), std::end(pIns->Keyboard))); // In order to properly compute the position, in file, of eventual extended settings // such as "attack" we need to keep the "real" size of the last sample as those extra // setting will follow this sample in the file FileReader::off_t extraOffset = file.GetPosition(); // Reading Samples std::vector samplemap(nsamples, 0); for(SAMPLEINDEX i = 0; i < nsamples; i++) { smp = GetNextFreeSample(nInstr, smp + 1); if(smp == SAMPLEINDEX_INVALID) break; samplemap[i] = smp; const FileReader::off_t offset = file.GetPosition(); if(!ReadITSSample(smp, file, false)) smp--; extraOffset = std::max(extraOffset, file.GetPosition()); file.Seek(offset + sizeof(ITSample)); } if(GetNumSamples() < smp) m_nSamples = smp; // Adjust sample assignment for(auto &sample : pIns->Keyboard) { if(sample > 0 && sample <= nsamples) { sample = samplemap[sample - 1]; } } if(file.Seek(extraOffset)) { // Read MPT crap ReadExtendedInstrumentProperties(pIns, file); } pIns->Convert(MOD_TYPE_IT, GetType()); pIns->Sanitize(GetType()); return true; } #ifndef MODPLUG_NO_FILESAVE bool CSoundFile::SaveITIInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool compress, bool allowExternal) const { ITInstrument iti; ModInstrument *pIns = Instruments[nInstr]; if((!pIns) || (filename.empty() && allowExternal)) return false; auto instSize = iti.ConvertToIT(*pIns, false, *this); // Create sample assignment table std::vector smptable; std::vector smpmap(GetNumSamples(), 0); for(size_t i = 0; i < NOTE_MAX; i++) { const SAMPLEINDEX smp = pIns->Keyboard[i]; if(smp && smp <= GetNumSamples()) { if(!smpmap[smp - 1]) { // We haven't considered this sample yet. smptable.push_back(smp); smpmap[smp - 1] = static_cast(smptable.size()); } iti.keyboard[i * 2 + 1] = smpmap[smp - 1]; } else { iti.keyboard[i * 2 + 1] = 0; } } iti.nos = static_cast(smptable.size()); smpmap.clear(); uint32 filePos = instSize; mpt::IO::WritePartial(f, iti, instSize); filePos += mpt::saturate_cast(smptable.size() * sizeof(ITSample)); // Writing sample headers + data std::vector sampleFlags; for(auto smp : smptable) { ITSample itss; itss.ConvertToIT(Samples[smp], GetType(), compress, compress, allowExternal); const bool isExternal = itss.cvt == ITSample::cvtExternalSample; mpt::String::Write(itss.name, m_szNames[smp]); itss.samplepointer = filePos; mpt::IO::Write(f, itss); // Write sample auto curPos = mpt::IO::TellWrite(f); mpt::IO::SeekAbsolute(f, filePos); if(!isExternal) { filePos += mpt::saturate_cast(itss.GetSampleFormat(0x0214).WriteSample(f, Samples[smp])); } else { #ifdef MPT_EXTERNAL_SAMPLES const std::string filenameU8 = GetSamplePath(smp).AbsolutePathToRelative(filename.GetPath()).ToUTF8(); const size_t strSize = filenameU8.size(); size_t intBytes = 0; if(mpt::IO::WriteVarInt(f, strSize, &intBytes)) { filePos += mpt::saturate_cast(intBytes + strSize); mpt::IO::WriteRaw(f, filenameU8.data(), strSize); } #endif // MPT_EXTERNAL_SAMPLES } mpt::IO::SeekAbsolute(f, curPos); } mpt::IO::SeekEnd(f); // Write 'MPTX' extension tag mpt::IO::WriteRaw(f, "XTPM", 4); WriteInstrumentHeaderStructOrField(pIns, f); // Write full extended header. return true; } #endif // MODPLUG_NO_FILESAVE /////////////////////////////////////////////////////////////////////////////////////////////////// // 8SVX / 16SVX Samples // IFF File Header struct IFFHeader { char form[4]; // "FORM" uint32be size; char magic[4]; // "8SVX" or "16SV" }; MPT_BINARY_STRUCT(IFFHeader, 12) // General IFF Chunk header struct IFFChunk { // 32-Bit chunk identifiers enum ChunkIdentifiers { idVHDR = MagicBE("VHDR"), idBODY = MagicBE("BODY"), idNAME = MagicBE("NAME"), }; uint32be id; // See ChunkIdentifiers uint32be length; // Chunk size without header size_t GetLength() const { if(length == 0) // Broken files return std::numeric_limits::max(); return length; } ChunkIdentifiers GetID() const { return static_cast(id.get()); } }; MPT_BINARY_STRUCT(IFFChunk, 8) struct IFFSampleHeader { uint32be oneShotHiSamples; // Samples in the high octave 1-shot part uint32be repeatHiSamples; // Samples in the high octave repeat part uint32be samplesPerHiCycle; // Samples/cycle in high octave, else 0 uint16be samplesPerSec; // Data sampling rate uint8be octave; // Octaves of waveforms uint8be compression; // Data compression technique used uint32be volume; }; MPT_BINARY_STRUCT(IFFSampleHeader, 20) bool CSoundFile::ReadIFFSample(SAMPLEINDEX nSample, FileReader &file) { file.Rewind(); IFFHeader fileHeader; if(!file.ReadStruct(fileHeader) || memcmp(fileHeader.form, "FORM", 4 ) || (memcmp(fileHeader.magic, "8SVX", 4) && memcmp(fileHeader.magic, "16SV", 4))) { return false; } ChunkReader chunkFile(file); ChunkReader::ChunkList chunks = chunkFile.ReadChunks(2); FileReader vhdrChunk = chunks.GetChunk(IFFChunk::idVHDR); FileReader bodyChunk = chunks.GetChunk(IFFChunk::idBODY); IFFSampleHeader sampleHeader; if(!bodyChunk.IsValid() || !vhdrChunk.IsValid() || !vhdrChunk.ReadStruct(sampleHeader)) { return false; } DestroySampleThreadsafe(nSample); // Default values const uint8 bytesPerSample = memcmp(fileHeader.magic, "8SVX", 4) ? 2 : 1; ModSample &sample = Samples[nSample]; sample.Initialize(); sample.nLoopStart = sampleHeader.oneShotHiSamples / bytesPerSample; sample.nLoopEnd = sample.nLoopStart + sampleHeader.repeatHiSamples / bytesPerSample; sample.nC5Speed = sampleHeader.samplesPerSec; sample.nVolume = static_cast(sampleHeader.volume >> 8); if(!sample.nVolume || sample.nVolume > 256) sample.nVolume = 256; if(!sample.nC5Speed) sample.nC5Speed = 22050; sample.Convert(MOD_TYPE_IT, GetType()); FileReader nameChunk = chunks.GetChunk(IFFChunk::idNAME); if(nameChunk.IsValid()) { nameChunk.ReadString(m_szNames[nSample], nameChunk.GetLength()); } else { strcpy(m_szNames[nSample], ""); } sample.nLength = mpt::saturate_cast(bodyChunk.GetLength() / bytesPerSample); if((sample.nLoopStart + 4 < sample.nLoopEnd) && (sample.nLoopEnd <= sample.nLength)) sample.uFlags.set(CHN_LOOP); // While this is an Amiga format, the 16SV version appears to be only used on PC, and only with little-endian sample data. SampleIO( (bytesPerSample == 2) ? SampleIO::_16bit : SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::signedPCM) .ReadSample(sample, bodyChunk); sample.PrecomputeLoops(*this, false); return true; } OPENMPT_NAMESPACE_END