/* * WAVTools.cpp * ------------ * Purpose: Definition of WAV file structures and helper functions * 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 "Loaders.h" #include "WAVTools.h" #include "Tagging.h" #include "../common/version.h" #ifndef MODPLUG_NO_FILESAVE #include "../common/mptFileIO.h" #endif OPENMPT_NAMESPACE_BEGIN /////////////////////////////////////////////////////////// // WAV Reading WAVReader::WAVReader(FileReader &inputFile) : file(inputFile) { file.Rewind(); RIFFHeader fileHeader; codePage = 28591; // ISO 8859-1 isDLS = false; subFormat = 0; mayBeCoolEdit16_8 = false; if(!file.ReadStruct(fileHeader) || (fileHeader.magic != RIFFHeader::idRIFF && fileHeader.magic != RIFFHeader::idLIST) || (fileHeader.type != RIFFHeader::idWAVE && fileHeader.type != RIFFHeader::idwave)) { return; } isDLS = (fileHeader.magic == RIFFHeader::idLIST); auto chunks = file.ReadChunks(2); if(chunks.size() >= 4 && chunks[1].GetHeader().GetID() == RIFFChunk::iddata && chunks[1].GetHeader().GetLength() % 2u != 0 && chunks[2].GetHeader().GetLength() == 0 && chunks[3].GetHeader().GetID() == RIFFChunk::id____) { // Houston, we have a problem: Old versions of (Open)MPT didn't write RIFF padding bytes. -_- // Luckily, the only RIFF chunk with an odd size those versions would ever write would be the "data" chunk // (which contains the sample data), and its size is only odd iff the sample has an odd length and is in // 8-Bit mono format. In all other cases, the sample size (and thus the chunk size) is even. // And we're even more lucky: The versions of (Open)MPT in question will always write a relatively small // (smaller than 256 bytes) "smpl" chunk after the "data" chunk. This means that after an unpadded sample, // we will always read "mpl?" (? being the length of the "smpl" chunk) as the next chunk magic. The first two // 32-Bit members of the "smpl" chunk are always zero in our case, so we are going to read a chunk length of 0 // next and the next chunk magic, which will always consist of four zero bytes. Hooray! We just checked for those // four zero bytes and can be pretty confident that we should not have applied padding. file.Seek(sizeof(RIFFHeader)); chunks = file.ReadChunks(1); } // Read format chunk FileReader formatChunk = chunks.GetChunk(RIFFChunk::idfmt_); if(!formatChunk.ReadStruct(formatInfo)) { return; } if(formatInfo.format == WAVFormatChunk::fmtPCM && formatChunk.BytesLeft() == 4) { uint16 size = formatChunk.ReadIntLE(); uint16 value = formatChunk.ReadIntLE(); if(size == 2 && value == 1) { // May be Cool Edit 16.8 format. // See SampleFormats.cpp for details. mayBeCoolEdit16_8 = true; } } else if(formatInfo.format == WAVFormatChunk::fmtExtensible) { WAVFormatChunkExtension extFormat; if(!formatChunk.ReadStruct(extFormat)) { return; } subFormat = static_cast(mpt::UUID(extFormat.subFormat).GetData1()); } // Read sample data sampleData = chunks.GetChunk(RIFFChunk::iddata); if(!sampleData.IsValid()) { // The old IMA ADPCM loader code looked for the "pcm " chunk instead of the "data" chunk... // Dunno why (Windows XP's audio recorder saves IMA ADPCM files with a "data" chunk), but we will just look for both. sampleData = chunks.GetChunk(RIFFChunk::idpcm_); } // "fact" chunk should contain sample length of compressed samples. sampleLength = chunks.GetChunk(RIFFChunk::idfact).ReadUint32LE(); if((formatInfo.format != WAVFormatChunk::fmtIMA_ADPCM || sampleLength == 0) && GetSampleSize() != 0) { if((GetBlockAlign() == 0) || (GetBlockAlign() / GetNumChannels() >= 2 * GetSampleSize())) { // Some samples have an incorrect blockAlign / sample size set (e.g. it's 8 in SQUARE.WAV while it should be 1), so let's better not always trust this value. // The idea here is, if block align is off by twice or more, it is unlikely to be describing sample padding inside the block. // Ignore it in this case and calculate the length based on the single sample size and number of channels instead. sampleLength = sampleData.GetLength() / GetSampleSize(); } else { // Correct case (so that 20bit WAVEFORMATEX files work). sampleLength = sampleData.GetLength() / GetBlockAlign(); } } // Determine string encoding codePage = GetFileCodePage(chunks); // Check for loop points, texts, etc... FindMetadataChunks(chunks); // DLS bank chunk wsmpChunk = chunks.GetChunk(RIFFChunk::idwsmp); } void WAVReader::FindMetadataChunks(ChunkReader::ChunkList &chunks) { // Read sample loop points and other sampler information smplChunk = chunks.GetChunk(RIFFChunk::idsmpl); instChunk = chunks.GetChunk(RIFFChunk::idinst); // Read sample cues cueChunk = chunks.GetChunk(RIFFChunk::idcue_); // Read text chunks ChunkReader listChunk = chunks.GetChunk(RIFFChunk::idLIST); if(listChunk.ReadMagic("INFO")) { infoChunk = listChunk.ReadChunks(2); } // Read MPT sample information xtraChunk = chunks.GetChunk(RIFFChunk::idxtra); } uint16 WAVReader::GetFileCodePage(ChunkReader::ChunkList &chunks) { FileReader csetChunk = chunks.GetChunk(RIFFChunk::idCSET); if(!csetChunk.IsValid()) { FileReader iSFT = infoChunk.GetChunk(RIFFChunk::idISFT); if(iSFT.ReadMagic("OpenMPT")) { std::string versionString; iSFT.ReadString(versionString, iSFT.BytesLeft()); versionString = mpt::String::Trim(versionString); Version version = Version::Parse(mpt::ToUnicode(mpt::Charset::ISO8859_1, versionString)); if(version && version < MPT_V("1.28.00.02")) { return 1252; // mpt::Charset::Windows1252; // OpenMPT up to and including 1.28.00.01 wrote metadata in windows-1252 encoding } else { return 28591; // mpt::Charset::ISO8859_1; // as per spec } } else { return 28591; // mpt::Charset::ISO8859_1; // as per spec } } if(!csetChunk.CanRead(2)) { // chunk not parsable return 28591; // mpt::Charset::ISO8859_1; } uint16 codepage = csetChunk.ReadUint16LE(); return codepage; } void WAVReader::ApplySampleSettings(ModSample &sample, mpt::Charset sampleCharset, mpt::charbuf &sampleName) { // Read sample name FileReader textChunk = infoChunk.GetChunk(RIFFChunk::idINAM); if(textChunk.IsValid()) { std::string sampleNameEncoded; textChunk.ReadString(sampleNameEncoded, textChunk.GetLength()); sampleName = mpt::ToCharset(sampleCharset, mpt::ToUnicode(codePage, mpt::Charset::Windows1252, sampleNameEncoded)); } if(isDLS) { // DLS sample -> sample filename sample.filename = sampleName; } // Read software name const bool isOldMPT = infoChunk.GetChunk(RIFFChunk::idISFT).ReadMagic("Modplug Tracker"); // Convert loops WAVSampleInfoChunk sampleInfo; smplChunk.Rewind(); if(smplChunk.ReadStruct(sampleInfo)) { WAVSampleLoop loopData; if(sampleInfo.numLoops > 1 && smplChunk.ReadStruct(loopData)) { // First loop: Sustain loop loopData.ApplyToSample(sample.nSustainStart, sample.nSustainEnd, sample.nLength, sample.uFlags, CHN_SUSTAINLOOP, CHN_PINGPONGSUSTAIN, isOldMPT); } // First loop (if only one loop is present) or second loop (if more than one loop is present): Normal sample loop if(smplChunk.ReadStruct(loopData)) { loopData.ApplyToSample(sample.nLoopStart, sample.nLoopEnd, sample.nLength, sample.uFlags, CHN_LOOP, CHN_PINGPONGLOOP, isOldMPT); } //sample.Transpose((60 - sampleInfo.baseNote) / 12.0); sample.rootNote = static_cast(sampleInfo.baseNote); if(sample.rootNote < 128) sample.rootNote += NOTE_MIN; else sample.rootNote = NOTE_NONE; sample.SanitizeLoops(); } if(sample.rootNote == NOTE_NONE && instChunk.LengthIsAtLeast(sizeof(WAVInstrumentChunk))) { WAVInstrumentChunk inst; instChunk.Rewind(); if(instChunk.ReadStruct(inst)) { sample.rootNote = inst.unshiftedNote; if(sample.rootNote < 128) sample.rootNote += NOTE_MIN; else sample.rootNote = NOTE_NONE; } } // Read cue points if(cueChunk.IsValid()) { uint32 numPoints = cueChunk.ReadUint32LE(); LimitMax(numPoints, mpt::saturate_cast(MPT_ARRAY_COUNT(sample.cues))); for(uint32 i = 0; i < numPoints; i++) { WAVCuePoint cuePoint; cueChunk.ReadStruct(cuePoint); sample.cues[i] = cuePoint.position; } std::fill(std::begin(sample.cues) + numPoints, std::end(sample.cues), MAX_SAMPLE_LENGTH); } // Read MPT extra info WAVExtraChunk mptInfo; xtraChunk.Rewind(); if(xtraChunk.ReadStruct(mptInfo)) { if(mptInfo.flags & WAVExtraChunk::setPanning) sample.uFlags.set(CHN_PANNING); sample.nPan = std::min(static_cast(mptInfo.defaultPan), uint16(256)); sample.nVolume = std::min(static_cast(mptInfo.defaultVolume), uint16(256)); sample.nGlobalVol = std::min(static_cast(mptInfo.globalVolume), uint16(64)); sample.nVibType = static_cast(mptInfo.vibratoType.get()); sample.nVibSweep = mptInfo.vibratoSweep; sample.nVibDepth = mptInfo.vibratoDepth; sample.nVibRate = mptInfo.vibratoRate; if(xtraChunk.CanRead(MAX_SAMPLENAME)) { // Name present (clipboard only) // FIXME: When modules can have individual encoding in OpenMPT or when // internal metadata gets converted to Unicode, we must adjust this to // also specify encoding. xtraChunk.ReadString(sampleName, MAX_SAMPLENAME); xtraChunk.ReadString(sample.filename, xtraChunk.BytesLeft()); } } } // Apply WAV loop information to a mod sample. void WAVSampleLoop::ApplyToSample(SmpLength &start, SmpLength &end, SmpLength sampleLength, SampleFlags &flags, ChannelFlags enableFlag, ChannelFlags bidiFlag, bool mptLoopFix) const { if(loopEnd == 0) { // Some WAV files seem to have loops going from 0 to 0... We should ignore those. return; } start = std::min(static_cast(loopStart), sampleLength); end = Clamp(static_cast(loopEnd), start, sampleLength); if(!mptLoopFix && end < sampleLength) { // RIFF loop end points are inclusive - old versions of MPT didn't consider this. end++; } flags.set(enableFlag); if(loopType == loopBidi) { flags.set(bidiFlag); } } // Convert internal loop information into a WAV loop. void WAVSampleLoop::ConvertToWAV(SmpLength start, SmpLength end, bool bidi) { identifier = 0; loopType = bidi ? loopBidi : loopForward; loopStart = mpt::saturate_cast(start); // Loop ends are *inclusive* in the RIFF standard, while they're *exclusive* in OpenMPT. if(end > start) { loopEnd = mpt::saturate_cast(end - 1); } else { loopEnd = loopStart; } fraction = 0; playCount = 0; } #ifndef MODPLUG_NO_FILESAVE /////////////////////////////////////////////////////////// // WAV Writing // Output to stream: Initialize with std::ostream*. WAVWriter::WAVWriter(std::ostream *stream) : s(stream) { // Skip file header for now Seek(sizeof(RIFFHeader)); } // Output to clipboard: Initialize with pointer to memory and size of reserved memory. WAVWriter::WAVWriter(mpt::byte_span data) : memory(data) { // Skip file header for now Seek(sizeof(RIFFHeader)); } WAVWriter::~WAVWriter() noexcept(false) { if(!s || s->good()) { Finalize(); } } // Finalize the file by closing the last open chunk and updating the file header. Returns total size of file. size_t WAVWriter::Finalize() { FinalizeChunk(); RIFFHeader fileHeader; Clear(fileHeader); fileHeader.magic = RIFFHeader::idRIFF; fileHeader.length = static_cast(totalSize - 8); fileHeader.type = RIFFHeader::idWAVE; Seek(0); Write(fileHeader); s = nullptr; memory = {}; return totalSize; } // Write a new chunk header to the file. void WAVWriter::StartChunk(RIFFChunk::ChunkIdentifiers id) { FinalizeChunk(); chunkStartPos = position; chunkHeader.id = id; Skip(sizeof(chunkHeader)); } // End current chunk by updating the chunk header and writing a padding byte if necessary. void WAVWriter::FinalizeChunk() { if(chunkStartPos != 0) { const size_t chunkSize = position - (chunkStartPos + sizeof(RIFFChunk)); chunkHeader.length = mpt::saturate_cast(chunkSize); size_t curPos = position; Seek(chunkStartPos); Write(chunkHeader); Seek(curPos); if((chunkSize % 2u) != 0) { // Write padding uint8 padding = 0; Write(padding); } chunkStartPos = 0; } } // Seek to a position in file. void WAVWriter::Seek(size_t pos) { position = pos; totalSize = std::max(totalSize, position); if(s != nullptr) { s->seekp(position); } } // Write some data to the file. void WAVWriter::Write(const void *data, size_t numBytes) { if(s != nullptr) { s->write(mpt::void_cast(data), numBytes); } else if(!memory.empty()) { if(position <= memory.size() && numBytes <= memory.size() - position) { memcpy(memory.data() + position, data, numBytes); } else { // Should never happen - did we calculate a wrong memory size? MPT_ASSERT_NOTREACHED(); } } position += numBytes; totalSize = std::max(totalSize, position); } // Write the WAV format to the file. void WAVWriter::WriteFormat(uint32 sampleRate, uint16 bitDepth, uint16 numChannels, WAVFormatChunk::SampleFormats encoding) { StartChunk(RIFFChunk::idfmt_); WAVFormatChunk wavFormat; Clear(wavFormat); bool extensible = (numChannels > 2); wavFormat.format = static_cast(extensible ? WAVFormatChunk::fmtExtensible : encoding); wavFormat.numChannels = numChannels; wavFormat.sampleRate = sampleRate; wavFormat.blockAlign = (bitDepth * numChannels + 7) / 8; wavFormat.byteRate = wavFormat.sampleRate * wavFormat.blockAlign; wavFormat.bitsPerSample = bitDepth; Write(wavFormat); if(extensible) { WAVFormatChunkExtension extFormat; Clear(extFormat); extFormat.size = sizeof(WAVFormatChunkExtension) - sizeof(uint16); extFormat.validBitsPerSample = bitDepth; switch(numChannels) { case 1: extFormat.channelMask = 0x0004; // FRONT_CENTER break; case 2: extFormat.channelMask = 0x0003; // FRONT_LEFT | FRONT_RIGHT break; case 3: extFormat.channelMask = 0x0103; // FRONT_LEFT | FRONT_RIGHT | BACK_CENTER break; case 4: extFormat.channelMask = 0x0033; // FRONT_LEFT | FRONT_RIGHT | BACK_LEFT | BACK_RIGHT break; default: extFormat.channelMask = 0; break; } extFormat.subFormat = mpt::UUID(static_cast(encoding), 0x0000, 0x0010, 0x800000AA00389B71ull); Write(extFormat); } } // Write text tags to the file. void WAVWriter::WriteMetatags(const FileTags &tags) { StartChunk(RIFFChunk::idCSET); Write(mpt::as_le(uint16(65001))); // code page (UTF-8) Write(mpt::as_le(uint16(0))); // country code (unset) Write(mpt::as_le(uint16(0))); // language (unset) Write(mpt::as_le(uint16(0))); // dialect (unset) StartChunk(RIFFChunk::idLIST); const char info[] = { 'I', 'N', 'F', 'O' }; WriteArray(info); WriteTag(RIFFChunk::idINAM, tags.title); WriteTag(RIFFChunk::idIART, tags.artist); WriteTag(RIFFChunk::idIPRD, tags.album); WriteTag(RIFFChunk::idICRD, tags.year); WriteTag(RIFFChunk::idICMT, tags.comments); WriteTag(RIFFChunk::idIGNR, tags.genre); WriteTag(RIFFChunk::idTURL, tags.url); WriteTag(RIFFChunk::idISFT, tags.encoder); //WriteTag(RIFFChunk:: , tags.bpm); WriteTag(RIFFChunk::idTRCK, tags.trackno); } // Write a single tag into a open idLIST chunk void WAVWriter::WriteTag(RIFFChunk::ChunkIdentifiers id, const mpt::ustring &utext) { std::string text = mpt::ToCharset(mpt::Charset::UTF8, utext); text = text.substr(0, uint32_max - 1u); if(!text.empty()) { const uint32 length = mpt::saturate_cast(text.length() + 1); RIFFChunk chunk; Clear(chunk); chunk.id = static_cast(id); chunk.length = length; Write(chunk); Write(text.c_str(), length); if((length % 2u) != 0) { uint8 padding = 0; Write(padding); } } } // Write a sample loop information chunk to the file. void WAVWriter::WriteLoopInformation(const ModSample &sample) { if(!sample.uFlags[CHN_LOOP | CHN_SUSTAINLOOP] && !ModCommand::IsNote(sample.rootNote)) { return; } StartChunk(RIFFChunk::idsmpl); WAVSampleInfoChunk info; uint32 sampleRate = sample.nC5Speed; if(sampleRate == 0) { sampleRate = ModSample::TransposeToFrequency(sample.RelativeTone, sample.nFineTune); } info.ConvertToWAV(sampleRate, sample.rootNote); // Set up loops WAVSampleLoop loops[2]; Clear(loops); if(sample.uFlags[CHN_SUSTAINLOOP]) { loops[info.numLoops++].ConvertToWAV(sample.nSustainStart, sample.nSustainEnd, sample.uFlags[CHN_PINGPONGSUSTAIN]); } if(sample.uFlags[CHN_LOOP]) { loops[info.numLoops++].ConvertToWAV(sample.nLoopStart, sample.nLoopEnd, sample.uFlags[CHN_PINGPONGLOOP]); } else if(sample.uFlags[CHN_SUSTAINLOOP]) { // Since there are no "loop types" to distinguish between sustain and normal loops, OpenMPT assumes // that the first loop is a sustain loop if there are two loops. If we only want a sustain loop, // we will have to write a second bogus loop. loops[info.numLoops++].ConvertToWAV(0, 0, false); } Write(info); for(uint32 i = 0; i < info.numLoops; i++) { Write(loops[i]); } } // Write a sample's cue points to the file. void WAVWriter::WriteCueInformation(const ModSample &sample) { uint32 numMarkers = 0; for(const auto cue : sample.cues) { if(cue < sample.nLength) numMarkers++; } StartChunk(RIFFChunk::idcue_); Write(mpt::as_le(numMarkers)); uint32 i = 0; for(const auto cue : sample.cues) { if(cue < sample.nLength) { WAVCuePoint cuePoint; cuePoint.ConvertToWAV(i++, cue); Write(cuePoint); } } } // Write MPT's sample information chunk to the file. void WAVWriter::WriteExtraInformation(const ModSample &sample, MODTYPE modType, const char *sampleName) { StartChunk(RIFFChunk::idxtra); WAVExtraChunk mptInfo; mptInfo.ConvertToWAV(sample, modType); Write(mptInfo); if(sampleName != nullptr) { // Write sample name (clipboard only) // FIXME: When modules can have individual encoding in OpenMPT or when // internal metadata gets converted to Unicode, we must adjust this to // also specify encoding. char name[MAX_SAMPLENAME]; mpt::String::WriteBuf(mpt::String::nullTerminated, name) = sampleName; WriteArray(name); char filename[MAX_SAMPLEFILENAME]; mpt::String::WriteBuf(mpt::String::nullTerminated, filename) = sample.filename; WriteArray(filename); } } #endif // MODPLUG_NO_FILESAVE OPENMPT_NAMESPACE_END