1 /*
2  * Load_669.cpp
3  * ------------
4  * Purpose: 669 Composer / UNIS 669 module loader
5  * Notes  : <opinion humble="false">This is better than Schism's 669 loader</opinion> :)
6  *          (some of this code is "heavily inspired" by Storlek's code from Schism Tracker, and improvements have been made where necessary.)
7  * Authors: Olivier Lapicque
8  *          OpenMPT Devs
9  * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
10  */
11 
12 
13 #include "stdafx.h"
14 #include "Loaders.h"
15 
16 OPENMPT_NAMESPACE_BEGIN
17 
18 struct _669FileHeader
19 {
20 	char  magic[2];			// 'if' (0x6669, ha ha) or 'JN'
21 	char  songMessage[108];	// Song Message
22 	uint8 samples;			// number of samples (1-64)
23 	uint8 patterns;			// number of patterns (1-128)
24 	uint8 restartPos;
25 	uint8 orders[128];
26 	uint8 tempoList[128];
27 	uint8 breaks[128];
28 };
29 
30 MPT_BINARY_STRUCT(_669FileHeader, 497)
31 
32 
33 struct _669Sample
34 {
35 	char     filename[13];
36 	uint32le length;
37 	uint32le loopStart;
38 	uint32le loopEnd;
39 
40 	// Convert a 669 sample header to OpenMPT's internal sample header.
ConvertToMPT_669Sample41 	void ConvertToMPT(ModSample &mptSmp) const
42 	{
43 		mptSmp.Initialize();
44 
45 		mptSmp.nC5Speed = 8363;
46 		mptSmp.nLength = length;
47 		mptSmp.nLoopStart = loopStart;
48 		mptSmp.nLoopEnd = loopEnd;
49 
50 		if(mptSmp.nLoopEnd > mptSmp.nLength && mptSmp.nLoopStart == 0)
51 		{
52 			mptSmp.nLoopEnd = 0;
53 		}
54 		if(mptSmp.nLoopEnd != 0)
55 		{
56 			mptSmp.uFlags = CHN_LOOP;
57 			mptSmp.SanitizeLoops();
58 		}
59 	}
60 };
61 
62 MPT_BINARY_STRUCT(_669Sample, 25)
63 
64 
ValidateHeader(const _669FileHeader & fileHeader)65 static bool ValidateHeader(const _669FileHeader &fileHeader)
66 {
67 	if((std::memcmp(fileHeader.magic, "if", 2) && std::memcmp(fileHeader.magic, "JN", 2))
68 		|| fileHeader.samples > 64
69 		|| fileHeader.restartPos >= 128
70 		|| fileHeader.patterns > 128)
71 	{
72 		return false;
73 	}
74 	for(std::size_t i = 0; i < std::size(fileHeader.breaks); i++)
75 	{
76 		if(fileHeader.orders[i] >= 128 && fileHeader.orders[i] < 0xFE)
77 			return false;
78 		if(fileHeader.orders[i] < 128 && fileHeader.tempoList[i] == 0)
79 			return false;
80 		if(fileHeader.tempoList[i] > 15)
81 			return false;
82 		if(fileHeader.breaks[i] >= 64)
83 			return false;
84 	}
85 	return true;
86 }
87 
88 
GetHeaderMinimumAdditionalSize(const _669FileHeader & fileHeader)89 static uint64 GetHeaderMinimumAdditionalSize(const _669FileHeader &fileHeader)
90 {
91 	return fileHeader.samples * sizeof(_669Sample) + fileHeader.patterns * 1536u;
92 }
93 
94 
ProbeFileHeader669(MemoryFileReader file,const uint64 * pfilesize)95 CSoundFile::ProbeResult CSoundFile::ProbeFileHeader669(MemoryFileReader file, const uint64 *pfilesize)
96 {
97 	_669FileHeader fileHeader;
98 	if(!file.ReadStruct(fileHeader))
99 	{
100 		return ProbeWantMoreData;
101 	}
102 	if(!ValidateHeader(fileHeader))
103 	{
104 		return ProbeFailure;
105 	}
106 	return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
107 }
108 
109 
Read669(FileReader & file,ModLoadingFlags loadFlags)110 bool CSoundFile::Read669(FileReader &file, ModLoadingFlags loadFlags)
111 {
112 	_669FileHeader fileHeader;
113 
114 	file.Rewind();
115 	if(!file.ReadStruct(fileHeader))
116 	{
117 		return false;
118 	}
119 	if(!ValidateHeader(fileHeader))
120 	{
121 		return false;
122 	}
123 	if(loadFlags == onlyVerifyHeader)
124 	{
125 		return true;
126 	}
127 
128 	if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader))))
129 	{
130 		return false;
131 	}
132 
133 	InitializeGlobals(MOD_TYPE_669);
134 	m_nMinPeriod = 28 << 2;
135 	m_nMaxPeriod = 1712 << 3;
136 	m_nDefaultTempo.Set(78);
137 	m_nDefaultSpeed = 4;
138 	m_nChannels = 8;
139 	m_playBehaviour.set(kPeriodsAreHertz);
140 #ifdef MODPLUG_TRACKER
141 	// 669 uses frequencies rather than periods, so linear slides mode will sound better in the higher octaves.
142 	//m_SongFlags.set(SONG_LINEARSLIDES);
143 #endif // MODPLUG_TRACKER
144 
145 	m_modFormat.formatName = U_("Composer 669");
146 	m_modFormat.type = U_("669");
147 	m_modFormat.madeWithTracker = !memcmp(fileHeader.magic, "if", 2) ? UL_("Composer 669") : UL_("UNIS 669");
148 	m_modFormat.charset = mpt::Charset::CP437;
149 
150 	m_nSamples = fileHeader.samples;
151 	for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++)
152 	{
153 		_669Sample sampleHeader;
154 		file.ReadStruct(sampleHeader);
155 		// Since 669 files have very unfortunate magic bytes ("if") and can
156 		// hardly be validated, reject any file with far too big samples.
157 		if(sampleHeader.length >= 0x4000000)
158 			return false;
159 		sampleHeader.ConvertToMPT(Samples[smp]);
160 		m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename);
161 	}
162 
163 	// Copy first song message line into song title
164 	m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songMessage, 36);
165 	// Song Message
166 	m_songMessage.ReadFixedLineLength(mpt::byte_cast<const std::byte*>(fileHeader.songMessage), 108, 36, 0);
167 
168 	// Reading Orders
169 	ReadOrderFromArray(Order(), fileHeader.orders, std::size(fileHeader.orders), 0xFF, 0xFE);
170 	if(Order()[fileHeader.restartPos] < fileHeader.patterns)
171 		Order().SetRestartPos(fileHeader.restartPos);
172 
173 	// Set up panning
174 	for(CHANNELINDEX chn = 0; chn < 8; chn++)
175 	{
176 		ChnSettings[chn].Reset();
177 		ChnSettings[chn].nPan = (chn & 1) ? 0xD0 : 0x30;
178 	}
179 
180 	// Reading Patterns
181 	Patterns.ResizeArray(fileHeader.patterns);
182 	for(PATTERNINDEX pat = 0; pat < fileHeader.patterns; pat++)
183 	{
184 		if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64))
185 		{
186 			file.Skip(64 * 8 * 3);
187 			continue;
188 		}
189 
190 		static constexpr ModCommand::COMMAND effTrans[] =
191 		{
192 			CMD_PORTAMENTOUP,	// Slide up (param * 80) Hz on every tick
193 			CMD_PORTAMENTODOWN,	// Slide down (param * 80) Hz on every tick
194 			CMD_TONEPORTAMENTO,	// Slide to note by (param * 40) Hz on every tick
195 			CMD_S3MCMDEX,		// Add (param * 80) Hz to sample frequency
196 			CMD_VIBRATO,		// Add (param * 669) Hz on every other tick
197 			CMD_SPEED,			// Set ticks per row
198 			CMD_PANNINGSLIDE,	// Extended UNIS 669 effect
199 			CMD_RETRIG,			// Extended UNIS 669 effect
200 		};
201 
202 		uint8 effect[8] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
203 		for(ROWINDEX row = 0; row < 64; row++)
204 		{
205 			PatternRow m = Patterns[pat].GetRow(row);
206 
207 			for(CHANNELINDEX chn = 0; chn < 8; chn++, m++)
208 			{
209 				const auto [noteInstr, instrVol, effParam] = file.ReadArray<uint8, 3>();
210 
211 				uint8 note = noteInstr >> 2;
212 				uint8 instr = ((noteInstr & 0x03) << 4) | (instrVol >> 4);
213 				uint8 vol = instrVol & 0x0F;
214 				if(noteInstr < 0xFE)
215 				{
216 					m->note = note + 36 + NOTE_MIN;
217 					m->instr = instr + 1;
218 					effect[chn] = 0xFF;
219 				}
220 				if(noteInstr <= 0xFE)
221 				{
222 					m->volcmd = VOLCMD_VOLUME;
223 					m->vol = ((vol * 64 + 8) / 15);
224 				}
225 
226 				if(effParam != 0xFF)
227 				{
228 					effect[chn] = effParam;
229 				}
230 				if((effParam & 0x0F) == 0 && effParam != 0x30)
231 				{
232 					// A param value of 0 resets the effect.
233 					effect[chn] = 0xFF;
234 				}
235 				if(effect[chn] == 0xFF)
236 				{
237 					continue;
238 				}
239 
240 				m->param = effect[chn] & 0x0F;
241 
242 				// Weird stuff happening in corehop.669 with effects > 8... they seem to do the same thing as if the high bit wasn't set, but the sample also behaves strangely.
243 				uint8 command = effect[chn] >> 4;
244 				if(command < static_cast<uint8>(std::size(effTrans)))
245 				{
246 					m->command = effTrans[command];
247 				} else
248 				{
249 					m->command = CMD_NONE;
250 					continue;
251 				}
252 
253 				// Fix some commands
254 				switch(command)
255 				{
256 				case 3:
257 					// D - frequency adjust
258 #ifdef MODPLUG_TRACKER
259 					// Since we convert to S3M, the finetune command will not quite do what we intend to do (it can adjust the frequency upwards and downwards), so try to approximate it using a fine slide.
260 					m->command = CMD_PORTAMENTOUP;
261 					m->param |= 0xF0;
262 #else
263 					m->param |= 0x20;
264 #endif
265 					effect[chn] = 0xFF;
266 					break;
267 
268 				case 4:
269 					// E - frequency vibrato - almost like an arpeggio, but does not arpeggiate by a given note but by a frequency amount.
270 #ifdef MODPLUG_TRACKER
271 					m->command = CMD_ARPEGGIO;
272 #endif
273 					m->param |= (m->param << 4);
274 					break;
275 
276 				case 5:
277 					// F - set tempo
278 					// TODO: param 0 is a "super fast tempo" in Unis 669 mode (?)
279 					effect[chn] = 0xFF;
280 					break;
281 
282 				case 6:
283 					// G - subcommands (extended)
284 					switch(m->param)
285 					{
286 					case 0:
287 						// balance fine slide left
288 						m->param = 0x4F;
289 						break;
290 					case 1:
291 						// balance fine slide right
292 						m->param = 0xF4;
293 						break;
294 					default:
295 						m->command = CMD_NONE;
296 					}
297 					break;
298 				}
299 			}
300 		}
301 
302 		// Write pattern break
303 		if(fileHeader.breaks[pat] < 63)
304 		{
305 			Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(fileHeader.breaks[pat]).RetryNextRow());
306 		}
307 		// And of course the speed...
308 		Patterns[pat].WriteEffect(EffectWriter(CMD_SPEED, fileHeader.tempoList[pat]).RetryNextRow());
309 	}
310 
311 	if(loadFlags & loadSampleData)
312 	{
313 		// Reading Samples
314 		const SampleIO sampleIO(
315 			SampleIO::_8bit,
316 			SampleIO::mono,
317 			SampleIO::littleEndian,
318 			SampleIO::unsignedPCM);
319 
320 		for(SAMPLEINDEX n = 1; n <= m_nSamples; n++)
321 		{
322 			sampleIO.ReadSample(Samples[n], file);
323 		}
324 	}
325 
326 	return true;
327 }
328 
329 
330 OPENMPT_NAMESPACE_END
331