1 /*
2  * WAVTools.cpp
3  * ------------
4  * Purpose: Definition of WAV file structures and helper functions
5  * Notes  : (currently none)
6  * Authors: OpenMPT Devs
7  * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
8  */
9 
10 
11 #include "stdafx.h"
12 #include "Loaders.h"
13 #include "WAVTools.h"
14 #include "Tagging.h"
15 #include "../common/version.h"
16 #ifndef MODPLUG_NO_FILESAVE
17 #include "mpt/io/io.hpp"
18 #include "mpt/io/io_virtual_wrapper.hpp"
19 #include "../common/mptFileIO.h"
20 #endif
21 
22 
23 OPENMPT_NAMESPACE_BEGIN
24 
25 
26 ///////////////////////////////////////////////////////////
27 // WAV Reading
28 
29 
WAVReader(FileReader & inputFile)30 WAVReader::WAVReader(FileReader &inputFile) : file(inputFile)
31 {
32 	file.Rewind();
33 
34 	RIFFHeader fileHeader;
35 	codePage = 28591; // ISO 8859-1
36 	isDLS = false;
37 	subFormat = 0;
38 	mayBeCoolEdit16_8 = false;
39 	if(!file.ReadStruct(fileHeader)
40 		|| (fileHeader.magic != RIFFHeader::idRIFF && fileHeader.magic != RIFFHeader::idLIST)
41 		|| (fileHeader.type != RIFFHeader::idWAVE && fileHeader.type != RIFFHeader::idwave))
42 	{
43 		return;
44 	}
45 
46 	isDLS = (fileHeader.magic == RIFFHeader::idLIST);
47 
48 	auto chunks = file.ReadChunks<RIFFChunk>(2);
49 
50 	if(chunks.chunks.size() >= 4
51 		&& chunks.chunks[1].GetHeader().GetID() == RIFFChunk::iddata
52 		&& chunks.chunks[1].GetHeader().GetLength() % 2u != 0
53 		&& chunks.chunks[2].GetHeader().GetLength() == 0
54 		&& chunks.chunks[3].GetHeader().GetID() == RIFFChunk::id____)
55 	{
56 		// Houston, we have a problem: Old versions of (Open)MPT didn't write RIFF padding bytes. -_-
57 		// Luckily, the only RIFF chunk with an odd size those versions would ever write would be the "data" chunk
58 		// (which contains the sample data), and its size is only odd iff the sample has an odd length and is in
59 		// 8-Bit mono format. In all other cases, the sample size (and thus the chunk size) is even.
60 
61 		// And we're even more lucky: The versions of (Open)MPT in question will always write a relatively small
62 		// (smaller than 256 bytes) "smpl" chunk after the "data" chunk. This means that after an unpadded sample,
63 		// we will always read "mpl?" (? being the length of the "smpl" chunk) as the next chunk magic. The first two
64 		// 32-Bit members of the "smpl" chunk are always zero in our case, so we are going to read a chunk length of 0
65 		// next and the next chunk magic, which will always consist of four zero bytes. Hooray! We just checked for those
66 		// four zero bytes and can be pretty confident that we should not have applied padding.
67 		file.Seek(sizeof(RIFFHeader));
68 		chunks = file.ReadChunks<RIFFChunk>(1);
69 	}
70 
71 	// Read format chunk
72 	FileReader formatChunk = chunks.GetChunk(RIFFChunk::idfmt_);
73 	if(!formatChunk.ReadStruct(formatInfo))
74 	{
75 		return;
76 	}
77 	if(formatInfo.format == WAVFormatChunk::fmtPCM && formatChunk.BytesLeft() == 4)
78 	{
79 		uint16 size = formatChunk.ReadIntLE<uint16>();
80 		uint16 value = formatChunk.ReadIntLE<uint16>();
81 		if(size == 2 && value == 1)
82 		{
83 			// May be Cool Edit 16.8 format.
84 			// See SampleFormats.cpp for details.
85 			mayBeCoolEdit16_8 = true;
86 		}
87 	} else if(formatInfo.format == WAVFormatChunk::fmtExtensible)
88 	{
89 		WAVFormatChunkExtension extFormat;
90 		if(!formatChunk.ReadStruct(extFormat))
91 		{
92 			return;
93 		}
94 		subFormat = static_cast<uint16>(mpt::UUID(extFormat.subFormat).GetData1());
95 	}
96 
97 	// Read sample data
98 	sampleData = chunks.GetChunk(RIFFChunk::iddata);
99 
100 	if(!sampleData.IsValid())
101 	{
102 		// The old IMA ADPCM loader code looked for the "pcm " chunk instead of the "data" chunk...
103 		// Dunno why (Windows XP's audio recorder saves IMA ADPCM files with a "data" chunk), but we will just look for both.
104 		sampleData = chunks.GetChunk(RIFFChunk::idpcm_);
105 	}
106 
107 	// "fact" chunk should contain sample length of compressed samples.
108 	sampleLength = chunks.GetChunk(RIFFChunk::idfact).ReadUint32LE();
109 
110 	if((formatInfo.format != WAVFormatChunk::fmtIMA_ADPCM || sampleLength == 0) && GetSampleSize() != 0)
111 	{
112 		if((GetBlockAlign() == 0) || (GetBlockAlign() / GetNumChannels() >= 2 * GetSampleSize()))
113 		{
114 			// 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.
115 			// The idea here is, if block align is off by twice or more, it is unlikely to be describing sample padding inside the block.
116 			// Ignore it in this case and calculate the length based on the single sample size and number of channels instead.
117 			sampleLength = sampleData.GetLength() / GetSampleSize();
118 		} else
119 		{
120 			// Correct case (so that 20bit WAVEFORMATEX files work).
121 			sampleLength = sampleData.GetLength() / GetBlockAlign();
122 		}
123 	}
124 
125 	// Determine string encoding
126 	codePage = GetFileCodePage(chunks);
127 
128 	// Check for loop points, texts, etc...
129 	FindMetadataChunks(chunks);
130 
131 	// DLS bank chunk
132 	wsmpChunk = chunks.GetChunk(RIFFChunk::idwsmp);
133 }
134 
135 
FindMetadataChunks(FileReader::ChunkList<RIFFChunk> & chunks)136 void WAVReader::FindMetadataChunks(FileReader::ChunkList<RIFFChunk> &chunks)
137 {
138 	// Read sample loop points and other sampler information
139 	smplChunk = chunks.GetChunk(RIFFChunk::idsmpl);
140 	instChunk = chunks.GetChunk(RIFFChunk::idinst);
141 
142 	// Read sample cues
143 	cueChunk = chunks.GetChunk(RIFFChunk::idcue_);
144 
145 	// Read text chunks
146 	FileReader listChunk = chunks.GetChunk(RIFFChunk::idLIST);
147 	if(listChunk.ReadMagic("INFO"))
148 	{
149 		infoChunk = listChunk.ReadChunks<RIFFChunk>(2);
150 	}
151 
152 	// Read MPT sample information
153 	xtraChunk = chunks.GetChunk(RIFFChunk::idxtra);
154 }
155 
156 
GetFileCodePage(FileReader::ChunkList<RIFFChunk> & chunks)157 uint16 WAVReader::GetFileCodePage(FileReader::ChunkList<RIFFChunk> &chunks)
158 {
159 	FileReader csetChunk = chunks.GetChunk(RIFFChunk::idCSET);
160 	if(!csetChunk.IsValid())
161 	{
162 		FileReader iSFT = infoChunk.GetChunk(RIFFChunk::idISFT);
163 		if(iSFT.ReadMagic("OpenMPT"))
164 		{
165 			std::string versionString;
166 			iSFT.ReadString<mpt::String::maybeNullTerminated>(versionString, iSFT.BytesLeft());
167 			versionString = mpt::trim(versionString);
168 			Version version = Version::Parse(mpt::ToUnicode(mpt::Charset::ISO8859_1, versionString));
169 			if(version && version < MPT_V("1.28.00.02"))
170 			{
171 				return 1252; // mpt::Charset::Windows1252; // OpenMPT up to and including 1.28.00.01 wrote metadata in windows-1252 encoding
172 			} else
173 			{
174 				return 28591; // mpt::Charset::ISO8859_1; // as per spec
175 			}
176 		} else
177 		{
178 			return 28591; // mpt::Charset::ISO8859_1; // as per spec
179 		}
180 	}
181 	if(!csetChunk.CanRead(2))
182 	{
183 		// chunk not parsable
184 		return 28591; // mpt::Charset::ISO8859_1;
185 	}
186 	uint16 codepage = csetChunk.ReadUint16LE();
187 	return codepage;
188 }
189 
190 
ApplySampleSettings(ModSample & sample,mpt::Charset sampleCharset,mpt::charbuf<MAX_SAMPLENAME> & sampleName)191 void WAVReader::ApplySampleSettings(ModSample &sample, mpt::Charset sampleCharset, mpt::charbuf<MAX_SAMPLENAME> &sampleName)
192 {
193 	// Read sample name
194 	FileReader textChunk = infoChunk.GetChunk(RIFFChunk::idINAM);
195 	if(textChunk.IsValid())
196 	{
197 		std::string sampleNameEncoded;
198 		textChunk.ReadString<mpt::String::nullTerminated>(sampleNameEncoded, textChunk.GetLength());
199 		sampleName = mpt::ToCharset(sampleCharset, mpt::ToUnicode(codePage, mpt::Charset::Windows1252, sampleNameEncoded));
200 	}
201 	if(isDLS)
202 	{
203 		// DLS sample -> sample filename
204 		sample.filename = sampleName;
205 	}
206 
207 	// Read software name
208 	const bool isOldMPT = infoChunk.GetChunk(RIFFChunk::idISFT).ReadMagic("Modplug Tracker");
209 
210 	// Convert loops
211 	WAVSampleInfoChunk sampleInfo;
212 	smplChunk.Rewind();
213 	if(smplChunk.ReadStruct(sampleInfo))
214 	{
215 		WAVSampleLoop loopData;
216 		if(sampleInfo.numLoops > 1 && smplChunk.ReadStruct(loopData))
217 		{
218 			// First loop: Sustain loop
219 			loopData.ApplyToSample(sample.nSustainStart, sample.nSustainEnd, sample.nLength, sample.uFlags, CHN_SUSTAINLOOP, CHN_PINGPONGSUSTAIN, isOldMPT);
220 		}
221 		// First loop (if only one loop is present) or second loop (if more than one loop is present): Normal sample loop
222 		if(smplChunk.ReadStruct(loopData))
223 		{
224 			loopData.ApplyToSample(sample.nLoopStart, sample.nLoopEnd, sample.nLength, sample.uFlags, CHN_LOOP, CHN_PINGPONGLOOP, isOldMPT);
225 		}
226 		//sample.Transpose((60 - sampleInfo.baseNote) / 12.0);
227 		sample.rootNote = static_cast<uint8>(sampleInfo.baseNote);
228 		if(sample.rootNote < 128)
229 			sample.rootNote += NOTE_MIN;
230 		else
231 			sample.rootNote = NOTE_NONE;
232 		sample.SanitizeLoops();
233 	}
234 
235 	if(sample.rootNote == NOTE_NONE && instChunk.LengthIsAtLeast(sizeof(WAVInstrumentChunk)))
236 	{
237 		WAVInstrumentChunk inst;
238 		instChunk.Rewind();
239 		if(instChunk.ReadStruct(inst))
240 		{
241 			sample.rootNote = inst.unshiftedNote;
242 			if(sample.rootNote < 128)
243 				sample.rootNote += NOTE_MIN;
244 			else
245 				sample.rootNote = NOTE_NONE;
246 		}
247 	}
248 
249 	// Read cue points
250 	if(cueChunk.IsValid())
251 	{
252 		uint32 numPoints = cueChunk.ReadUint32LE();
253 		LimitMax(numPoints, mpt::saturate_cast<uint32>(std::size(sample.cues)));
254 		for(uint32 i = 0; i < numPoints; i++)
255 		{
256 			WAVCuePoint cuePoint;
257 			cueChunk.ReadStruct(cuePoint);
258 			sample.cues[i] = cuePoint.position;
259 		}
260 		std::fill(std::begin(sample.cues) + numPoints, std::end(sample.cues), MAX_SAMPLE_LENGTH);
261 	}
262 
263 	// Read MPT extra info
264 	WAVExtraChunk mptInfo;
265 	xtraChunk.Rewind();
266 	if(xtraChunk.ReadStruct(mptInfo))
267 	{
268 		if(mptInfo.flags & WAVExtraChunk::setPanning) sample.uFlags.set(CHN_PANNING);
269 
270 		sample.nPan = std::min(static_cast<uint16>(mptInfo.defaultPan), uint16(256));
271 		sample.nVolume = std::min(static_cast<uint16>(mptInfo.defaultVolume), uint16(256));
272 		sample.nGlobalVol = std::min(static_cast<uint16>(mptInfo.globalVolume), uint16(64));
273 		sample.nVibType = static_cast<VibratoType>(mptInfo.vibratoType.get());
274 		sample.nVibSweep = mptInfo.vibratoSweep;
275 		sample.nVibDepth = mptInfo.vibratoDepth;
276 		sample.nVibRate = mptInfo.vibratoRate;
277 
278 		if(xtraChunk.CanRead(MAX_SAMPLENAME))
279 		{
280 			// Name present (clipboard only)
281 			// FIXME: When modules can have individual encoding in OpenMPT or when
282 			// internal metadata gets converted to Unicode, we must adjust this to
283 			// also specify encoding.
284 			xtraChunk.ReadString<mpt::String::nullTerminated>(sampleName, MAX_SAMPLENAME);
285 			xtraChunk.ReadString<mpt::String::nullTerminated>(sample.filename, xtraChunk.BytesLeft());
286 		}
287 	}
288 }
289 
290 
291 // Apply WAV loop information to a mod sample.
ApplyToSample(SmpLength & start,SmpLength & end,SmpLength sampleLength,SampleFlags & flags,ChannelFlags enableFlag,ChannelFlags bidiFlag,bool mptLoopFix) const292 void WAVSampleLoop::ApplyToSample(SmpLength &start, SmpLength &end, SmpLength sampleLength, SampleFlags &flags, ChannelFlags enableFlag, ChannelFlags bidiFlag, bool mptLoopFix) const
293 {
294 	if(loopEnd == 0)
295 	{
296 		// Some WAV files seem to have loops going from 0 to 0... We should ignore those.
297 		return;
298 	}
299 	start = std::min(static_cast<SmpLength>(loopStart), sampleLength);
300 	end = Clamp(static_cast<SmpLength>(loopEnd), start, sampleLength);
301 	if(!mptLoopFix && end < sampleLength)
302 	{
303 		// RIFF loop end points are inclusive - old versions of MPT didn't consider this.
304 		end++;
305 	}
306 
307 	flags.set(enableFlag);
308 	if(loopType == loopBidi)
309 	{
310 		flags.set(bidiFlag);
311 	}
312 }
313 
314 
315 // Convert internal loop information into a WAV loop.
ConvertToWAV(SmpLength start,SmpLength end,bool bidi)316 void WAVSampleLoop::ConvertToWAV(SmpLength start, SmpLength end, bool bidi)
317 {
318 	identifier = 0;
319 	loopType = bidi ? loopBidi : loopForward;
320 	loopStart = mpt::saturate_cast<uint32>(start);
321 	// Loop ends are *inclusive* in the RIFF standard, while they're *exclusive* in OpenMPT.
322 	if(end > start)
323 	{
324 		loopEnd = mpt::saturate_cast<uint32>(end - 1);
325 	} else
326 	{
327 		loopEnd = loopStart;
328 	}
329 	fraction = 0;
330 	playCount = 0;
331 }
332 
333 
334 #ifndef MODPLUG_NO_FILESAVE
335 
336 ///////////////////////////////////////////////////////////
337 // WAV Writing
338 
339 
340 // Output to stream: Initialize with std::ostream*.
WAVWriter(mpt::IO::OFileBase & stream)341 WAVWriter::WAVWriter(mpt::IO::OFileBase &stream)
342 	: s(stream)
343 {
344 	// Skip file header for now
345 	Seek(sizeof(RIFFHeader));
346 }
347 
348 
~WAVWriter()349 WAVWriter::~WAVWriter()
350 {
351 	MPT_ASSERT(finalized);
352 }
353 
354 
355 // Finalize the file by closing the last open chunk and updating the file header. Returns total size of file.
Finalize()356 std::size_t WAVWriter::Finalize()
357 {
358 	FinalizeChunk();
359 
360 	RIFFHeader fileHeader;
361 	Clear(fileHeader);
362 	fileHeader.magic = RIFFHeader::idRIFF;
363 	fileHeader.length = static_cast<uint32>(totalSize - 8);
364 	fileHeader.type = RIFFHeader::idWAVE;
365 
366 	Seek(0);
367 	Write(fileHeader);
368 	finalized = true;
369 
370 	return totalSize;
371 }
372 
373 
374 // Write a new chunk header to the file.
StartChunk(RIFFChunk::ChunkIdentifiers id)375 void WAVWriter::StartChunk(RIFFChunk::ChunkIdentifiers id)
376 {
377 	FinalizeChunk();
378 
379 	chunkStartPos = position;
380 	chunkHeader.id = id;
381 	Skip(sizeof(chunkHeader));
382 }
383 
384 
385 // End current chunk by updating the chunk header and writing a padding byte if necessary.
FinalizeChunk()386 void WAVWriter::FinalizeChunk()
387 {
388 	if(chunkStartPos != 0)
389 	{
390 		const std::size_t chunkSize = position - (chunkStartPos + sizeof(RIFFChunk));
391 		chunkHeader.length = mpt::saturate_cast<uint32>(chunkSize);
392 
393 		std::size_t curPos = position;
394 		Seek(chunkStartPos);
395 		Write(chunkHeader);
396 
397 		Seek(curPos);
398 		if((chunkSize % 2u) != 0)
399 		{
400 			// Write padding
401 			uint8 padding = 0;
402 			Write(padding);
403 		}
404 
405 		chunkStartPos = 0;
406 	}
407 }
408 
409 
410 // Seek to a position in file.
Seek(std::size_t pos)411 void WAVWriter::Seek(std::size_t pos)
412 {
413 	position = pos;
414 	totalSize = std::max(totalSize, position);
415 	mpt::IO::SeekAbsolute(s, pos);
416 }
417 
418 
419 // Write some data to the file.
Write(mpt::const_byte_span data)420 void WAVWriter::Write(mpt::const_byte_span data)
421 {
422 	MPT_ASSERT(!finalized);
423 	auto success = mpt::IO::WriteRaw(s, data);
424 	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)
425 	if(!success)
426 	{
427 		return;
428 	}
429 	position += data.size();
430 	totalSize = std::max(totalSize, position);
431 }
432 
433 
WriteBeforeDirect()434 void WAVWriter::WriteBeforeDirect()
435 {
436 	MPT_ASSERT(!finalized);
437 }
438 
439 
WriteAfterDirect(bool success,std::size_t count)440 void WAVWriter::WriteAfterDirect(bool success, std::size_t count)
441 {
442 	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)
443 	if (!success)
444 	{
445 		return;
446 	}
447 	position += count;
448 	totalSize = std::max(totalSize, position);
449 }
450 
451 
452 // Write the WAV format to the file.
WriteFormat(uint32 sampleRate,uint16 bitDepth,uint16 numChannels,WAVFormatChunk::SampleFormats encoding)453 void WAVWriter::WriteFormat(uint32 sampleRate, uint16 bitDepth, uint16 numChannels, WAVFormatChunk::SampleFormats encoding)
454 {
455 	StartChunk(RIFFChunk::idfmt_);
456 	WAVFormatChunk wavFormat;
457 	Clear(wavFormat);
458 
459 	bool extensible = (numChannels > 2);
460 
461 	wavFormat.format = static_cast<uint16>(extensible ? WAVFormatChunk::fmtExtensible : encoding);
462 	wavFormat.numChannels = numChannels;
463 	wavFormat.sampleRate = sampleRate;
464 	wavFormat.blockAlign = (bitDepth * numChannels + 7) / 8;
465 	wavFormat.byteRate = wavFormat.sampleRate * wavFormat.blockAlign;
466 	wavFormat.bitsPerSample = bitDepth;
467 
468 	Write(wavFormat);
469 
470 	if(extensible)
471 	{
472 		WAVFormatChunkExtension extFormat;
473 		Clear(extFormat);
474 		extFormat.size = sizeof(WAVFormatChunkExtension) - sizeof(uint16);
475 		extFormat.validBitsPerSample = bitDepth;
476 		switch(numChannels)
477 		{
478 		case 1:
479 			extFormat.channelMask = 0x0004;	// FRONT_CENTER
480 			break;
481 		case 2:
482 			extFormat.channelMask = 0x0003;	// FRONT_LEFT | FRONT_RIGHT
483 			break;
484 		case 3:
485 			extFormat.channelMask = 0x0103;	// FRONT_LEFT | FRONT_RIGHT | BACK_CENTER
486 			break;
487 		case 4:
488 			extFormat.channelMask = 0x0033;	// FRONT_LEFT | FRONT_RIGHT | BACK_LEFT | BACK_RIGHT
489 			break;
490 		default:
491 			extFormat.channelMask = 0;
492 			break;
493 		}
494 		extFormat.subFormat = mpt::UUID(static_cast<uint16>(encoding), 0x0000, 0x0010, 0x800000AA00389B71ull);
495 		Write(extFormat);
496 	}
497 }
498 
499 
500 // Write text tags to the file.
WriteMetatags(const FileTags & tags)501 void WAVWriter::WriteMetatags(const FileTags &tags)
502 {
503 	StartChunk(RIFFChunk::idCSET);
504 	Write(mpt::as_le(uint16(65001)));  // code page    (UTF-8)
505 	Write(mpt::as_le(uint16(0)));      // country code (unset)
506 	Write(mpt::as_le(uint16(0)));      // language     (unset)
507 	Write(mpt::as_le(uint16(0)));      // dialect      (unset)
508 
509 	StartChunk(RIFFChunk::idLIST);
510 	const char info[] = { 'I', 'N', 'F', 'O' };
511 	Write(info);
512 
513 	WriteTag(RIFFChunk::idINAM, tags.title);
514 	WriteTag(RIFFChunk::idIART, tags.artist);
515 	WriteTag(RIFFChunk::idIPRD, tags.album);
516 	WriteTag(RIFFChunk::idICRD, tags.year);
517 	WriteTag(RIFFChunk::idICMT, tags.comments);
518 	WriteTag(RIFFChunk::idIGNR, tags.genre);
519 	WriteTag(RIFFChunk::idTURL, tags.url);
520 	WriteTag(RIFFChunk::idISFT, tags.encoder);
521 	//WriteTag(RIFFChunk::      , tags.bpm);
522 	WriteTag(RIFFChunk::idTRCK, tags.trackno);
523 }
524 
525 
526 // Write a single tag into a open idLIST chunk
WriteTag(RIFFChunk::ChunkIdentifiers id,const mpt::ustring & utext)527 void WAVWriter::WriteTag(RIFFChunk::ChunkIdentifiers id, const mpt::ustring &utext)
528 {
529 	std::string text = mpt::ToCharset(mpt::Charset::UTF8, utext);
530 	text = text.substr(0, uint32_max - 1u);
531 	if(!text.empty())
532 	{
533 		const uint32 length = mpt::saturate_cast<uint32>(text.length() + 1);
534 
535 		RIFFChunk chunk;
536 		Clear(chunk);
537 		chunk.id = static_cast<uint32>(id);
538 		chunk.length = length;
539 		Write(chunk);
540 		Write(mpt::byte_cast<mpt::const_byte_span>(mpt::span(text.c_str(), length)));
541 
542 		if((length % 2u) != 0)
543 		{
544 			uint8 padding = 0;
545 			Write(padding);
546 		}
547 	}
548 }
549 
550 
551 // Write a sample loop information chunk to the file.
WriteLoopInformation(const ModSample & sample)552 void WAVWriter::WriteLoopInformation(const ModSample &sample)
553 {
554 	if(!sample.uFlags[CHN_LOOP | CHN_SUSTAINLOOP] && !ModCommand::IsNote(sample.rootNote))
555 	{
556 		return;
557 	}
558 
559 	StartChunk(RIFFChunk::idsmpl);
560 	WAVSampleInfoChunk info;
561 
562 	uint32 sampleRate = sample.nC5Speed;
563 	if(sampleRate == 0)
564 	{
565 		sampleRate = ModSample::TransposeToFrequency(sample.RelativeTone, sample.nFineTune);
566 	}
567 
568 	info.ConvertToWAV(sampleRate, sample.rootNote);
569 
570 	// Set up loops
571 	WAVSampleLoop loops[2];
572 	Clear(loops);
573 	if(sample.uFlags[CHN_SUSTAINLOOP])
574 	{
575 		loops[info.numLoops++].ConvertToWAV(sample.nSustainStart, sample.nSustainEnd, sample.uFlags[CHN_PINGPONGSUSTAIN]);
576 	}
577 	if(sample.uFlags[CHN_LOOP])
578 	{
579 		loops[info.numLoops++].ConvertToWAV(sample.nLoopStart, sample.nLoopEnd, sample.uFlags[CHN_PINGPONGLOOP]);
580 	} else if(sample.uFlags[CHN_SUSTAINLOOP])
581 	{
582 		// Since there are no "loop types" to distinguish between sustain and normal loops, OpenMPT assumes
583 		// that the first loop is a sustain loop if there are two loops. If we only want a sustain loop,
584 		// we will have to write a second bogus loop.
585 		loops[info.numLoops++].ConvertToWAV(0, 0, false);
586 	}
587 
588 	Write(info);
589 	for(uint32 i = 0; i < info.numLoops; i++)
590 	{
591 		Write(loops[i]);
592 	}
593 }
594 
595 
596 // Write a sample's cue points to the file.
WriteCueInformation(const ModSample & sample)597 void WAVWriter::WriteCueInformation(const ModSample &sample)
598 {
599 	uint32 numMarkers = 0;
600 	for(const auto cue : sample.cues)
601 	{
602 		if(cue < sample.nLength)
603 			numMarkers++;
604 	}
605 
606 	StartChunk(RIFFChunk::idcue_);
607 	Write(mpt::as_le(numMarkers));
608 	uint32 i = 0;
609 	for(const auto cue : sample.cues)
610 	{
611 		if(cue < sample.nLength)
612 		{
613 			WAVCuePoint cuePoint;
614 			cuePoint.ConvertToWAV(i++, cue);
615 			Write(cuePoint);
616 		}
617 	}
618 }
619 
620 
621 // Write MPT's sample information chunk to the file.
WriteExtraInformation(const ModSample & sample,MODTYPE modType,const char * sampleName)622 void WAVWriter::WriteExtraInformation(const ModSample &sample, MODTYPE modType, const char *sampleName)
623 {
624 	StartChunk(RIFFChunk::idxtra);
625 	WAVExtraChunk mptInfo;
626 
627 	mptInfo.ConvertToWAV(sample, modType);
628 	Write(mptInfo);
629 
630 	if(sampleName != nullptr)
631 	{
632 		// Write sample name (clipboard only)
633 
634 		// FIXME: When modules can have individual encoding in OpenMPT or when
635 		// internal metadata gets converted to Unicode, we must adjust this to
636 		// also specify encoding.
637 
638 		char name[MAX_SAMPLENAME];
639 		mpt::String::WriteBuf(mpt::String::nullTerminated, name) = sampleName;
640 		Write(name);
641 
642 		char filename[MAX_SAMPLEFILENAME];
643 		mpt::String::WriteBuf(mpt::String::nullTerminated, filename) = sample.filename;
644 		Write(filename);
645 	}
646 }
647 
648 #endif // MODPLUG_NO_FILESAVE
649 
650 
651 OPENMPT_NAMESPACE_END
652