2653 lines
73 KiB
C++
2653 lines
73 KiB
C++
/*
|
|
* 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 "Dlsbank.h"
|
|
#endif // MODPLUG_TRACKER
|
|
#include "../soundlib/AudioCriticalSection.h"
|
|
#ifndef MODPLUG_NO_FILESAVE
|
|
#include "mpt/io/base.hpp"
|
|
#include "mpt/io/io.hpp"
|
|
#include "mpt/io/io_stdstream.hpp"
|
|
#include "../common/mptFileIO.h"
|
|
#endif // !MODPLUG_NO_FILESAVE
|
|
#include "../common/misc_util.h"
|
|
#include "openmpt/base/Endian.hpp"
|
|
#include "Tagging.h"
|
|
#include "ITTools.h"
|
|
#include "XMTools.h"
|
|
#include "S3MTools.h"
|
|
#include "WAVTools.h"
|
|
#include "../common/version.h"
|
|
#include "Loaders.h"
|
|
#include "../common/FileReader.h"
|
|
#include "../soundlib/ModSampleCopy.h"
|
|
#include <functional>
|
|
#include <map>
|
|
|
|
|
|
OPENMPT_NAMESPACE_BEGIN
|
|
|
|
|
|
using namespace mpt::uuid_literals;
|
|
|
|
|
|
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)
|
|
&& !ReadBRRSample(nSample, file)
|
|
&& !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.GetOptionalFileName().value_or(P_("")));
|
|
#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<bool> 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<SAMPLEINDEX> sourceSample; // Sample index in source song
|
|
std::vector<SAMPLEINDEX> 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(pIns->filename.empty() && srcSong.GetpModDoc() != nullptr && &srcSong != this)
|
|
{
|
|
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;
|
|
m_szNames[targetSample] = srcSong.m_szNames[sourceSample];
|
|
|
|
if(sourceSmp.HasSampleData())
|
|
{
|
|
if(targetSmp.CopyWaveform(sourceSmp))
|
|
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((targetSmp.filename.empty()) && srcSong.GetpModDoc() != nullptr && &srcSong != this)
|
|
{
|
|
targetSmp.filename = mpt::ToCharset(GetCharsetInternal(), srcSong.GetpModDoc()->GetTitle());
|
|
}
|
|
#endif
|
|
|
|
if(targetSmp.uFlags[CHN_ADLIB] && !SupportsOPL())
|
|
{
|
|
AddToLog(LogInformation, U_("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 constexpr int8 IMAIndexTab[8] = { -1, -1, -1, -1, 2, 4, 6, 8 };
|
|
static constexpr int16 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::PinnedView blockView = block.GetPinnedView();
|
|
const std::byte *data = blockView.data();
|
|
const uint32 blockSize = static_cast<uint32>(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<uint8>(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<int16>(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);
|
|
|
|
static constexpr WAVFormatChunk::SampleFormats SupportedFormats[] = {WAVFormatChunk::fmtPCM, WAVFormatChunk::fmtFloat, WAVFormatChunk::fmtIMA_ADPCM, WAVFormatChunk::fmtMP3, WAVFormatChunk::fmtALaw, WAVFormatChunk::fmtULaw};
|
|
if(!wavFile.IsValid()
|
|
|| wavFile.GetNumChannels() == 0
|
|
|| wavFile.GetNumChannels() > 2
|
|
|| (wavFile.GetBitsPerSample() == 0 && wavFile.GetSampleFormat() != WAVFormatChunk::fmtMP3)
|
|
|| (wavFile.GetBitsPerSample() < 32 && wavFile.GetSampleFormat() == WAVFormatChunk::fmtFloat)
|
|
|| (wavFile.GetBitsPerSample() > 64)
|
|
|| !mpt::contains(SupportedFormats, wavFile.GetSampleFormat()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DestroySampleThreadsafe(nSample);
|
|
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
|
|
{
|
|
const ModSample &sample = Samples[nSample];
|
|
if(sample.uFlags[CHN_ADLIB])
|
|
return false;
|
|
|
|
mpt::IO::OFile<std::ostream> ff(f);
|
|
WAVWriter file(ff);
|
|
|
|
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);
|
|
file.Finalize();
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif // MODPLUG_NO_FILESAVE
|
|
|
|
|
|
|
|
/////////////////
|
|
// Sony Wave64 //
|
|
|
|
|
|
struct Wave64FileHeader
|
|
{
|
|
mpt::GUIDms GuidRIFF;
|
|
uint64le FileSize;
|
|
mpt::GUIDms GuidWAVE;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(Wave64FileHeader, 40)
|
|
|
|
|
|
struct Wave64ChunkHeader
|
|
{
|
|
mpt::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<FileReader::off_t>(length);
|
|
}
|
|
|
|
mpt::UUID GetID() const
|
|
{
|
|
return mpt::UUID(header.GuidChunk);
|
|
}
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(Wave64Chunk, 24)
|
|
|
|
|
|
static void Wave64TagFromLISTINFO(mpt::ustring & dst, uint16 codePage, const FileReader::ChunkList<RIFFChunk> & infoChunk, RIFFChunk::ChunkIdentifiers id)
|
|
{
|
|
if(!infoChunk.ChunkExists(id))
|
|
{
|
|
return;
|
|
}
|
|
FileReader textChunk = infoChunk.GetChunk(id);
|
|
if(!textChunk.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
std::string str;
|
|
textChunk.ReadString<mpt::String::maybeNullTerminated>(str, textChunk.GetLength());
|
|
str = mpt::replace(str, std::string("\r\n"), std::string("\n"));
|
|
str = mpt::replace(str, std::string("\r"), std::string("\n"));
|
|
dst = mpt::ToUnicode(codePage, mpt::Charset::Windows1252, 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;
|
|
}
|
|
|
|
FileReader chunkFile = file;
|
|
auto chunkList = chunkFile.ReadChunks<Wave64Chunk>(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<uint16>(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::Charset::ISO8859_1
|
|
FileReader csetChunk = chunkList.GetChunk(guidCSET);
|
|
if(csetChunk.IsValid())
|
|
{
|
|
if(csetChunk.CanRead(2))
|
|
{
|
|
codePage = csetChunk.ReadUint16LE();
|
|
}
|
|
}
|
|
|
|
if(chunkList.ChunkExists(guidLIST))
|
|
{
|
|
FileReader listChunk = chunkList.GetChunk(guidLIST);
|
|
if(listChunk.ReadMagic("INFO"))
|
|
{
|
|
auto infoChunk = listChunk.ReadChunks<RIFFChunk>(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<SmpLength>(audioData.GetLength() / (sampleIO.GetEncodedBitsPerSample()/8));
|
|
|
|
ModSample &mptSample = Samples[nSample];
|
|
DestroySampleThreadsafe(nSample);
|
|
mptSample.Initialize();
|
|
mptSample.nLength = length;
|
|
mptSample.nC5Speed = format.sampleRate;
|
|
|
|
sampleIO.ReadSample(mptSample, audioData);
|
|
|
|
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; // check note.h for the correspondance.
|
|
uint32le high_freq; // check note.h for the correspondance.
|
|
uint32le 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<int32>(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);
|
|
|
|
that->m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, 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] > ' ')
|
|
{
|
|
m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, 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;
|
|
|
|
pIns->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name);
|
|
pIns->nFadeOut = 2048;
|
|
if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))
|
|
{
|
|
pIns->nNNA = NewNoteAction::NoteOff;
|
|
pIns->nDNA = DuplicateNoteAction::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) : static_cast<uint8>(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.GetSampleOffset()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DestroySampleThreadsafe(nSample);
|
|
|
|
ModSample &sample = Samples[nSample];
|
|
sampleHeader.ConvertToMPT(sample);
|
|
m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name);
|
|
|
|
if(sampleHeader.sampleType < S3MSampleHeader::typeAdMel)
|
|
sampleHeader.GetSampleFormat(false).ReadSample(sample, file);
|
|
else if(SupportsOPL())
|
|
InitOPL();
|
|
else
|
|
AddToLog(LogInformation, U_("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{};
|
|
SmpLength length = sampleHeader.ConvertToS3M(sample);
|
|
mpt::String::WriteBuf(mpt::String::nullTerminated, sampleHeader.name) = m_szNames[smp];
|
|
mpt::String::WriteBuf(mpt::String::maybeNullTerminated, sampleHeader.reserved2) = mpt::ToCharset(mpt::Charset::UTF8, 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();
|
|
const auto magic = file.ReadArray<char, 4>();
|
|
if((memcmp(magic.data(), "SBI\x1A", 4) && memcmp(magic.data(), "SBI\x1D", 4)) // 1D = broken JuceOPLVSTi files
|
|
|| !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(LogInformation, U_("OPL instruments are not supported by this format."));
|
|
return true;
|
|
}
|
|
|
|
DestroySampleThreadsafe(sample);
|
|
InitOPL();
|
|
|
|
ModSample &mptSmp = Samples[sample];
|
|
mptSmp.Initialize(MOD_TYPE_S3M);
|
|
file.ReadString<mpt::String::nullTerminated>(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<SAMPLEINDEX> 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<SampleIO> 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;
|
|
}
|
|
|
|
mptSample.filename = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name);
|
|
m_szNames[sampleMap[i]] = mpt::String::ReadBuf(mpt::String::spacePadded, 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<SAMPLEINDEX> 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<SampleIO> 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::WriteBuf(mpt::String::spacePadded, 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());
|
|
|
|
mptSample.filename = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name);
|
|
m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name);
|
|
|
|
// Read sample data
|
|
sampleHeader.GetSampleFormat().ReadSample(Samples[nSample], file);
|
|
Samples[nSample].PrecomputeLoops(*this, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
///////////////
|
|
// 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<int64>::max(); // spec
|
|
}
|
|
if(length < 0)
|
|
{
|
|
length = std::numeric_limits<int64>::max(); // heuristic
|
|
}
|
|
return mpt::saturate_cast<FileReader::off_t>(length);
|
|
}
|
|
|
|
ChunkIdentifiers GetID() const
|
|
{
|
|
return static_cast<ChunkIdentifiers>(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<std::string,std::string> & infoMap, const std::string & key)
|
|
{
|
|
auto item = infoMap.find(key);
|
|
if(item == infoMap.end())
|
|
{
|
|
return;
|
|
}
|
|
if(item->second.empty())
|
|
{
|
|
return;
|
|
}
|
|
dst = mpt::ToUnicode(mpt::Charset::UTF8, 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;
|
|
}
|
|
|
|
auto chunkList = file.ReadChunks<CAFChunk>(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(!mpt::in_range<uint32>(mpt::saturate_round<int64>(audioFormat.mSampleRate)))
|
|
{
|
|
return false;
|
|
}
|
|
uint32 sampleRate = static_cast<uint32>(mpt::saturate_round<int64>(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<SampleIO::Bitdepth>((audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame) * 8);
|
|
sampleIO = SampleIO(bitdepth, channels, endianness, encoding);
|
|
} else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(mayNormalize)
|
|
{
|
|
sampleIO.MayNormalize();
|
|
}
|
|
|
|
/*
|
|
std::map<uint32, std::string> 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<int64>();
|
|
if(offset >= 0 && mpt::in_range<FileReader::off_t>(offset))
|
|
{
|
|
stringData.Seek(mpt::saturate_cast<FileReader::off_t>(offset));
|
|
std::string str;
|
|
if(stringData.ReadNullString(str))
|
|
{
|
|
stringMap[stringID] = str;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
std::map<std::string, std::string> 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<SmpLength>((audioData.GetLength() / audioFormat.mBytesPerPacket) * audioFormat.mFramesPerPacket);
|
|
|
|
ModSample &mptSample = Samples[nSample];
|
|
DestroySampleThreadsafe(nSample);
|
|
mptSample.Initialize();
|
|
mptSample.nLength = length;
|
|
mptSample.nC5Speed = sampleRate;
|
|
|
|
sampleIO.ReadSample(mptSample, audioData);
|
|
|
|
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<ChunkIdentifiers>(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();
|
|
|
|
// Verify header
|
|
AIFFHeader fileHeader;
|
|
if(!file.ReadStruct(fileHeader)
|
|
|| memcmp(fileHeader.magic, "FORM", 4)
|
|
|| (memcmp(fileHeader.type, "AIFF", 4) && memcmp(fileHeader.type, "AIFC", 4)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto chunks = file.ReadChunks<AIFFChunk>(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))
|
|
{
|
|
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) || !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;
|
|
} else if(!memcmp(compression, "raw ", 4))
|
|
{
|
|
sampleIO |= SampleIO::unsignedPCM;
|
|
}
|
|
|
|
if(mayNormalize)
|
|
{
|
|
sampleIO.MayNormalize();
|
|
}
|
|
|
|
if(soundChunk.CanRead(sampleHeader.offset))
|
|
{
|
|
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<AIFFMarker> 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<mpt::String::spacePadded>(m_szNames[nSample], nameChunk.GetLength());
|
|
} else
|
|
{
|
|
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;
|
|
}
|
|
const auto field = std::string_view(line).substr(0, pos);
|
|
// Scan for invalid chars
|
|
for(auto c : field)
|
|
{
|
|
if(!mpt::is_in_range(c, 'a', 'z') && !mpt::is_in_range(c, 'A', 'Z') && !mpt::is_in_range(c, '0', '9') && c != '-' && c != '_')
|
|
{
|
|
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
|
|
const auto magic = file.ReadArray<char, 4>();
|
|
const bool bigEndian = !std::memcmp(magic.data(), ".snd", 4);
|
|
const bool littleEndian = !std::memcmp(magic.data(), "dns.", 4);
|
|
if(!bigEndian && !littleEndian)
|
|
return false;
|
|
|
|
auto readUint32 = std::bind(bigEndian ? &FileReader::ReadUint32BE : &FileReader::ReadUint32LE, file);
|
|
|
|
uint32 dataOffset = readUint32(); // must be divisible by 8 according to spec, however, there are files that ignore this requirement
|
|
uint32 dataSize = readUint32();
|
|
uint32 encoding = readUint32();
|
|
uint32 sampleRate = readUint32();
|
|
uint32 channels = readUint32();
|
|
|
|
// 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, bigEndian ? SampleIO::bigEndian : SampleIO::littleEndian, 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.
|
|
// We only read up to the first \0 byte.
|
|
file.Seek(24);
|
|
std::string annotation;
|
|
file.ReadString<mpt::String::maybeNullTerminated>(annotation, dataOffset - 24);
|
|
annotation = mpt::replace(annotation, std::string("\r\n"), std::string("\n"));
|
|
annotation = mpt::replace(annotation, std::string("\r"), std::string("\n"));
|
|
mpt::Charset charset = mpt::IsUTF8(annotation) ? mpt::Charset::UTF8 : mpt::Charset::ISO8859_1;
|
|
const auto lines = mpt::String::Split<std::string>(annotation, "\n");
|
|
bool hasFields = false;
|
|
for(const auto &line : lines)
|
|
{
|
|
if(AUIsAnnotationLineWithField(line))
|
|
{
|
|
hasFields = true;
|
|
break;
|
|
}
|
|
}
|
|
if(hasFields)
|
|
{
|
|
std::map<std::string, std::vector<std::string>> linesPerField;
|
|
std::string lastField = "comment";
|
|
for(const auto &line : lines)
|
|
{
|
|
if(AUIsAnnotationLineWithField(line))
|
|
{
|
|
lastField = mpt::ToLowerCaseAscii(mpt::trim(AUGetAnnotationFieldFromLine(line)));
|
|
}
|
|
linesPerField[lastField].push_back(AUTrimFieldFromAnnotationLine(line));
|
|
}
|
|
tags.title = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["title" ], std::string("\n")));
|
|
tags.artist = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["artist" ], std::string("\n")));
|
|
tags.album = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["album" ], std::string("\n")));
|
|
tags.trackno = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["track" ], std::string("\n")));
|
|
tags.genre = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["genre" ], std::string("\n")));
|
|
tags.comments = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["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::trim_right(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<SmpLength>(file.BytesLeft());
|
|
if(dataSize != 0xFFFFFFFF)
|
|
LimitMax(length, dataSize);
|
|
mptSample.nLength = (length * 8u) / (sampleIO.GetEncodedBitsPerSample() * channels);
|
|
mptSample.nC5Speed = sampleRate;
|
|
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));
|
|
m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::spacePaddedNull, sampleHeader.name);
|
|
|
|
if(sample.uFlags[CHN_ADLIB])
|
|
{
|
|
OPLPatch patch;
|
|
file.ReadArray(patch);
|
|
sample.SetAdlib(true, patch);
|
|
InitOPL();
|
|
if(!SupportsOPL())
|
|
{
|
|
AddToLog(LogInformation, U_("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<mpt::String::maybeNullTerminated>(filenameU8, strLen);
|
|
mpt::PathString filename = mpt::PathString::FromUTF8(filenameU8);
|
|
|
|
if(!filename.empty())
|
|
{
|
|
if(file.GetOptionalFileName())
|
|
{
|
|
filename = filename.RelativePathToAbsolute(file.GetOptionalFileName()->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<SAMPLEINDEX>(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<SAMPLEINDEX>(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<SAMPLEINDEX> 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<SAMPLEINDEX> smptable;
|
|
std::vector<uint8> 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<uint8>(smptable.size());
|
|
}
|
|
iti.keyboard[i * 2 + 1] = smpmap[smp - 1];
|
|
} else
|
|
{
|
|
iti.keyboard[i * 2 + 1] = 0;
|
|
}
|
|
}
|
|
iti.nos = static_cast<uint8>(smptable.size());
|
|
smpmap.clear();
|
|
|
|
uint32 filePos = instSize;
|
|
mpt::IO::WritePartial(f, iti, instSize);
|
|
|
|
filePos += mpt::saturate_cast<uint32>(smptable.size() * sizeof(ITSample));
|
|
|
|
// Writing sample headers + data
|
|
std::vector<SampleIO> sampleFlags;
|
|
for(auto smp : smptable)
|
|
{
|
|
ITSample itss;
|
|
itss.ConvertToIT(Samples[smp], GetType(), compress, compress, allowExternal);
|
|
const bool isExternal = itss.cvt == ITSample::cvtExternalSample;
|
|
|
|
mpt::String::WriteBuf(mpt::String::nullTerminated, 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<uint32>(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<uint32>(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 / MAUD Samples
|
|
|
|
// IFF File Header
|
|
struct IFFHeader
|
|
{
|
|
char form[4]; // "FORM"
|
|
uint32be size;
|
|
char magic[4]; // "8SVX", "16SV", "MAUD"
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(IFFHeader, 12)
|
|
|
|
|
|
// General IFF Chunk header
|
|
struct IFFChunk
|
|
{
|
|
// 32-Bit chunk identifiers
|
|
enum ChunkIdentifiers
|
|
{
|
|
// 8SVX / 16SV
|
|
idVHDR = MagicBE("VHDR"),
|
|
idBODY = MagicBE("BODY"),
|
|
idCHAN = MagicBE("CHAN"),
|
|
|
|
// MAUD
|
|
idMHDR = MagicBE("MHDR"),
|
|
idMDAT = MagicBE("MDAT"),
|
|
|
|
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<size_t>::max();
|
|
return length;
|
|
}
|
|
|
|
ChunkIdentifiers GetID() const
|
|
{
|
|
return static_cast<ChunkIdentifiers>(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) && memcmp(fileHeader.magic, "MAUD", 4)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const auto chunks = file.ReadChunks<IFFChunk>(2);
|
|
FileReader sampleData;
|
|
|
|
SampleIO sampleIO(SampleIO::_8bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM);
|
|
uint32 numSamples = 0, sampleRate = 0, loopStart = 0, loopLength = 0, volume = 0;
|
|
|
|
if(!memcmp(fileHeader.magic, "MAUD", 4))
|
|
{
|
|
FileReader mhdrChunk = chunks.GetChunk(IFFChunk::idMHDR);
|
|
sampleData = chunks.GetChunk(IFFChunk::idMDAT);
|
|
if(!mhdrChunk.LengthIs(32)
|
|
|| !sampleData.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
numSamples = mhdrChunk.ReadUint32BE();
|
|
const uint16 bitsPerSample = mhdrChunk.ReadUint16BE();
|
|
mhdrChunk.Skip(2); // bits per sample after decompression
|
|
sampleRate = mhdrChunk.ReadUint32BE();
|
|
const auto [clockDivide, channelInformation, numChannels, compressionType] = mhdrChunk.ReadArray<uint16be, 4>();
|
|
if(!clockDivide)
|
|
return false;
|
|
else
|
|
sampleRate /= clockDivide;
|
|
|
|
if(numChannels != (channelInformation + 1))
|
|
return false;
|
|
if(numChannels == 2)
|
|
sampleIO |= SampleIO::stereoInterleaved;
|
|
|
|
if(bitsPerSample == 8 && compressionType == 0)
|
|
sampleIO |= SampleIO::unsignedPCM;
|
|
else if(bitsPerSample == 8 && compressionType == 2)
|
|
sampleIO |= SampleIO::aLaw;
|
|
else if(bitsPerSample == 8 && compressionType == 3)
|
|
sampleIO |= SampleIO::uLaw;
|
|
else if(bitsPerSample == 16 && compressionType == 0)
|
|
sampleIO |= SampleIO::_16bit;
|
|
else
|
|
return false;
|
|
} else
|
|
{
|
|
FileReader vhdrChunk = chunks.GetChunk(IFFChunk::idVHDR);
|
|
FileReader chanChunk = chunks.GetChunk(IFFChunk::idCHAN);
|
|
sampleData = chunks.GetChunk(IFFChunk::idBODY);
|
|
IFFSampleHeader sampleHeader;
|
|
if(!sampleData.IsValid()
|
|
|| !vhdrChunk.IsValid()
|
|
|| !vhdrChunk.ReadStruct(sampleHeader))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const uint8 bytesPerSample = memcmp(fileHeader.magic, "8SVX", 4) ? 2 : 1;
|
|
const uint8 numChannels = chanChunk.ReadUint32BE() == 6 ? 2 : 1;
|
|
const uint8 bytesPerFrame = bytesPerSample * numChannels;
|
|
|
|
// While this is an Amiga format, the 16SV version appears to be only used on PC, and only with little-endian sample data.
|
|
if(bytesPerSample == 2)
|
|
sampleIO = SampleIO(SampleIO::_16bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::signedPCM);
|
|
if(numChannels == 2)
|
|
sampleIO |= SampleIO::stereoSplit;
|
|
|
|
loopStart = sampleHeader.oneShotHiSamples / bytesPerFrame;
|
|
loopLength = sampleHeader.repeatHiSamples / bytesPerFrame;
|
|
sampleRate = sampleHeader.samplesPerSec;
|
|
volume = sampleHeader.volume;
|
|
numSamples = mpt::saturate_cast<SmpLength>(sampleData.GetLength() / bytesPerFrame);
|
|
}
|
|
|
|
DestroySampleThreadsafe(nSample);
|
|
ModSample &sample = Samples[nSample];
|
|
sample.Initialize();
|
|
sample.nLength = numSamples;
|
|
sample.nLoopStart = loopStart;
|
|
sample.nLoopEnd = sample.nLoopStart + loopLength;
|
|
if((sample.nLoopStart + 4 < sample.nLoopEnd) && (sample.nLoopEnd <= sample.nLength))
|
|
sample.uFlags.set(CHN_LOOP);
|
|
|
|
sample.nC5Speed = sampleRate;
|
|
if(!sample.nC5Speed)
|
|
sample.nC5Speed = 22050;
|
|
|
|
sample.nVolume = static_cast<uint16>(volume / 256);
|
|
if(!sample.nVolume || sample.nVolume > 256)
|
|
sample.nVolume = 256;
|
|
|
|
sample.Convert(MOD_TYPE_IT, GetType());
|
|
|
|
FileReader nameChunk = chunks.GetChunk(IFFChunk::idNAME);
|
|
if(nameChunk.IsValid())
|
|
nameChunk.ReadString<mpt::String::maybeNullTerminated>(m_szNames[nSample], nameChunk.GetLength());
|
|
else
|
|
m_szNames[nSample] = "";
|
|
|
|
sampleIO.ReadSample(sample, sampleData);
|
|
sample.PrecomputeLoops(*this, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
OPENMPT_NAMESPACE_END
|