1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
|
/*
* load_j2b.cpp
* ------------
* Purpose: RIFF AM and RIFF AMFF (Galaxy Sound System) module loader
* Notes : J2B is a compressed variant of RIFF AM and RIFF AMFF files used in Jazz Jackrabbit 2.
* It seems like no other game used the AM(FF) format.
* RIFF AM is the newer version of the format, generally following the RIFF "standard" closely.
* Authors: Johannes Schultz (OpenMPT port, reverse engineering + loader implementation of the instrument format)
* kode54 (foo_dumb - this is almost a complete port of his code, thanks)
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Loaders.h"
#include "mpt/io/base.hpp"
#if defined(MPT_WITH_ZLIB)
#include <zlib.h>
#elif defined(MPT_WITH_MINIZ)
#include <miniz/miniz.h>
#endif
#ifdef MPT_ALL_LOGGING
#define J2B_LOG
#endif
OPENMPT_NAMESPACE_BEGIN
// First off, a nice vibrato translation LUT.
static constexpr VibratoType j2bAutoVibratoTrans[] =
{
VIB_SINE, VIB_SQUARE, VIB_RAMP_UP, VIB_RAMP_DOWN, VIB_RANDOM,
};
// header for compressed j2b files
struct J2BFileHeader
{
// Magic Bytes
// 32-Bit J2B header identifiers
enum : uint32 {
magicDEADBEAF = 0xAFBEADDEu,
magicDEADBABE = 0xBEBAADDEu
};
char signature[4]; // MUSE
uint32le deadbeaf; // 0xDEADBEAF (AM) or 0xDEADBABE (AMFF)
uint32le fileLength; // complete filesize
uint32le crc32; // checksum of the compressed data block
uint32le packedLength; // length of the compressed data block
uint32le unpackedLength; // length of the decompressed module
};
MPT_BINARY_STRUCT(J2BFileHeader, 24)
// AM(FF) stuff
struct AMFFRiffChunk
{
// 32-Bit chunk identifiers
enum ChunkIdentifiers
{
idRIFF = MagicLE("RIFF"),
idAMFF = MagicLE("AMFF"),
idAM__ = MagicLE("AM "),
idMAIN = MagicLE("MAIN"),
idINIT = MagicLE("INIT"),
idORDR = MagicLE("ORDR"),
idPATT = MagicLE("PATT"),
idINST = MagicLE("INST"),
idSAMP = MagicLE("SAMP"),
idAI__ = MagicLE("AI "),
idAS__ = MagicLE("AS "),
};
uint32le id; // See ChunkIdentifiers
uint32le length; // Chunk size without header
size_t GetLength() const
{
return length;
}
ChunkIdentifiers GetID() const
{
return static_cast<ChunkIdentifiers>(id.get());
}
};
MPT_BINARY_STRUCT(AMFFRiffChunk, 8)
// This header is used for both AM's "INIT" as well as AMFF's "MAIN" chunk
struct AMFFMainChunk
{
// Main Chunk flags
enum MainFlags
{
amigaSlides = 0x01,
};
char songname[64];
uint8le flags;
uint8le channels;
uint8le speed;
uint8le tempo;
uint16le minPeriod; // 16x Amiga periods, but we should ignore them - otherwise some high notes in Medivo.j2b won't sound correct.
uint16le maxPeriod; // Ditto
uint8le globalvolume;
};
MPT_BINARY_STRUCT(AMFFMainChunk, 73)
// AMFF instrument envelope (old format)
struct AMFFEnvelope
{
// Envelope flags (also used for RIFF AM)
enum EnvelopeFlags
{
envEnabled = 0x01,
envSustain = 0x02,
envLoop = 0x04,
};
struct EnvPoint
{
uint16le tick;
uint8le value; // 0...64
};
uint8le envFlags; // high nibble = pan env flags, low nibble = vol env flags (both nibbles work the same way)
uint8le envNumPoints; // high nibble = pan env length, low nibble = vol env length
uint8le envSustainPoints; // you guessed it... high nibble = pan env sustain point, low nibble = vol env sustain point
uint8le envLoopStarts; // I guess you know the pattern now.
uint8le envLoopEnds; // same here.
EnvPoint volEnv[10];
EnvPoint panEnv[10];
// Convert weird envelope data to OpenMPT's internal format.
void ConvertEnvelope(uint8 flags, uint8 numPoints, uint8 sustainPoint, uint8 loopStart, uint8 loopEnd, const EnvPoint (&points)[10], InstrumentEnvelope &mptEnv) const
{
// The buggy mod2j2b converter will actually NOT limit this to 10 points if the envelope is longer.
mptEnv.resize(std::min(numPoints, static_cast<uint8>(10)));
mptEnv.nSustainStart = mptEnv.nSustainEnd = sustainPoint;
mptEnv.nLoopStart = loopStart;
mptEnv.nLoopEnd = loopEnd;
for(uint32 i = 0; i < mptEnv.size(); i++)
{
mptEnv[i].tick = points[i].tick >> 4;
if(i == 0)
mptEnv[0].tick = 0;
else if(mptEnv[i].tick < mptEnv[i - 1].tick)
mptEnv[i].tick = mptEnv[i - 1].tick + 1;
mptEnv[i].value = Clamp<uint8, uint8>(points[i].value, 0, 64);
}
mptEnv.dwFlags.set(ENV_ENABLED, (flags & AMFFEnvelope::envEnabled) != 0);
mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & AMFFEnvelope::envSustain) && mptEnv.nSustainStart <= mptEnv.size());
mptEnv.dwFlags.set(ENV_LOOP, (flags & AMFFEnvelope::envLoop) && mptEnv.nLoopStart <= mptEnv.nLoopEnd && mptEnv.nLoopStart <= mptEnv.size());
}
void ConvertToMPT(ModInstrument &mptIns) const
{
// interleaved envelope data... meh. gotta split it up here and decode it separately.
// note: mod2j2b is BUGGY and always writes ($original_num_points & 0x0F) in the header,
// but just has room for 10 envelope points. That means that long (>= 16 points)
// envelopes are cut off, and envelopes have to be trimmed to 10 points, even if
// the header claims that they are longer.
// For XM files the number of points also appears to be off by one,
// but luckily there are no official J2Bs using envelopes anyway.
ConvertEnvelope(envFlags & 0x0F, envNumPoints & 0x0F, envSustainPoints & 0x0F, envLoopStarts & 0x0F, envLoopEnds & 0x0F, volEnv, mptIns.VolEnv);
ConvertEnvelope(envFlags >> 4, envNumPoints >> 4, envSustainPoints >> 4, envLoopStarts >> 4, envLoopEnds >> 4, panEnv, mptIns.PanEnv);
}
};
MPT_BINARY_STRUCT(AMFFEnvelope::EnvPoint, 3)
MPT_BINARY_STRUCT(AMFFEnvelope, 65)
// AMFF instrument header (old format)
struct AMFFInstrumentHeader
{
uint8le unknown; // 0x00
uint8le index; // actual instrument number
char name[28];
uint8le numSamples;
uint8le sampleMap[120];
uint8le vibratoType;
uint16le vibratoSweep;
uint16le vibratoDepth;
uint16le vibratoRate;
AMFFEnvelope envelopes;
uint16le fadeout;
// Convert instrument data to OpenMPT's internal format.
void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX baseSample)
{
mptIns.name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, name);
static_assert(mpt::array_size<decltype(sampleMap)>::size <= mpt::array_size<decltype(mptIns.Keyboard)>::size);
for(size_t i = 0; i < std::size(sampleMap); i++)
{
mptIns.Keyboard[i] = sampleMap[i] + baseSample + 1;
}
mptIns.nFadeOut = fadeout << 5;
envelopes.ConvertToMPT(mptIns);
}
};
MPT_BINARY_STRUCT(AMFFInstrumentHeader, 225)
// AMFF sample header (old format)
struct AMFFSampleHeader
{
// Sample flags (also used for RIFF AM)
enum SampleFlags
{
smp16Bit = 0x04,
smpLoop = 0x08,
smpPingPong = 0x10,
smpPanning = 0x20,
smpExists = 0x80,
// some flags are still missing... what is e.g. 0x8000?
};
uint32le id; // "SAMP"
uint32le chunkSize; // header + sample size
char name[28];
uint8le pan;
uint8le volume;
uint16le flags;
uint32le length;
uint32le loopStart;
uint32le loopEnd;
uint32le sampleRate;
uint32le reserved1;
uint32le reserved2;
// Convert sample header to OpenMPT's internal format.
void ConvertToMPT(AMFFInstrumentHeader &instrHeader, ModSample &mptSmp) const
{
mptSmp.Initialize();
mptSmp.nPan = pan * 4;
mptSmp.nVolume = volume * 4;
mptSmp.nGlobalVol = 64;
mptSmp.nLength = length;
mptSmp.nLoopStart = loopStart;
mptSmp.nLoopEnd = loopEnd;
mptSmp.nC5Speed = sampleRate;
if(instrHeader.vibratoType < std::size(j2bAutoVibratoTrans))
mptSmp.nVibType = j2bAutoVibratoTrans[instrHeader.vibratoType];
mptSmp.nVibSweep = static_cast<uint8>(instrHeader.vibratoSweep);
mptSmp.nVibRate = static_cast<uint8>(instrHeader.vibratoRate / 16);
mptSmp.nVibDepth = static_cast<uint8>(instrHeader.vibratoDepth / 4);
if((mptSmp.nVibRate | mptSmp.nVibDepth) != 0)
{
// Convert XM-style vibrato sweep to IT
mptSmp.nVibSweep = 255 - mptSmp.nVibSweep;
}
if(flags & AMFFSampleHeader::smp16Bit)
mptSmp.uFlags.set(CHN_16BIT);
if(flags & AMFFSampleHeader::smpLoop)
mptSmp.uFlags.set(CHN_LOOP);
if(flags & AMFFSampleHeader::smpPingPong)
mptSmp.uFlags.set(CHN_PINGPONGLOOP);
if(flags & AMFFSampleHeader::smpPanning)
mptSmp.uFlags.set(CHN_PANNING);
}
// Retrieve the internal sample format flags for this sample.
SampleIO GetSampleFormat() const
{
return SampleIO(
(flags & AMFFSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit,
SampleIO::mono,
SampleIO::littleEndian,
SampleIO::signedPCM);
}
};
MPT_BINARY_STRUCT(AMFFSampleHeader, 64)
// AM instrument envelope (new format)
struct AMEnvelope
{
struct EnvPoint
{
uint16le tick;
int16le value;
};
uint16le flags;
uint8le numPoints; // actually, it's num. points - 1, and 0xFF if there is no envelope
uint8le sustainPoint;
uint8le loopStart;
uint8le loopEnd;
EnvPoint values[10];
uint16le fadeout; // why is this here? it's only needed for the volume envelope...
// Convert envelope data to OpenMPT's internal format.
void ConvertToMPT(InstrumentEnvelope &mptEnv, EnvelopeType envType) const
{
if(numPoints == 0xFF || numPoints == 0)
return;
mptEnv.resize(std::min(numPoints + 1, 10));
mptEnv.nSustainStart = mptEnv.nSustainEnd = sustainPoint;
mptEnv.nLoopStart = loopStart;
mptEnv.nLoopEnd = loopEnd;
int32 scale = 0, offset = 0;
switch(envType)
{
case ENV_VOLUME: // 0....32767
default:
scale = 32767 / ENVELOPE_MAX;
break;
case ENV_PITCH: // -4096....4096
scale = 8192 / ENVELOPE_MAX;
offset = 4096;
break;
case ENV_PANNING: // -32768...32767
scale = 65536 / ENVELOPE_MAX;
offset = 32768;
break;
}
for(uint32 i = 0; i < mptEnv.size(); i++)
{
mptEnv[i].tick = values[i].tick >> 4;
if(i == 0)
mptEnv[i].tick = 0;
else if(mptEnv[i].tick < mptEnv[i - 1].tick)
mptEnv[i].tick = mptEnv[i - 1].tick + 1;
int32 val = values[i].value + offset;
val = (val + scale / 2) / scale;
mptEnv[i].value = static_cast<EnvelopeNode::value_t>(std::clamp(val, int32(ENVELOPE_MIN), int32(ENVELOPE_MAX)));
}
mptEnv.dwFlags.set(ENV_ENABLED, (flags & AMFFEnvelope::envEnabled) != 0);
mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & AMFFEnvelope::envSustain) && mptEnv.nSustainStart <= mptEnv.size());
mptEnv.dwFlags.set(ENV_LOOP, (flags & AMFFEnvelope::envLoop) && mptEnv.nLoopStart <= mptEnv.nLoopEnd && mptEnv.nLoopStart <= mptEnv.size());
}
};
MPT_BINARY_STRUCT(AMEnvelope::EnvPoint, 4)
MPT_BINARY_STRUCT(AMEnvelope, 48)
// AM instrument header (new format)
struct AMInstrumentHeader
{
uint32le headSize; // Header size (i.e. the size of this struct)
uint8le unknown1; // 0x00
uint8le index; // Actual instrument number
char name[32];
uint8le sampleMap[128];
uint8le vibratoType;
uint16le vibratoSweep;
uint16le vibratoDepth;
uint16le vibratoRate;
uint8le unknown2[7];
AMEnvelope volEnv;
AMEnvelope pitchEnv;
AMEnvelope panEnv;
uint16le numSamples;
// Convert instrument data to OpenMPT's internal format.
void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX baseSample)
{
mptIns.name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, name);
static_assert(mpt::array_size<decltype(sampleMap)>::size <= mpt::array_size<decltype(mptIns.Keyboard)>::size);
for(uint8 i = 0; i < std::size(sampleMap); i++)
{
mptIns.Keyboard[i] = sampleMap[i] + baseSample + 1;
}
mptIns.nFadeOut = volEnv.fadeout << 5;
volEnv.ConvertToMPT(mptIns.VolEnv, ENV_VOLUME);
pitchEnv.ConvertToMPT(mptIns.PitchEnv, ENV_PITCH);
panEnv.ConvertToMPT(mptIns.PanEnv, ENV_PANNING);
if(numSamples == 0)
{
MemsetZero(mptIns.Keyboard);
}
}
};
MPT_BINARY_STRUCT(AMInstrumentHeader, 326)
// AM sample header (new format)
struct AMSampleHeader
{
uint32le headSize; // Header size (i.e. the size of this struct), apparently not including headSize.
char name[32];
uint16le pan;
uint16le volume;
uint16le flags;
uint16le unknown; // 0x0000 / 0x0080?
uint32le length;
uint32le loopStart;
uint32le loopEnd;
uint32le sampleRate;
// Convert sample header to OpenMPT's internal format.
void ConvertToMPT(AMInstrumentHeader &instrHeader, ModSample &mptSmp) const
{
mptSmp.Initialize();
mptSmp.nPan = std::min(pan.get(), uint16(32767)) * 256 / 32767;
mptSmp.nVolume = std::min(volume.get(), uint16(32767)) * 256 / 32767;
mptSmp.nGlobalVol = 64;
mptSmp.nLength = length;
mptSmp.nLoopStart = loopStart;
mptSmp.nLoopEnd = loopEnd;
mptSmp.nC5Speed = sampleRate;
if(instrHeader.vibratoType < std::size(j2bAutoVibratoTrans))
mptSmp.nVibType = j2bAutoVibratoTrans[instrHeader.vibratoType];
mptSmp.nVibSweep = static_cast<uint8>(instrHeader.vibratoSweep);
mptSmp.nVibRate = static_cast<uint8>(instrHeader.vibratoRate / 16);
mptSmp.nVibDepth = static_cast<uint8>(instrHeader.vibratoDepth / 4);
if((mptSmp.nVibRate | mptSmp.nVibDepth) != 0)
{
// Convert XM-style vibrato sweep to IT
mptSmp.nVibSweep = 255 - mptSmp.nVibSweep;
}
if(flags & AMFFSampleHeader::smp16Bit)
mptSmp.uFlags.set(CHN_16BIT);
if(flags & AMFFSampleHeader::smpLoop)
mptSmp.uFlags.set(CHN_LOOP);
if(flags & AMFFSampleHeader::smpPingPong)
mptSmp.uFlags.set(CHN_PINGPONGLOOP);
if(flags & AMFFSampleHeader::smpPanning)
mptSmp.uFlags.set(CHN_PANNING);
}
// Retrieve the internal sample format flags for this sample.
SampleIO GetSampleFormat() const
{
return SampleIO(
(flags & AMFFSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit,
SampleIO::mono,
SampleIO::littleEndian,
SampleIO::signedPCM);
}
};
MPT_BINARY_STRUCT(AMSampleHeader, 60)
// Convert RIFF AM(FF) pattern data to MPT pattern data.
static bool ConvertAMPattern(FileReader chunk, PATTERNINDEX pat, bool isAM, CSoundFile &sndFile)
{
// Effect translation LUT
static constexpr EffectCommand amEffTrans[] =
{
CMD_ARPEGGIO, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_TONEPORTAMENTO,
CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, CMD_TREMOLO,
CMD_PANNING8, CMD_OFFSET, CMD_VOLUMESLIDE, CMD_POSITIONJUMP,
CMD_VOLUME, CMD_PATTERNBREAK, CMD_MODCMDEX, CMD_TEMPO,
CMD_GLOBALVOLUME, CMD_GLOBALVOLSLIDE, CMD_KEYOFF, CMD_SETENVPOSITION,
CMD_CHANNELVOLUME, CMD_CHANNELVOLSLIDE, CMD_PANNINGSLIDE, CMD_RETRIG,
CMD_TREMOR, CMD_XFINEPORTAUPDOWN,
};
enum
{
rowDone = 0, // Advance to next row
channelMask = 0x1F, // Mask for retrieving channel information
volFlag = 0x20, // Volume effect present
noteFlag = 0x40, // Note + instr present
effectFlag = 0x80, // Effect information present
dataFlag = 0xE0, // Channel data present
};
if(chunk.NoBytesLeft())
{
return false;
}
ROWINDEX numRows = Clamp(static_cast<ROWINDEX>(chunk.ReadUint8()) + 1, ROWINDEX(1), MAX_PATTERN_ROWS);
if(!sndFile.Patterns.Insert(pat, numRows))
return false;
const CHANNELINDEX channels = sndFile.GetNumChannels();
if(channels == 0)
return false;
ROWINDEX row = 0;
while(row < numRows && chunk.CanRead(1))
{
const uint8 flags = chunk.ReadUint8();
if(flags == rowDone)
{
row++;
continue;
}
ModCommand &m = *sndFile.Patterns[pat].GetpModCommand(row, std::min(static_cast<CHANNELINDEX>(flags & channelMask), static_cast<CHANNELINDEX>(channels - 1)));
if(flags & dataFlag)
{
if(flags & effectFlag) // effect
{
m.param = chunk.ReadUint8();
uint8 command = chunk.ReadUint8();
if(command < std::size(amEffTrans))
{
// command translation
m.command = amEffTrans[command];
} else
{
#ifdef J2B_LOG
MPT_LOG_GLOBAL(LogDebug, "J2B", MPT_UFORMAT("J2B: Unknown command: 0x{}, param 0x{}")(mpt::ufmt::HEX0<2>(command), mpt::ufmt::HEX0<2>(m.param)));
#endif
m.command = CMD_NONE;
}
// Handling special commands
switch(m.command)
{
case CMD_ARPEGGIO:
if(m.param == 0) m.command = CMD_NONE;
break;
case CMD_VOLUME:
if(m.volcmd == VOLCMD_NONE)
{
m.volcmd = VOLCMD_VOLUME;
m.vol = Clamp(m.param, uint8(0), uint8(64));
m.command = CMD_NONE;
m.param = 0;
}
break;
case CMD_TONEPORTAVOL:
case CMD_VIBRATOVOL:
case CMD_VOLUMESLIDE:
case CMD_GLOBALVOLSLIDE:
case CMD_PANNINGSLIDE:
if (m.param & 0xF0) m.param &= 0xF0;
break;
case CMD_PANNING8:
if(m.param <= 0x80) m.param = mpt::saturate_cast<uint8>(m.param * 2);
else if(m.param == 0xA4) {m.command = CMD_S3MCMDEX; m.param = 0x91;}
break;
case CMD_PATTERNBREAK:
m.param = ((m.param >> 4) * 10) + (m.param & 0x0F);
break;
case CMD_MODCMDEX:
m.ExtendedMODtoS3MEffect();
break;
case CMD_TEMPO:
if(m.param <= 0x1F) m.command = CMD_SPEED;
break;
case CMD_XFINEPORTAUPDOWN:
switch(m.param & 0xF0)
{
case 0x10:
m.command = CMD_PORTAMENTOUP;
break;
case 0x20:
m.command = CMD_PORTAMENTODOWN;
break;
}
m.param = (m.param & 0x0F) | 0xE0;
break;
}
}
if (flags & noteFlag) // note + ins
{
const auto [instr, note] = chunk.ReadArray<uint8, 2>();
m.instr = instr;
m.note = note;
if(m.note == 0x80) m.note = NOTE_KEYOFF;
else if(m.note > 0x80) m.note = NOTE_FADE; // I guess the support for IT "note fade" notes was not intended in mod2j2b, but hey, it works! :-D
}
if (flags & volFlag) // volume
{
m.volcmd = VOLCMD_VOLUME;
m.vol = chunk.ReadUint8();
if(isAM)
{
m.vol = m.vol * 64 / 127;
}
}
}
}
return true;
}
struct AMFFRiffChunkFormat
{
uint32le format;
};
MPT_BINARY_STRUCT(AMFFRiffChunkFormat, 4)
static bool ValidateHeader(const AMFFRiffChunk &fileHeader)
{
if(fileHeader.id != AMFFRiffChunk::idRIFF)
{
return false;
}
if(fileHeader.GetLength() < 8 + sizeof(AMFFMainChunk))
{
return false;
}
return true;
}
static bool ValidateHeader(const AMFFRiffChunkFormat &formatHeader)
{
if(formatHeader.format != AMFFRiffChunk::idAMFF && formatHeader.format != AMFFRiffChunk::idAM__)
{
return false;
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAM(MemoryFileReader file, const uint64 *pfilesize)
{
AMFFRiffChunk fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
AMFFRiffChunkFormat formatHeader;
if(!file.ReadStruct(formatHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(formatHeader))
{
return ProbeFailure;
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
bool CSoundFile::ReadAM(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
AMFFRiffChunk fileHeader;
if(!file.ReadStruct(fileHeader))
{
return false;
}
if(!ValidateHeader(fileHeader))
{
return false;
}
AMFFRiffChunkFormat formatHeader;
if(!file.ReadStruct(formatHeader))
{
return false;
}
if(!ValidateHeader(formatHeader))
{
return false;
}
bool isAM; // false: AMFF, true: AM
uint32 format = formatHeader.format;
if(format == AMFFRiffChunk::idAMFF)
isAM = false; // "AMFF"
else if(format == AMFFRiffChunk::idAM__)
isAM = true; // "AM "
else
return false;
ChunkReader chunkFile(file);
// The main chunk is almost identical in both formats but uses different chunk IDs.
// "MAIN" - Song info (AMFF)
// "INIT" - Song info (AM)
AMFFRiffChunk::ChunkIdentifiers mainChunkID = isAM ? AMFFRiffChunk::idINIT : AMFFRiffChunk::idMAIN;
// RIFF AM has a padding byte so that all chunks have an even size.
ChunkReader::ChunkList<AMFFRiffChunk> chunks;
if(loadFlags == onlyVerifyHeader)
chunks = chunkFile.ReadChunksUntil<AMFFRiffChunk>(isAM ? 2 : 1, mainChunkID);
else
chunks = chunkFile.ReadChunks<AMFFRiffChunk>(isAM ? 2 : 1);
FileReader chunkMain(chunks.GetChunk(mainChunkID));
AMFFMainChunk mainChunk;
if(!chunkMain.IsValid()
|| !chunkMain.ReadStruct(mainChunk)
|| mainChunk.channels < 1
|| !chunkMain.CanRead(mainChunk.channels))
{
return false;
} else if(loadFlags == onlyVerifyHeader)
{
return true;
}
InitializeGlobals(MOD_TYPE_J2B);
m_SongFlags = SONG_ITOLDEFFECTS | SONG_ITCOMPATGXX;
m_SongFlags.set(SONG_LINEARSLIDES, !(mainChunk.flags & AMFFMainChunk::amigaSlides));
m_nChannels = std::min(static_cast<CHANNELINDEX>(mainChunk.channels), static_cast<CHANNELINDEX>(MAX_BASECHANNELS));
m_nDefaultSpeed = mainChunk.speed;
m_nDefaultTempo.Set(mainChunk.tempo);
m_nDefaultGlobalVolume = mainChunk.globalvolume * 2;
m_modFormat.formatName = isAM ? UL_("Galaxy Sound System (new version)") : UL_("Galaxy Sound System (old version)");
m_modFormat.type = U_("j2b");
m_modFormat.charset = mpt::Charset::CP437;
m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, mainChunk.songname);
// It seems like there's no way to differentiate between
// Muted and Surround channels (they're all 0xA0) - might
// be a limitation in mod2j2b.
for(CHANNELINDEX nChn = 0; nChn < m_nChannels; nChn++)
{
ChnSettings[nChn].Reset();
uint8 pan = chunkMain.ReadUint8();
if(isAM)
{
if(pan > 128)
ChnSettings[nChn].dwFlags = CHN_MUTE;
else
ChnSettings[nChn].nPan = pan * 2;
} else
{
if(pan >= 128)
ChnSettings[nChn].dwFlags = CHN_MUTE;
else
ChnSettings[nChn].nPan = static_cast<uint16>(std::min(pan * 4, 256));
}
}
if(chunks.ChunkExists(AMFFRiffChunk::idORDR))
{
// "ORDR" - Order list
FileReader chunk(chunks.GetChunk(AMFFRiffChunk::idORDR));
uint8 numOrders = chunk.ReadUint8() + 1;
ReadOrderFromFile<uint8>(Order(), chunk, numOrders, 0xFF, 0xFE);
}
// "PATT" - Pattern data for one pattern
if(loadFlags & loadPatternData)
{
PATTERNINDEX maxPattern = 0;
auto pattChunks = chunks.GetAllChunks(AMFFRiffChunk::idPATT);
Patterns.ResizeArray(static_cast<PATTERNINDEX>(pattChunks.size()));
for(auto chunk : pattChunks)
{
PATTERNINDEX pat = chunk.ReadUint8();
size_t patternSize = chunk.ReadUint32LE();
ConvertAMPattern(chunk.ReadChunk(patternSize), pat, isAM, *this);
maxPattern = std::max(maxPattern, pat);
}
for(PATTERNINDEX pat = 0; pat < maxPattern; pat++)
{
if(!Patterns.IsValidPat(pat))
Patterns.Insert(pat, 64);
}
}
if(!isAM)
{
// "INST" - Instrument (only in RIFF AMFF)
auto instChunks = chunks.GetAllChunks(AMFFRiffChunk::idINST);
for(auto chunk : instChunks)
{
AMFFInstrumentHeader instrHeader;
if(!chunk.ReadStruct(instrHeader))
{
continue;
}
const INSTRUMENTINDEX instr = instrHeader.index + 1;
if(instr >= MAX_INSTRUMENTS)
continue;
ModInstrument *pIns = AllocateInstrument(instr);
if(pIns == nullptr)
{
continue;
}
instrHeader.ConvertToMPT(*pIns, m_nSamples);
// read sample sub-chunks - this is a rather "flat" format compared to RIFF AM and has no nested RIFF chunks.
for(size_t samples = 0; samples < instrHeader.numSamples; samples++)
{
AMFFSampleHeader sampleHeader;
if(!CanAddMoreSamples() || !chunk.ReadStruct(sampleHeader))
{
continue;
}
const SAMPLEINDEX smp = ++m_nSamples;
if(sampleHeader.id != AMFFRiffChunk::idSAMP)
{
continue;
}
m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name);
sampleHeader.ConvertToMPT(instrHeader, Samples[smp]);
if(loadFlags & loadSampleData)
sampleHeader.GetSampleFormat().ReadSample(Samples[smp], chunk);
else
chunk.Skip(Samples[smp].GetSampleSizeInBytes());
}
}
} else
{
// "RIFF" - Instrument (only in RIFF AM)
auto instChunks = chunks.GetAllChunks(AMFFRiffChunk::idRIFF);
for(ChunkReader chunk : instChunks)
{
if(chunk.ReadUint32LE() != AMFFRiffChunk::idAI__)
{
continue;
}
AMFFRiffChunk instChunk;
if(!chunk.ReadStruct(instChunk) || instChunk.id != AMFFRiffChunk::idINST)
{
continue;
}
AMInstrumentHeader instrHeader;
if(!chunk.ReadStruct(instrHeader))
{
continue;
}
MPT_ASSERT(instrHeader.headSize + 4 == sizeof(instrHeader));
const INSTRUMENTINDEX instr = instrHeader.index + 1;
if(instr >= MAX_INSTRUMENTS)
continue;
ModInstrument *pIns = AllocateInstrument(instr);
if(pIns == nullptr)
{
continue;
}
instrHeader.ConvertToMPT(*pIns, m_nSamples);
// Read sample sub-chunks (RIFF nesting ftw)
auto sampleChunks = chunk.ReadChunks<AMFFRiffChunk>(2).GetAllChunks(AMFFRiffChunk::idRIFF);
MPT_ASSERT(sampleChunks.size() == instrHeader.numSamples);
for(auto sampleChunk : sampleChunks)
{
if(sampleChunk.ReadUint32LE() != AMFFRiffChunk::idAS__ || !CanAddMoreSamples())
{
continue;
}
// Don't read more samples than the instrument header claims to have.
if((instrHeader.numSamples--) == 0)
{
break;
}
const SAMPLEINDEX smp = ++m_nSamples;
// Aaand even more nested chunks! Great, innit?
AMFFRiffChunk sampleHeaderChunk;
if(!sampleChunk.ReadStruct(sampleHeaderChunk) || sampleHeaderChunk.id != AMFFRiffChunk::idSAMP)
{
break;
}
FileReader sampleFileChunk = sampleChunk.ReadChunk(sampleHeaderChunk.length);
AMSampleHeader sampleHeader;
if(!sampleFileChunk.ReadStruct(sampleHeader))
{
break;
}
m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name);
sampleHeader.ConvertToMPT(instrHeader, Samples[smp]);
if(loadFlags & loadSampleData)
{
sampleFileChunk.Seek(sampleHeader.headSize + 4);
sampleHeader.GetSampleFormat().ReadSample(Samples[smp], sampleFileChunk);
}
}
}
}
return true;
}
static bool ValidateHeader(const J2BFileHeader &fileHeader)
{
if(std::memcmp(fileHeader.signature, "MUSE", 4)
|| (fileHeader.deadbeaf != J2BFileHeader::magicDEADBEAF // 0xDEADBEAF (RIFF AM)
&& fileHeader.deadbeaf != J2BFileHeader::magicDEADBABE) // 0xDEADBABE (RIFF AMFF)
)
{
return false;
}
if(fileHeader.packedLength == 0)
{
return false;
}
if(fileHeader.fileLength != fileHeader.packedLength + sizeof(J2BFileHeader))
{
return false;
}
return true;
}
static bool ValidateHeaderFileSize(const J2BFileHeader &fileHeader, uint64 filesize)
{
if(filesize != fileHeader.fileLength)
{
return false;
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderJ2B(MemoryFileReader file, const uint64 *pfilesize)
{
J2BFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
if(pfilesize)
{
if(!ValidateHeaderFileSize(fileHeader, *pfilesize))
{
return ProbeFailure;
}
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
bool CSoundFile::ReadJ2B(FileReader &file, ModLoadingFlags loadFlags)
{
#if !defined(MPT_WITH_ZLIB) && !defined(MPT_WITH_MINIZ)
MPT_UNREFERENCED_PARAMETER(file);
MPT_UNREFERENCED_PARAMETER(loadFlags);
return false;
#else
file.Rewind();
J2BFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return false;
}
if(!ValidateHeader(fileHeader))
{
return false;
}
if(fileHeader.fileLength != file.GetLength()
|| fileHeader.packedLength != file.BytesLeft()
)
{
return false;
}
if(loadFlags == onlyVerifyHeader)
{
return true;
}
// Header is valid, now unpack the RIFF AM file using inflate
z_stream strm{};
if(inflateInit(&strm) != Z_OK)
return false;
uint32 remainRead = fileHeader.packedLength, remainWrite = fileHeader.unpackedLength, totalWritten = 0;
uint32 crc = 0;
std::vector<Bytef> amFileData(remainWrite);
int retVal = Z_OK;
while(remainRead && remainWrite && retVal != Z_STREAM_END)
{
Bytef buffer[mpt::IO::BUFFERSIZE_TINY];
uint32 readSize = std::min(static_cast<uint32>(sizeof(buffer)), remainRead);
file.ReadRaw(mpt::span(buffer, readSize));
crc = crc32(crc, buffer, readSize);
strm.avail_in = readSize;
strm.next_in = buffer;
do
{
strm.avail_out = remainWrite;
strm.next_out = amFileData.data() + totalWritten;
retVal = inflate(&strm, Z_NO_FLUSH);
uint32 written = remainWrite - strm.avail_out;
totalWritten += written;
remainWrite -= written;
} while(remainWrite && strm.avail_out == 0);
remainRead -= readSize;
}
inflateEnd(&strm);
bool result = false;
#ifndef MPT_BUILD_FUZZER
if(fileHeader.crc32 == crc && !remainWrite && retVal == Z_STREAM_END)
#endif
{
// Success, now load the RIFF AM(FF) module.
FileReader amFile(mpt::as_span(amFileData));
result = ReadAM(amFile, loadFlags);
}
return result;
#endif
}
OPENMPT_NAMESPACE_END
|