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