2024-09-24 12:54:57 +00:00
/*
* 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 "mpt/io/io.hpp"
# include "mpt/io/io_virtual_wrapper.hpp"
# 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 < RIFFChunk > ( 2 ) ;
if ( chunks . chunks . size ( ) > = 4
& & chunks . chunks [ 1 ] . GetHeader ( ) . GetID ( ) = = RIFFChunk : : iddata
& & chunks . chunks [ 1 ] . GetHeader ( ) . GetLength ( ) % 2u ! = 0
& & chunks . chunks [ 2 ] . GetHeader ( ) . GetLength ( ) = = 0
& & chunks . 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 < RIFFChunk > ( 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 > ( ) ;
uint16 value = formatChunk . ReadIntLE < uint16 > ( ) ;
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 < uint16 > ( 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 ( FileReader : : ChunkList < RIFFChunk > & 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
FileReader listChunk = chunks . GetChunk ( RIFFChunk : : idLIST ) ;
if ( listChunk . ReadMagic ( " INFO " ) )
{
infoChunk = listChunk . ReadChunks < RIFFChunk > ( 2 ) ;
}
// Read MPT sample information
xtraChunk = chunks . GetChunk ( RIFFChunk : : idxtra ) ;
}
uint16 WAVReader : : GetFileCodePage ( FileReader : : ChunkList < RIFFChunk > & 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 < mpt : : String : : maybeNullTerminated > ( versionString , iSFT . BytesLeft ( ) ) ;
versionString = mpt : : trim ( versionString ) ;
Version version = Version : : Parse ( mpt : : ToUnicode ( mpt : : Charset : : ISO8859_1 , versionString ) ) ;
if ( version & & version < MPT_V ( " 1.28.00.02 " ) )
{
2024-09-29 02:04:03 +00:00
return 1252 ; // mpt::Charset::Windows1252; // OpenMPT up to and including 1.28.00.01 wrote metadata in arch-1252 encoding
2024-09-24 12:54:57 +00:00
} 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 < MAX_SAMPLENAME > & sampleName )
{
// Read sample name
FileReader textChunk = infoChunk . GetChunk ( RIFFChunk : : idINAM ) ;
if ( textChunk . IsValid ( ) )
{
std : : string sampleNameEncoded ;
textChunk . ReadString < mpt : : String : : nullTerminated > ( 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 < uint8 > ( 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 < uint32 > ( std : : size ( 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 < uint16 > ( mptInfo . defaultPan ) , uint16 ( 256 ) ) ;
sample . nVolume = std : : min ( static_cast < uint16 > ( mptInfo . defaultVolume ) , uint16 ( 256 ) ) ;
sample . nGlobalVol = std : : min ( static_cast < uint16 > ( mptInfo . globalVolume ) , uint16 ( 64 ) ) ;
sample . nVibType = static_cast < VibratoType > ( 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 < mpt : : String : : nullTerminated > ( sampleName , MAX_SAMPLENAME ) ;
xtraChunk . ReadString < mpt : : String : : nullTerminated > ( 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 < SmpLength > ( loopStart ) , sampleLength ) ;
end = Clamp ( static_cast < SmpLength > ( 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 < uint32 > ( start ) ;
// Loop ends are *inclusive* in the RIFF standard, while they're *exclusive* in OpenMPT.
if ( end > start )
{
loopEnd = mpt : : saturate_cast < uint32 > ( end - 1 ) ;
} else
{
loopEnd = loopStart ;
}
fraction = 0 ;
playCount = 0 ;
}
# ifndef MODPLUG_NO_FILESAVE
///////////////////////////////////////////////////////////
// WAV Writing
// Output to stream: Initialize with std::ostream*.
WAVWriter : : WAVWriter ( mpt : : IO : : OFileBase & stream )
: s ( stream )
{
// Skip file header for now
Seek ( sizeof ( RIFFHeader ) ) ;
}
WAVWriter : : ~ WAVWriter ( )
{
MPT_ASSERT ( finalized ) ;
}
// Finalize the file by closing the last open chunk and updating the file header. Returns total size of file.
std : : size_t WAVWriter : : Finalize ( )
{
FinalizeChunk ( ) ;
RIFFHeader fileHeader ;
Clear ( fileHeader ) ;
fileHeader . magic = RIFFHeader : : idRIFF ;
fileHeader . length = static_cast < uint32 > ( totalSize - 8 ) ;
fileHeader . type = RIFFHeader : : idWAVE ;
Seek ( 0 ) ;
Write ( fileHeader ) ;
finalized = true ;
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 std : : size_t chunkSize = position - ( chunkStartPos + sizeof ( RIFFChunk ) ) ;
chunkHeader . length = mpt : : saturate_cast < uint32 > ( chunkSize ) ;
std : : 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 ( std : : size_t pos )
{
position = pos ;
totalSize = std : : max ( totalSize , position ) ;
mpt : : IO : : SeekAbsolute ( s , pos ) ;
}
// Write some data to the file.
void WAVWriter : : Write ( mpt : : const_byte_span data )
{
MPT_ASSERT ( ! finalized ) ;
auto success = mpt : : IO : : WriteRaw ( s , data ) ;
MPT_ASSERT ( success ) ; // this assertion is useful to catch mis-calculation of required buffer size for pre-allocate in-memory file buffers (like in View_smp.cpp for clipboard)
if ( ! success )
{
return ;
}
position + = data . size ( ) ;
totalSize = std : : max ( totalSize , position ) ;
}
void WAVWriter : : WriteBeforeDirect ( )
{
MPT_ASSERT ( ! finalized ) ;
}
void WAVWriter : : WriteAfterDirect ( bool success , std : : size_t count )
{
MPT_ASSERT ( success ) ; // this assertion is useful to catch mis-calculation of required buffer size for pre-allocate in-memory file buffers (like in View_smp.cpp for clipboard)
if ( ! success )
{
return ;
}
position + = count ;
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 < uint16 > ( 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 < uint16 > ( 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 ' } ;
Write ( 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 < uint32 > ( text . length ( ) + 1 ) ;
RIFFChunk chunk ;
Clear ( chunk ) ;
chunk . id = static_cast < uint32 > ( id ) ;
chunk . length = length ;
Write ( chunk ) ;
Write ( mpt : : byte_cast < mpt : : const_byte_span > ( mpt : : span ( 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 ;
Write ( name ) ;
char filename [ MAX_SAMPLEFILENAME ] ;
mpt : : String : : WriteBuf ( mpt : : String : : nullTerminated , filename ) = sample . filename ;
Write ( filename ) ;
}
}
# endif // MODPLUG_NO_FILESAVE
OPENMPT_NAMESPACE_END