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