1 /*
2  * Load_dsm.cpp
3  * ------------
4  * Purpose: Digisound Interface Kit (DSIK) Internal Format (DSM v2 / RIFF) module loader
5  * Notes  : 1. There is also another fundamentally different DSIK DSM v1 module format, not handled here.
6  *          MilkyTracker can load it, but the only files of this format seen in the wild are also
7  *          available in their original format, so I did not bother implementing it so far.
8  *
9  *          2. Using both PLAY.EXE v1.02 and v2.00, commands not supported in MOD do not seem to do
10  *          anything at all.
11  *          In particular commands 0x11-0x13 handled below are ignored, and no files have been spotted
12  *          in the wild using any commands > 0x0F at all.
13  *          S3M-style retrigger does not seem to exist - it is translated to volume slides by CONV.EXE,
14  *          and J00 in S3M files is not converted either. S3M pattern loops (SBx) are not converted
15  *          properly by CONV.EXE and completely ignored by PLAY.EXE.
16  *          Command 8 (set panning) uses 00-80 for regular panning and A4 for surround, probably
17  *          making DSIK one of the first applications to use this particular encoding scheme still
18  *          used in "extended" S3Ms today.
19  * Authors: OpenMPT Devs
20  * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
21  */
22 
23 
24 #include "stdafx.h"
25 #include "Loaders.h"
26 
27 OPENMPT_NAMESPACE_BEGIN
28 
29 struct DSMChunk
30 {
31 	char     magic[4];
32 	uint32le size;
33 };
34 
35 MPT_BINARY_STRUCT(DSMChunk, 8)
36 
37 
38 struct DSMSongHeader
39 {
40 	char     songName[28];
41 	uint16le fileVersion;
42 	uint16le flags;
43 	uint16le orderPos;
44 	uint16le restartPos;
45 	uint16le numOrders;
46 	uint16le numSamples;
47 	uint16le numPatterns;
48 	uint16le numChannels;
49 	uint8le  globalVol;
50 	uint8le  mastervol;
51 	uint8le  speed;
52 	uint8le  bpm;
53 	uint8le  panPos[16];
54 	uint8le  orders[128];
55 };
56 
57 MPT_BINARY_STRUCT(DSMSongHeader, 192)
58 
59 
60 struct DSMSampleHeader
61 {
62 	char     filename[13];
63 	uint16le flags;
64 	uint8le  volume;
65 	uint32le length;
66 	uint32le loopStart;
67 	uint32le loopEnd;
68 	uint32le dataPtr;	// Interal sample pointer during playback in DSIK
69 	uint32le sampleRate;
70 	char     sampleName[28];
71 
72 	// Convert a DSM sample header to OpenMPT's internal sample header.
ConvertToMPTDSMSampleHeader73 	void ConvertToMPT(ModSample &mptSmp) const
74 	{
75 		mptSmp.Initialize();
76 		mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename);
77 
78 		mptSmp.nC5Speed = sampleRate;
79 		mptSmp.uFlags.set(CHN_LOOP, (flags & 1) != 0);
80 		mptSmp.nLength = length;
81 		mptSmp.nLoopStart = loopStart;
82 		mptSmp.nLoopEnd = loopEnd;
83 		mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4;
84 	}
85 
86 	// Retrieve the internal sample format flags for this sample.
GetSampleFormatDSMSampleHeader87 	SampleIO GetSampleFormat() const
88 	{
89 		SampleIO sampleIO(
90 			SampleIO::_8bit,
91 			SampleIO::mono,
92 			SampleIO::littleEndian,
93 			SampleIO::unsignedPCM);
94 		if(flags & 0x40)
95 			sampleIO |= SampleIO::deltaPCM;	// fairlight.dsm by Comrade J
96 		else if(flags & 0x02)
97 			sampleIO |= SampleIO::signedPCM;
98 		if(flags & 0x04)
99 			sampleIO |= SampleIO::_16bit;
100 		return sampleIO;
101 	}
102 };
103 
104 MPT_BINARY_STRUCT(DSMSampleHeader, 64)
105 
106 
107 struct DSMHeader
108 {
109 	char fileMagic0[4];
110 	char fileMagic1[4];
111 	char fileMagic2[4];
112 };
113 
114 MPT_BINARY_STRUCT(DSMHeader, 12)
115 
116 
ValidateHeader(const DSMHeader & fileHeader)117 static bool ValidateHeader(const DSMHeader &fileHeader)
118 {
119 	if(!std::memcmp(fileHeader.fileMagic0, "RIFF", 4)
120 		&& !std::memcmp(fileHeader.fileMagic2, "DSMF", 4))
121 	{
122 		// "Normal" DSM files with RIFF header
123 		// <RIFF> <file size> <DSMF>
124 		return true;
125 	} else if(!std::memcmp(fileHeader.fileMagic0, "DSMF", 4))
126 	{
127 		// DSM files with alternative header
128 		// <DSMF> <4 bytes, usually 4x NUL or RIFF> <file size> <4 bytes, usually DSMF but not always>
129 		return true;
130 	} else
131 	{
132 		return false;
133 	}
134 }
135 
136 
ProbeFileHeaderDSM(MemoryFileReader file,const uint64 * pfilesize)137 CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDSM(MemoryFileReader file, const uint64 *pfilesize)
138 {
139 	DSMHeader fileHeader;
140 	if(!file.ReadStruct(fileHeader))
141 	{
142 		return ProbeWantMoreData;
143 	}
144 	if(!ValidateHeader(fileHeader))
145 	{
146 		return ProbeFailure;
147 	}
148 	if(std::memcmp(fileHeader.fileMagic0, "DSMF", 4) == 0)
149 	{
150 		if(!file.Skip(4))
151 		{
152 			return ProbeWantMoreData;
153 		}
154 	}
155 	DSMChunk chunkHeader;
156 	if(!file.ReadStruct(chunkHeader))
157 	{
158 		return ProbeWantMoreData;
159 	}
160 	if(std::memcmp(chunkHeader.magic, "SONG", 4))
161 	{
162 		return ProbeFailure;
163 	}
164 	MPT_UNREFERENCED_PARAMETER(pfilesize);
165 	return ProbeSuccess;
166 }
167 
168 
ReadDSM(FileReader & file,ModLoadingFlags loadFlags)169 bool CSoundFile::ReadDSM(FileReader &file, ModLoadingFlags loadFlags)
170 {
171 	file.Rewind();
172 
173 	DSMHeader fileHeader;
174 	if(!file.ReadStruct(fileHeader))
175 	{
176 		return false;
177 	}
178 	if(!ValidateHeader(fileHeader))
179 	{
180 		return false;
181 	}
182 	if(std::memcmp(fileHeader.fileMagic0, "DSMF", 4) == 0)
183 	{
184 		file.Skip(4);
185 	}
186 	DSMChunk chunkHeader;
187 	if(!file.ReadStruct(chunkHeader))
188 	{
189 		return false;
190 	}
191 	// Technically, the song chunk could be anywhere in the file, but we're going to simplify
192 	// things by not using a chunk header here and just expect it to be right at the beginning.
193 	if(std::memcmp(chunkHeader.magic, "SONG", 4))
194 	{
195 		return false;
196 	}
197 	if(loadFlags == onlyVerifyHeader)
198 	{
199 		return true;
200 	}
201 
202 	DSMSongHeader songHeader;
203 	file.ReadStructPartial(songHeader, chunkHeader.size);
204 	if(songHeader.numOrders > 128 || songHeader.numChannels > 16 || songHeader.numPatterns > 256 || songHeader.restartPos > 128)
205 	{
206 		return false;
207 	}
208 
209 	InitializeGlobals(MOD_TYPE_DSM);
210 
211 	m_modFormat.formatName = U_("DSIK Format");
212 	m_modFormat.type = U_("dsm");
213 	m_modFormat.charset = mpt::Charset::CP437;
214 
215 	m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, songHeader.songName);
216 	m_nChannels = std::max(songHeader.numChannels.get(), uint16(1));
217 	m_nDefaultSpeed = songHeader.speed;
218 	m_nDefaultTempo.Set(songHeader.bpm);
219 	m_nDefaultGlobalVolume = std::min(songHeader.globalVol.get(), uint8(64)) * 4u;
220 	if(!m_nDefaultGlobalVolume) m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;
221 	if(songHeader.mastervol == 0x80)
222 	{
223 		m_nSamplePreAmp = std::min(256u / m_nChannels, 128u);
224 	} else
225 	{
226 		m_nSamplePreAmp = songHeader.mastervol & 0x7F;
227 	}
228 
229 	// Read channel panning
230 	for(CHANNELINDEX chn = 0; chn < 16; chn++)
231 	{
232 		ChnSettings[chn].Reset();
233 		if(songHeader.panPos[chn] <= 0x80)
234 		{
235 			ChnSettings[chn].nPan = songHeader.panPos[chn] * 2;
236 		}
237 	}
238 
239 	ReadOrderFromArray(Order(), songHeader.orders, songHeader.numOrders, 0xFF, 0xFE);
240 	if(songHeader.restartPos < songHeader.numOrders)
241 		Order().SetRestartPos(songHeader.restartPos);
242 
243 	// Read pattern and sample chunks
244 	PATTERNINDEX patNum = 0;
245 	while(file.ReadStruct(chunkHeader))
246 	{
247 		FileReader chunk = file.ReadChunk(chunkHeader.size);
248 
249 		if(!memcmp(chunkHeader.magic, "PATT", 4) && (loadFlags & loadPatternData))
250 		{
251 			// Read pattern
252 			if(!Patterns.Insert(patNum, 64))
253 			{
254 				continue;
255 			}
256 			chunk.Skip(2);
257 
258 			ModCommand dummy = ModCommand::Empty();
259 			ROWINDEX row = 0;
260 			while(chunk.CanRead(1) && row < 64)
261 			{
262 				uint8 flag = chunk.ReadUint8();
263 				if(!flag)
264 				{
265 					row++;
266 					continue;
267 				}
268 
269 				CHANNELINDEX chn = (flag & 0x0F);
270 				ModCommand &m = (chn < GetNumChannels() ? *Patterns[patNum].GetpModCommand(row, chn) : dummy);
271 
272 				if(flag & 0x80)
273 				{
274 					uint8 note = chunk.ReadUint8();
275 					if(note)
276 					{
277 						if(note <= 12 * 9) note += 11 + NOTE_MIN;
278 						m.note = note;
279 					}
280 				}
281 				if(flag & 0x40)
282 				{
283 					m.instr = chunk.ReadUint8();
284 				}
285 				if (flag & 0x20)
286 				{
287 					m.volcmd = VOLCMD_VOLUME;
288 					m.vol = std::min(chunk.ReadUint8(), uint8(64));
289 				}
290 				if(flag & 0x10)
291 				{
292 					auto [command, param] = chunk.ReadArray<uint8, 2>();
293 					switch(command)
294 					{
295 						// Portamentos
296 					case 0x11:
297 					case 0x12:
298 						command &= 0x0F;
299 						break;
300 						// 3D Sound (?)
301 					case 0x13:
302 						command = 'X' - 55;
303 						param = 0x91;
304 						break;
305 					default:
306 						// Volume + Offset (?)
307 						if(command > 0x10)
308 							command = ((command & 0xF0) == 0x20) ? 0x09 : 0xFF;
309 					}
310 					m.command = command;
311 					m.param = param;
312 					ConvertModCommand(m);
313 				}
314 			}
315 			patNum++;
316 		} else if(!memcmp(chunkHeader.magic, "INST", 4) && CanAddMoreSamples())
317 		{
318 			// Read sample
319 			m_nSamples++;
320 			ModSample &sample = Samples[m_nSamples];
321 
322 			DSMSampleHeader sampleHeader;
323 			chunk.ReadStruct(sampleHeader);
324 			sampleHeader.ConvertToMPT(sample);
325 
326 			m_szNames[m_nSamples] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.sampleName);
327 
328 			if(loadFlags & loadSampleData)
329 			{
330 				sampleHeader.GetSampleFormat().ReadSample(sample, chunk);
331 			}
332 		}
333 	}
334 
335 	return true;
336 }
337 
338 
339 OPENMPT_NAMESPACE_END
340