@modplayjs/fmt-mo3 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mo3.js ADDED
@@ -0,0 +1,1310 @@
1
+ // SPDX-License-Identifier: BSD-3-Clause
2
+ // Copyright (c) 2026 Bitti09 — modplayjs contributors
3
+ // Ported from: OpenMPT soundlib/Load_mo3.cpp — container/header/pattern/
4
+ // instrument/sample parsing (MO3 v0-v5), with the UNMO3-derived LZ music
5
+ // depacker (mo3LZ.ts) and a minimp3 Layer-3 port (minimp3.ts) for
6
+ // MP3-compressed samples.
7
+ import { ParseError, PeriodType, C4_NTSC_RATE } from '@modplayjs/core';
8
+ import { FX_ARPEGGIO, FX_PORTA_UP, FX_PORTA_DN, FX_TONEPORTA, FX_VIBRATO, FX_TONE_VSLIDE, FX_VIBRA_VSLIDE, FX_TREMOLO, FX_SETPAN, FX_OFFSET, FX_VOLSLIDE, FX_JUMP, FX_VOLSET, FX_BREAK, FX_EXTENDED, FX_SPEED, FX_GLOBALVOL, FX_GVOL_SLIDE, FX_KEYOFF, FX_ENVPOS, FX_PANSLIDE, FX_MULTI_RETRIG, FX_TREMOR, FX_XF_PORTA, FX_TRK_VOL, FX_TRK_VSLIDE, FX_IT_BPM, FX_PANBRELLO, FX_MACRO, FX_FINE_VIBRATO, } from '@modplayjs/core';
9
+ import { ReadEventType } from '@modplayjs/core';
10
+ import { depackMO3Music, lastConsumed } from './mo3LZ.js';
11
+ import { parseMPEGFrame } from './mpegFrame.js';
12
+ import { mp3decInit, mp3decDecodeFrame } from './minimp3.js';
13
+ import { decodeOggStream, mergeSharedOggHeader } from './ogg.js';
14
+ // MO3HeaderFlags (Load_mo3.cpp:69-85)
15
+ const FLAG_LINEAR_SLIDES = 0x0001;
16
+ const FLAG_IS_S3M = 0x0002;
17
+ // const _unused_FLAG_S3M_FAST_SLIDES = 0x0004;void 0 as unknown;
18
+ const FLAG_IS_MTM = 0x0008;
19
+ // const _unused_FLAG_S3M_AMIGA_LIMITS = 0x0010;
20
+ const FLAG_IS_MOD = 0x0080;
21
+ const FLAG_IS_IT = 0x0100;
22
+ const FLAG_INSTRUMENT_MODE = 0x0200;
23
+ // const _unused_FLAG_IT_COMPAT_GXX = 0x0400;
24
+ // const _unused_FLAG_IT_OLD_FX = 0x0800;
25
+ // const _unused_FLAG_MODPLUG_MODE = 0x10000;
26
+ // const _unused_FLAG_UNKNOWN = 0x20000;
27
+ // const _unused_FLAG_MOD_VBLANK = 0x80000;
28
+ // const _unused_FLAG_HAS_PLUGINS = 0x100000;
29
+ const FX_NONE = 0x00;
30
+ // MO3SampleFlags (Load_mo3.cpp:269-282)
31
+ const SMP_16BIT = 0x01;
32
+ // const _SMP_LOOP = 0x10;
33
+ // const _SMP_PINGPONG = 0x20;
34
+ // const _SMP_SUSTAIN = 0x100;
35
+ // const _SMP_SUSTAIN_PINGPONG = 0x200;
36
+ const SMP_STEREO = 0x400;
37
+ const SMP_COMPRESSION_MPEG = 0x1000;
38
+ const SMP_COMPRESSION_OGG = 0x3000; // Ogg sample
39
+ const SMP_SHARED_OGG = 0x1000 | 0x2000 | 0x4000; // Ogg sample, shared header
40
+ // const _SMP_SHARED_OGG = 0x7000;
41
+ const SMP_DELTA_COMPRESSION = 0x2000;
42
+ const SMP_DELTA_PREDICTION = 0x4000;
43
+ // const SMP_OPL_INSTRUMENT = 0x8000;
44
+ const SMP_COMPRESSION_MASK = 0xf000;
45
+ // OpenMPT CMD_* codes are the effTrans INDEX (not values) — the table maps
46
+ // straight to libxmp FX codes (effects.h). Volume-command enum values are
47
+ // OpenMPT VOLCMD_* (Sndfile.h) handled in applyVolCmd.
48
+ /** effTrans (Load_mo3.cpp:1009-1067) — OpenMPT CMD_* index → libxmp FX code.
49
+ * Values match effects.h (our Event.fxt codes). */
50
+ const EFF_TRANS = [
51
+ FX_NONE, FX_NONE, FX_NONE, FX_ARPEGGIO, // 00-03
52
+ FX_PORTA_UP, FX_PORTA_DN, FX_TONEPORTA, FX_VIBRATO, // 04-07
53
+ FX_TONE_VSLIDE, FX_VIBRA_VSLIDE, FX_TREMOLO, FX_SETPAN, // 08-0b
54
+ FX_OFFSET, FX_VOLSLIDE, FX_JUMP, FX_VOLSET, // 0c-0f
55
+ FX_BREAK, FX_EXTENDED, FX_SPEED, FX_TREMOR, // 10-13
56
+ FX_NONE, FX_NONE, FX_GLOBALVOL, FX_GVOL_SLIDE, // 14-17
57
+ FX_KEYOFF, FX_ENVPOS, FX_PANSLIDE, FX_NONE, // 18-1b
58
+ FX_MULTI_RETRIG, FX_XF_PORTA, FX_XF_PORTA, FX_NONE, // 1c-1f
59
+ FX_NONE, FX_SPEED, FX_VOLSLIDE, FX_PORTA_DN, // 20-23 (IT/S3M)
60
+ FX_PORTA_UP, FX_TREMOR, FX_MULTI_RETRIG, FX_FINE_VIBRATO, // 24-27
61
+ FX_TRK_VOL, FX_TRK_VSLIDE, FX_PANSLIDE, FX_EXTENDED, // 28-2b (S3MCMDEX → extended)
62
+ FX_IT_BPM, FX_GVOL_SLIDE, FX_PANBRELLO, FX_MACRO, // 2c-2f
63
+ FX_NONE, FX_NONE, FX_NONE, FX_NONE, // 30-33
64
+ FX_NONE, FX_NONE, FX_NONE, FX_NONE, // 34-37
65
+ 0xa6, 0xbe, // 38-39 (MPTM)
66
+ ];
67
+ // OpenMPT volume command enum values (Sndfile.h VOLCMD_*)
68
+ const VOLCMD_NONE = 0;
69
+ const VOLCMD_VOLUME = 1;
70
+ const VOLCMD_PANNING = 2;
71
+ const VOLCMD_VOLSLIDEUP = 3;
72
+ const VOLCMD_VOLSLIDEDOWN = 4;
73
+ const VOLCMD_FINEVOLUP = 5;
74
+ const VOLCMD_FINEVOLDOWN = 6;
75
+ const VOLCMD_VIBRATOSPEED = 7;
76
+ const VOLCMD_VIBRATODEPTH = 8;
77
+ const VOLCMD_PANSLIDELEFT = 9;
78
+ const VOLCMD_PANSLIDERIGHT = 10;
79
+ const VOLCMD_TONEPORTAMENTO = 11;
80
+ const VOLCMD_PORTAUP = 12;
81
+ const VOLCMD_PORTADOWN = 13;
82
+ const VOLCMD_OFFSET = 14;
83
+ // OpenMPT envelope flag bits → our Envelope.flags (XMP_ENVELOPE_*)
84
+ const ENV_ENABLED = 0x01;
85
+ const ENV_SUSTAIN = 0x02;
86
+ const ENV_LOOP = 0x04;
87
+ const ENV_FILTER = 0x10;
88
+ const ENV_CARRY = 0x20;
89
+ const NOTE_MIN = 13; // C-0 in libxmp note numbering
90
+ const NOTE_KEYOFF = 0x78; // 120
91
+ const NOTE_NOTECUT = 0x79; // 121
92
+ const NOTE_FADE = 0x7a; // 122
93
+ function rU8(r) {
94
+ return r.buf[r.pos++] ?? 0;
95
+ }
96
+ function rU16(r) {
97
+ const v = (r.buf[r.pos] | (r.buf[r.pos + 1] << 8)) >>> 0;
98
+ r.pos += 2;
99
+ return v;
100
+ }
101
+ function rI16(r) {
102
+ const v = rU16(r);
103
+ return v >= 0x8000 ? v - 0x10000 : v;
104
+ }
105
+ function rU32(r) {
106
+ const v = (r.buf[r.pos] | (r.buf[r.pos + 1] << 8) | (r.buf[r.pos + 2] << 16) | (r.buf[r.pos + 3] << 24)) >>> 0;
107
+ r.pos += 4;
108
+ return v;
109
+ }
110
+ function rI32(r) {
111
+ const v = rU32(r);
112
+ return v | 0;
113
+ }
114
+ function rNullString(r) {
115
+ let s = '';
116
+ while (r.pos < r.buf.length) {
117
+ const c = r.buf[r.pos++];
118
+ if (c === 0)
119
+ break;
120
+ s += String.fromCharCode(c);
121
+ }
122
+ return s;
123
+ }
124
+ // AutoVibratoIT2XM (Load_mo3.cpp:289-295 area) — IT vibrato type → XM type
125
+ const AUTO_VIBRATO_IT2XM = [0, 2, 4, 1, 3, 0, 0, 0];
126
+ function zeroEnvelope() {
127
+ return { flags: 0, npt: 0, scl: 0, sus: 0, sue: 0, lps: 0, lpe: 0, x: [], y: [] };
128
+ }
129
+ function zeroInstrument(name, sub) {
130
+ return {
131
+ name,
132
+ volume: 0x40,
133
+ nsm: 0,
134
+ rls: 0,
135
+ map: new Array(121).fill(0),
136
+ mapXpo: new Array(121).fill(0),
137
+ sub,
138
+ aei: zeroEnvelope(),
139
+ fei: zeroEnvelope(),
140
+ pei: zeroEnvelope(),
141
+ };
142
+ }
143
+ // MO3Envelope read + convert (Load_mo3.cpp:112-153)
144
+ function readMo3Envelope(r, envShift, isXm, env) {
145
+ const flags = rU8(r);
146
+ const numNodes = rU8(r);
147
+ const sustainStart = rU8(r);
148
+ const sustainEnd = rU8(r);
149
+ const loopStart = rU8(r);
150
+ const loopEnd = rU8(r);
151
+ const points = [];
152
+ for (let i = 0; i < 25; i++) {
153
+ points.push([rI16(r), rI16(r)]);
154
+ }
155
+ env.flags =
156
+ ((flags & 0x01) !== 0 ? ENV_ENABLED : 0) |
157
+ ((flags & 0x02) !== 0 ? ENV_SUSTAIN : 0) |
158
+ ((flags & 0x04) !== 0 ? ENV_LOOP : 0) |
159
+ ((flags & 0x10) !== 0 ? ENV_FILTER : 0) |
160
+ ((flags & 0x20) !== 0 ? ENV_CARRY : 0);
161
+ const n = Math.min(numNodes, 25);
162
+ env.npt = n;
163
+ env.sus = sustainStart;
164
+ env.sue = isXm ? sustainStart : sustainEnd;
165
+ env.lps = loopStart;
166
+ env.lpe = loopEnd;
167
+ env.x = [];
168
+ env.y = [];
169
+ for (let i = 0; i < n; i++) {
170
+ let tick = points[i][0];
171
+ if (i > 0 && tick < env.x[i - 1])
172
+ tick = env.x[i - 1] + 1;
173
+ env.x.push(tick);
174
+ env.y.push(Math.min(64, Math.max(0, points[i][1] >> envShift)));
175
+ }
176
+ }
177
+ export function mo3Test(bytes) {
178
+ return (bytes.length > 8 &&
179
+ bytes[0] === 0x4d /* M */ && bytes[1] === 0x4f /* O */ && bytes[2] === 0x33 /* 3 */ &&
180
+ bytes[3] <= 5 &&
181
+ ((bytes[4] | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24)) >>> 0) > 422 &&
182
+ ((bytes[4] | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24)) >>> 0) < 0x20000000);
183
+ }
184
+ function parseMO3(bytes, fail) {
185
+ const version = bytes[3];
186
+ const musicSize = ((bytes[4] | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24)) >>> 0);
187
+ if (musicSize <= 422 || musicSize >= 0x20000000)
188
+ fail('MO3: bad music size');
189
+ if (version > 5)
190
+ fail('MO3: bad version');
191
+ let compressedSize = 0;
192
+ let inner;
193
+ if (version >= 5) {
194
+ compressedSize = ((bytes[8] | (bytes[9] << 8) | (bytes[10] << 16) | (bytes[11] << 24)) >>> 0);
195
+ inner = depackMO3Music(bytes.subarray(0, 12 + compressedSize));
196
+ }
197
+ else {
198
+ inner = depackMO3Music(bytes);
199
+ }
200
+ const r = { buf: inner, pos: 0 };
201
+ const songName = rNullString(r);
202
+ const songMessage = rNullString(r);
203
+ const numChannels = rU8(r);
204
+ const numOrders = rU16(r);
205
+ const restartPos = rU16(r);
206
+ const numPatterns = rU16(r);
207
+ const numTracks = rU16(r);
208
+ const numInstruments = rU16(r);
209
+ const numSamples = rU16(r);
210
+ const defaultSpeed = rU8(r);
211
+ const defaultTempo = rU8(r);
212
+ const flags = rU32(r) >>> 0;
213
+ const globalVol = rU8(r);
214
+ const panSeparation = rU8(r);
215
+ const sampleVolume = (rU8(r) << 24) >> 24;
216
+ const chnVolume = [];
217
+ for (let i = 0; i < 64; i++)
218
+ chnVolume.push(rU8(r));
219
+ const chnPan = [];
220
+ for (let i = 0; i < 64; i++)
221
+ chnPan.push(rU8(r));
222
+ const sfxMacros = [];
223
+ for (let i = 0; i < 16; i++)
224
+ sfxMacros.push(rU8(r));
225
+ const fixedMacros = [];
226
+ for (let i = 0; i < 128; i++)
227
+ fixedMacros.push([rU8(r), rU8(r)]);
228
+ if (numChannels === 0 || numChannels > 64)
229
+ fail('MO3: bad channel count');
230
+ if (restartPos > numOrders)
231
+ fail('MO3: bad restart pos');
232
+ if (numInstruments >= 255 || numSamples >= 255)
233
+ fail('MO3: bad ins/smp count');
234
+ const isIT = (flags & FLAG_IS_IT) !== 0;
235
+ const isS3M = (flags & FLAG_IS_S3M) !== 0;
236
+ const isMOD = (flags & FLAG_IS_MOD) !== 0;
237
+ const isMTM = (flags & FLAG_IS_MTM) !== 0;
238
+ const modType = isIT ? 'it' : isS3M ? 's3m' : isMOD ? 'mod' : isMTM ? 'mtm' : 'xm';
239
+ const isXm = modType === 'xm';
240
+ // Orders (hasOrderSeparators for IT/S3M: 0xFF separators, 0xFE marks)
241
+ const hasOrderSeparators = !(modType === 'mod' || isXm);
242
+ const orders = [];
243
+ for (let i = 0; i < numOrders; i++) {
244
+ const b = rU8(r);
245
+ if (hasOrderSeparators && b === 0xff)
246
+ break; // order separator = end
247
+ orders.push(hasOrderSeparators && b === 0xfe ? 254 : b);
248
+ }
249
+ const len = orders.length;
250
+ // Track assignments + pattern lengths
251
+ const trackChunkOff = r.pos;
252
+ r.pos += numPatterns * numChannels * 2;
253
+ const patLengthChunkOff = r.pos;
254
+ r.pos += numPatterns * 2;
255
+ const trackOffsets = [];
256
+ const trackLens = [];
257
+ for (let i = 0; i < numTracks; i++) {
258
+ trackLens.push(rU32(r));
259
+ trackOffsets.push(r.pos);
260
+ r.pos += trackLens[i];
261
+ }
262
+ // Patterns
263
+ const patterns = [];
264
+ const noteOffset = modType === 'mtm' ? 13 + NOTE_MIN : !isIT ? 12 + NOTE_MIN : NOTE_MIN;
265
+ for (let pat = 0; pat < numPatterns; pat++) {
266
+ const numRows = inner[patLengthChunkOff + pat * 2] | (inner[patLengthChunkOff + pat * 2 + 1] << 8);
267
+ const rows = numRows === 0 ? 64 : numRows;
268
+ const tracks = [];
269
+ for (let chn = 0; chn < numChannels; chn++) {
270
+ const ev = [];
271
+ for (let row = 0; row < rows; row++) {
272
+ ev.push({ note: 0, ins: 0, vol: 0, fxt: 0, fxp: 0, f2t: 0, f2p: 0 });
273
+ }
274
+ tracks.push({ rows, event: ev });
275
+ }
276
+ patterns.push({ rows, tracks });
277
+ }
278
+ // decode tracks per pattern/channel
279
+ for (let pat = 0; pat < numPatterns; pat++) {
280
+ const numRows = inner[patLengthChunkOff + pat * 2] | (inner[patLengthChunkOff + pat * 2 + 1] << 8);
281
+ const rows = numRows === 0 ? 64 : numRows;
282
+ for (let chn = 0; chn < numChannels; chn++) {
283
+ const trackIndex = inner[trackChunkOff + (pat * numChannels + chn) * 2] |
284
+ (inner[trackChunkOff + (pat * numChannels + chn) * 2 + 1] << 8);
285
+ if (trackIndex >= numTracks)
286
+ continue;
287
+ const tr = { buf: inner, pos: trackOffsets[trackIndex] };
288
+ let row = 0;
289
+ while (row < rows) {
290
+ const b = rU8(tr);
291
+ if (b === 0)
292
+ break;
293
+ const numCommands = b & 0x0f;
294
+ const rep = b >> 4;
295
+ const m = { note: 0, ins: 0, vol: 0, fxt: 0, fxp: 0, f2t: 0, f2p: 0 };
296
+ let volcmd = VOLCMD_NONE;
297
+ let volParam = 0;
298
+ for (let c = 0; c < numCommands; c++) {
299
+ const cmd0 = rU8(tr);
300
+ const cmd1 = rU8(tr);
301
+ switch (cmd0) {
302
+ case 0x01: { // Note
303
+ let note = cmd1;
304
+ if (note < 120)
305
+ note += noteOffset;
306
+ else if (note === 0xff)
307
+ note = NOTE_KEYOFF;
308
+ else if (note === 0xfe)
309
+ note = NOTE_NOTECUT;
310
+ else
311
+ note = NOTE_FADE;
312
+ m.note = note;
313
+ break;
314
+ }
315
+ case 0x02: // Instrument
316
+ m.ins = cmd1 + 1;
317
+ break;
318
+ case 0x06: { // Tone portamento
319
+ if (volcmd === VOLCMD_NONE && isXm && !(cmd1 & 0x0f)) {
320
+ volcmd = VOLCMD_TONEPORTAMENTO;
321
+ volParam = cmd1 >> 4;
322
+ }
323
+ else if (volcmd === VOLCMD_NONE && isIT) {
324
+ const IT_PORTA_VOL = [0, 4, 8, 16, 32, 64, 96, 128, 255];
325
+ let found = false;
326
+ for (let i = 0; i < 9; i++) {
327
+ if (IT_PORTA_VOL[i] === cmd1) {
328
+ volcmd = VOLCMD_TONEPORTAMENTO;
329
+ volParam = i;
330
+ found = true;
331
+ break;
332
+ }
333
+ }
334
+ if (!found) {
335
+ m.fxt = FX_TONEPORTA;
336
+ m.fxp = cmd1;
337
+ }
338
+ }
339
+ else {
340
+ m.fxt = FX_TONEPORTA;
341
+ m.fxp = cmd1;
342
+ }
343
+ break;
344
+ }
345
+ case 0x07: { // Vibrato
346
+ if (volcmd === VOLCMD_NONE && cmd1 < 10 && isIT) {
347
+ volcmd = VOLCMD_VIBRATODEPTH;
348
+ volParam = cmd1;
349
+ }
350
+ else {
351
+ m.fxt = FX_VIBRATO;
352
+ m.fxp = cmd1;
353
+ }
354
+ break;
355
+ }
356
+ case 0x0b: { // Panning
357
+ if (volcmd === VOLCMD_NONE) {
358
+ if (isIT && cmd1 === 0xff) {
359
+ volcmd = VOLCMD_PANNING;
360
+ volParam = 64;
361
+ break;
362
+ }
363
+ if ((isIT && !(cmd1 & 0x03)) || (isXm && !(cmd1 & 0x0f))) {
364
+ volcmd = VOLCMD_PANNING;
365
+ volParam = cmd1 / 4;
366
+ break;
367
+ }
368
+ }
369
+ m.fxt = FX_SETPAN;
370
+ m.fxp = cmd1;
371
+ break;
372
+ }
373
+ case 0x0f: { // Volume
374
+ if (modType !== 'mod' && volcmd === VOLCMD_NONE && cmd1 <= 64) {
375
+ volcmd = VOLCMD_VOLUME;
376
+ volParam = cmd1;
377
+ }
378
+ else {
379
+ m.fxt = FX_VOLSET;
380
+ m.fxp = cmd1;
381
+ }
382
+ break;
383
+ }
384
+ case 0x10: { // Pattern break (BCD in MOD/XM/S3M/MTM)
385
+ m.fxt = FX_BREAK;
386
+ m.fxp = cmd1;
387
+ if (!isIT)
388
+ m.fxp = ((cmd1 >> 4) * 10) + (cmd1 & 0x0f);
389
+ break;
390
+ }
391
+ case 0x12: { // Combined tempo/speed
392
+ m.fxt = cmd1 < 0x20 ? FX_SPEED : FX_IT_BPM;
393
+ m.fxp = cmd1;
394
+ break;
395
+ }
396
+ case 0x14:
397
+ case 0x15: { // XM volume column volume slides
398
+ if ((cmd1 & 0xf0) !== 0) {
399
+ volcmd = cmd0 === 0x14 ? VOLCMD_VOLSLIDEUP : VOLCMD_FINEVOLUP;
400
+ volParam = cmd1 >> 4;
401
+ }
402
+ else {
403
+ volcmd = cmd0 === 0x14 ? VOLCMD_VOLSLIDEDOWN : VOLCMD_FINEVOLDOWN;
404
+ volParam = cmd1 & 0x0f;
405
+ }
406
+ break;
407
+ }
408
+ case 0x1b: { // XM volume column pan slides
409
+ if ((cmd1 & 0xf0) !== 0) {
410
+ volcmd = VOLCMD_PANSLIDERIGHT;
411
+ volParam = cmd1 >> 4;
412
+ }
413
+ else {
414
+ volcmd = VOLCMD_PANSLIDELEFT;
415
+ volParam = cmd1 & 0x0f;
416
+ }
417
+ break;
418
+ }
419
+ case 0x1d: // XM extra fine porta up
420
+ m.fxt = FX_XF_PORTA;
421
+ m.fxp = 0x10 | cmd1;
422
+ break;
423
+ case 0x1e: // XM extra fine porta down
424
+ m.fxt = FX_XF_PORTA;
425
+ m.fxp = 0x20 | cmd1;
426
+ break;
427
+ case 0x1f:
428
+ case 0x20: { // XM volume column vibrato
429
+ volcmd = cmd0 === 0x1f ? VOLCMD_VIBRATOSPEED : VOLCMD_VIBRATODEPTH;
430
+ volParam = cmd1;
431
+ break;
432
+ }
433
+ case 0x22: { // IT/S3M volume slide
434
+ if (m.fxt === FX_TONEPORTA)
435
+ m.fxt = FX_TONE_VSLIDE;
436
+ else if (m.fxt === FX_VIBRATO)
437
+ m.fxt = FX_VIBRA_VSLIDE;
438
+ else
439
+ m.fxt = FX_VOLSLIDE;
440
+ m.fxp = cmd1;
441
+ break;
442
+ }
443
+ case 0x30: { // IT volume column volume slides
444
+ m.vol = cmd1 % 10;
445
+ if (cmd1 < 10) {
446
+ volcmd = VOLCMD_FINEVOLUP;
447
+ m.vol = cmd1 % 10;
448
+ }
449
+ else if (cmd1 < 20) {
450
+ volcmd = VOLCMD_FINEVOLDOWN;
451
+ m.vol = cmd1 % 10;
452
+ }
453
+ else if (cmd1 < 30) {
454
+ volcmd = VOLCMD_VOLSLIDEUP;
455
+ m.vol = cmd1 % 10;
456
+ }
457
+ else if (cmd1 < 40) {
458
+ volcmd = VOLCMD_VOLSLIDEDOWN;
459
+ m.vol = cmd1 % 10;
460
+ }
461
+ break;
462
+ }
463
+ case 0x31:
464
+ case 0x32: { // IT volume column portamento
465
+ volcmd = cmd0 === 0x31 ? VOLCMD_PORTADOWN : VOLCMD_PORTAUP;
466
+ volParam = cmd1;
467
+ break;
468
+ }
469
+ case 0x34: { // IT volume column offset etc.
470
+ if (cmd1 >= 223 && cmd1 <= 232) {
471
+ volcmd = VOLCMD_OFFSET;
472
+ volParam = cmd1 - 223;
473
+ }
474
+ break;
475
+ }
476
+ default: {
477
+ if (cmd0 < EFF_TRANS.length) {
478
+ const fx = EFF_TRANS[cmd0];
479
+ if (fx !== FX_NONE || cmd0 === 0) {
480
+ m.fxt = fx;
481
+ m.fxp = cmd1;
482
+ }
483
+ }
484
+ break;
485
+ }
486
+ }
487
+ }
488
+ // Translate OpenMPT volume command → libxmp volume-column encoding
489
+ applyVolCmd(m, volcmd, volParam, modType);
490
+ const targetRow = Math.min(row + rep, rows);
491
+ while (row < targetRow) {
492
+ const t = patterns[pat].tracks[chn];
493
+ t.event[row] = { ...m };
494
+ row++;
495
+ }
496
+ }
497
+ }
498
+ }
499
+ // Instruments
500
+ const instruments = [];
501
+ const instrVibrato = [];
502
+ const isSampleMode = !isXm && !(flags & FLAG_INSTRUMENT_MODE);
503
+ const insCount = modType === 'xm' || !isSampleMode ? numInstruments : numInstruments;
504
+ for (let ins = 0; ins < insCount; ins++) {
505
+ const name = rNullString(r);
506
+ if (version >= 5)
507
+ rNullString(r); // filename
508
+ const insFlags = rU32(r) >>> 0;
509
+ const sampleMap = [];
510
+ for (let i = 0; i < 120; i++) {
511
+ sampleMap.push([rU16(r), rU16(r)]);
512
+ }
513
+ const aei = zeroEnvelope();
514
+ const pei = zeroEnvelope();
515
+ const fei = zeroEnvelope();
516
+ readMo3Envelope(r, 0, isXm, aei);
517
+ readMo3Envelope(r, 0, isXm, pei);
518
+ readMo3Envelope(r, 5, isXm, fei);
519
+ const vibType = rU8(r);
520
+ const vibSweep = rU8(r);
521
+ const vibDepth = rU8(r);
522
+ const vibRate = rU8(r);
523
+ const fadeOut = rU16(r);
524
+ const midiChannel = rU8(r);
525
+ const midiBank = rU8(r);
526
+ const midiPatch = rU8(r);
527
+ const midiBend = rU8(r);
528
+ const globalVol = rU8(r);
529
+ const panning = rU16(r);
530
+ const nna = rU8(r);
531
+ void nna;
532
+ const pps = rU8(r);
533
+ const ppc = rU8(r);
534
+ const dct = rU8(r);
535
+ const dca = rU8(r);
536
+ const volSwing = rU16(r);
537
+ const panSwing = rU16(r);
538
+ const cutoff = rU8(r);
539
+ const resonance = rU8(r);
540
+ void insFlags;
541
+ void midiChannel;
542
+ void midiBank;
543
+ void midiPatch;
544
+ void midiBend;
545
+ void volSwing;
546
+ void panSwing;
547
+ void pps;
548
+ void ppc;
549
+ const xxi = zeroInstrument(name, []);
550
+ if (isXm) {
551
+ for (let i = 0; i < 96; i++) {
552
+ xxi.map[i + 12] = sampleMap[i][1] + 1 === 0 ? 0 : sampleMap[i][1];
553
+ xxi.map[i + 12] = sampleMap[i][1] === 0xffff ? 0xff : sampleMap[i][1];
554
+ }
555
+ }
556
+ else {
557
+ for (let i = 0; i < 120; i++) {
558
+ xxi.mapXpo[i] = sampleMap[i][0] - (i - 12);
559
+ xxi.map[i] = sampleMap[i][1] === 0xffff ? 0xff : sampleMap[i][1] + 1;
560
+ }
561
+ }
562
+ // Envelope flags: our Envelope.flags uses XMP_ENVELOPE_* bits (same values)
563
+ xxi.aei = aei;
564
+ xxi.pei = pei;
565
+ xxi.fei = fei;
566
+ xxi.rls = fadeOut;
567
+ xxi.volume = isIT ? Math.min(globalVol, 128) / 2 : 0x40;
568
+ if (panning <= 256) {
569
+ // baked into sub pan below
570
+ }
571
+ xxi.nsm = 0; // filled by sample registration
572
+ instruments.push(xxi);
573
+ if (isXm) {
574
+ instrVibrato.push({ type: AUTO_VIBRATO_IT2XM[vibType & 7], sweep: vibSweep, depth: vibDepth, rate: vibRate });
575
+ }
576
+ void dct;
577
+ void dca;
578
+ void cutoff;
579
+ void resonance;
580
+ void panSeparation;
581
+ void sampleVolume;
582
+ }
583
+ if (isSampleMode) {
584
+ // sample mode: no instruments
585
+ instruments.length = 0;
586
+ }
587
+ // Samples
588
+ const frequencyIsHertz = version >= 5 || !(flags & FLAG_LINEAR_SLIDES);
589
+ const sampleHeaders = [];
590
+ const sampleNames = [];
591
+ for (let smp = 0; smp < numSamples; smp++) {
592
+ const name = rNullString(r);
593
+ if (version >= 5)
594
+ rNullString(r);
595
+ const freqFinetune = rU32(r);
596
+ const transpose = (rU8(r) << 24) >> 24;
597
+ const defaultVolume = rU8(r);
598
+ const panning = rU16(r);
599
+ const length = rU32(r);
600
+ const loopStart = rU32(r);
601
+ const loopEnd = rU32(r);
602
+ const sflags = rU16(r) >>> 0;
603
+ const vibType = rU8(r);
604
+ const vibSweep = rU8(r);
605
+ const vibDepth = rU8(r);
606
+ const vibRate = rU8(r);
607
+ const globalVol = rU8(r);
608
+ const sustainStart = rU32(r);
609
+ const sustainEnd = rU32(r);
610
+ const compressedSize = rI32(r);
611
+ const encoderDelay = rU16(r);
612
+ // Load_mo3.cpp:1368-1372: shared-header index for smpSharedOgg.
613
+ let sharedOggHeader = 0;
614
+ if (version >= 5 && (sflags & SMP_COMPRESSION_MASK) === SMP_SHARED_OGG) {
615
+ sharedOggHeader = (rU16(r) << 16) >> 16;
616
+ }
617
+ sampleNames.push(name);
618
+ sampleHeaders.push({
619
+ flags: sflags, length, loopStart, loopEnd, sustainStart, sustainEnd,
620
+ compressedSize, encoderDelay, vibType, vibSweep, vibDepth, vibRate,
621
+ globalVol, finetune: freqFinetune, transpose, volume: defaultVolume, panning,
622
+ sharedOggHeader,
623
+ });
624
+ void frequencyIsHertz;
625
+ }
626
+ // Build ModuleData
627
+ const channels = [];
628
+ for (let i = 0; i < numChannels; i++) {
629
+ let pan = 0x80;
630
+ let flg = 0;
631
+ if (isIT) {
632
+ // volume from header
633
+ }
634
+ if (!isXm) {
635
+ if (chnPan[i] === 127)
636
+ flg |= 0x10; // SURROUND
637
+ else if (chnPan[i] === 255)
638
+ pan = 256;
639
+ else
640
+ pan = chnPan[i];
641
+ }
642
+ channels.push({ pan: Math.min(255, pan), vol: isIT ? Math.min(chnVolume[i], 64) : 0x40, flg });
643
+ }
644
+ let c4rate = 8363;
645
+ let periodType = PeriodType.AMIGA;
646
+ let quirks = 0;
647
+ let readEventType = ReadEventType.FT2;
648
+ if (isIT) {
649
+ readEventType = ReadEventType.IT;
650
+ quirks = 0x3f16cf0; // QUIRKS_IT (matches fmt-it)
651
+ periodType = (flags & FLAG_LINEAR_SLIDES) !== 0 ? PeriodType.LINEAR : PeriodType.AMIGA;
652
+ }
653
+ else if (isS3M) {
654
+ readEventType = ReadEventType.ST3;
655
+ periodType = (flags & FLAG_LINEAR_SLIDES) !== 0 ? PeriodType.LINEAR : PeriodType.AMIGA;
656
+ }
657
+ else if (isXm) {
658
+ readEventType = ReadEventType.FT2;
659
+ periodType = (flags & FLAG_LINEAR_SLIDES) !== 0 ? PeriodType.LINEAR : PeriodType.AMIGA;
660
+ }
661
+ const mod = {
662
+ title: songName,
663
+ format: 'mo3', // dispatch key — our plugin name (readEventType carries the semantics)
664
+ comment: songMessage,
665
+ chn: numChannels,
666
+ pat: numPatterns,
667
+ ins: isSampleMode ? 0 : numInstruments,
668
+ len,
669
+ restart: restartPos >= len ? 0 : restartPos,
670
+ xxo: orders,
671
+ channels,
672
+ patterns,
673
+ instruments: isSampleMode ? [] : instruments,
674
+ samples: [],
675
+ num_sequences: 0,
676
+ sequences: [],
677
+ speed: defaultSpeed || 6,
678
+ bpm: defaultTempo || 125,
679
+ volbase: 0x40,
680
+ gvolbase: isIT ? 0x80 : isS3M ? 0x40 : 0x40,
681
+ gvol: isIT ? Math.min(globalVol, 128) * 2 : isS3M ? Math.min(globalVol, 64) * 4 : 0x40,
682
+ quirks,
683
+ flowMode: 0,
684
+ readEventType,
685
+ periodType,
686
+ defpan: 0x80,
687
+ time_factor: 10,
688
+ rrate: 250,
689
+ c4rate,
690
+ compare_vblank: false,
691
+ tracker: `MO3 v${version}`,
692
+ };
693
+ void sampleVolume;
694
+ // Sample chunks live in the FILE after the depacked music chunk
695
+ let filePos = version >= 5 ? 12 + compressedSize : lastConsumed();
696
+ const sampleChunks = [];
697
+ for (let smp = 0; smp < numSamples; smp++) {
698
+ const h = sampleHeaders[smp];
699
+ if (h.compressedSize > 0) {
700
+ sampleChunks.push({ data: bytes, off: filePos, size: h.compressedSize });
701
+ filePos += h.compressedSize;
702
+ }
703
+ else if (h.compressedSize === 0 && h.length > 0) {
704
+ const frames = h.length * ((h.flags & SMP_16BIT ? 2 : 1) * ((h.flags & SMP_STEREO) ? 2 : 1));
705
+ sampleChunks.push({ data: bytes, off: filePos, size: frames });
706
+ filePos += frames;
707
+ }
708
+ else {
709
+ sampleChunks.push({ data: bytes, off: 0, size: 0 });
710
+ }
711
+ }
712
+ return { mod, sampleChunks, sampleHeaders, version };
713
+ }
714
+ /** Apply OpenMPT volume command → libxmp volume column encoding. */
715
+ function applyVolCmd(m, volcmd, volParam, modType) {
716
+ const isIT = modType === 'it';
717
+ const isXm = modType === 'xm';
718
+ const volFx = (code, param) => {
719
+ // our events carry the volume column as f2t/f2p with libxmp FX codes
720
+ m.f2t = code;
721
+ m.f2p = param;
722
+ };
723
+ switch (volcmd) {
724
+ case VOLCMD_VOLUME:
725
+ m.vol = volParam + 1;
726
+ break;
727
+ case VOLCMD_PANNING:
728
+ volFx(0x08 /* FX_SETPAN */, volParam * 4);
729
+ break;
730
+ case VOLCMD_VOLSLIDEUP:
731
+ volFx(isIT ? 0xa4 : 0x0a /* volslide */, isIT ? volParam : (volParam << 4));
732
+ break;
733
+ case VOLCMD_VOLSLIDEDOWN:
734
+ volFx(isIT ? 0xa4 : 0x0a, isIT ? volParam : volParam);
735
+ break;
736
+ case VOLCMD_FINEVOLUP:
737
+ if (isXm)
738
+ volFx(0x0e /* FX_EXTENDED */, (0x0a << 4) | volParam);
739
+ else if (isIT)
740
+ volFx(0x82, volParam);
741
+ else
742
+ volFx(0x0e, (0x0a << 4) | volParam);
743
+ break;
744
+ case VOLCMD_FINEVOLDOWN:
745
+ if (isXm)
746
+ volFx(0x0e, (0x0b << 4) | volParam);
747
+ else if (isIT)
748
+ volFx(0x82, volParam);
749
+ else
750
+ volFx(0x0e, (0x0b << 4) | volParam);
751
+ break;
752
+ case VOLCMD_TONEPORTAMENTO:
753
+ volFx(0x03, isXm ? volParam << 4 : volParam);
754
+ break;
755
+ case VOLCMD_VIBRATODEPTH:
756
+ volFx(isIT ? 0xac : 0x04, volParam);
757
+ break;
758
+ case VOLCMD_VIBRATOSPEED:
759
+ volFx(0x04, volParam << 4);
760
+ break;
761
+ case VOLCMD_PANSLIDELEFT:
762
+ volFx(0xb5 /* FX_PANSL_NOMEM */, volParam << 4);
763
+ break;
764
+ case VOLCMD_PANSLIDERIGHT:
765
+ volFx(0xb5, volParam);
766
+ break;
767
+ case VOLCMD_PORTAUP:
768
+ volFx(0x01, volParam);
769
+ break;
770
+ case VOLCMD_PORTADOWN:
771
+ volFx(0x02, volParam);
772
+ break;
773
+ case VOLCMD_OFFSET:
774
+ volFx(0x09, volParam);
775
+ break;
776
+ default:
777
+ break;
778
+ }
779
+ }
780
+ // ---------------------------------------------------------------------------
781
+ // Sample data decoding (Load_mo3.cpp:1598-2039)
782
+ // ---------------------------------------------------------------------------
783
+ // UnpackMO3DeltaSample (Load_mo3.cpp:617-662)
784
+ function unpackMo3DeltaSample(data, off, dst, length, numChannels, is16) {
785
+ let dh = is16 ? 8 : 4;
786
+ let carry = 0;
787
+ let dataAcc = 0;
788
+ let pos = off;
789
+ const end = data.length;
790
+ const readCtrlBit = () => {
791
+ dataAcc = (dataAcc << 1) & 0x1ff;
792
+ carry = dataAcc > 0xff ? 1 : 0;
793
+ dataAcc &= 0xff;
794
+ if (dataAcc === 0) {
795
+ if (pos >= end)
796
+ return false;
797
+ dataAcc = data[pos++];
798
+ dataAcc = ((dataAcc << 1) + 1) & 0x1ff;
799
+ carry = dataAcc > 0xff ? 1 : 0;
800
+ dataAcc &= 0xff;
801
+ }
802
+ return true;
803
+ };
804
+ const shift = is16 ? 15 : 7;
805
+ const mask = is16 ? 0xffff : 0xff;
806
+ for (let chn = 0; chn < numChannels; chn++) {
807
+ let p = chn;
808
+ const pEnd = length * numChannels;
809
+ let previous = 0;
810
+ while (p < pEnd) {
811
+ let val = 0;
812
+ // Properties::Decode
813
+ if (is16) {
814
+ if (dh < 5) {
815
+ do {
816
+ if (!readCtrlBit())
817
+ return;
818
+ val = ((val << 1) + carry) & mask;
819
+ if (!readCtrlBit())
820
+ return;
821
+ val = ((val << 1) + carry) & mask;
822
+ if (!readCtrlBit())
823
+ return;
824
+ val = ((val << 1) + carry) & mask;
825
+ } while (carry);
826
+ }
827
+ else {
828
+ do {
829
+ if (!readCtrlBit())
830
+ return;
831
+ val = ((val << 1) + carry) & mask;
832
+ if (!readCtrlBit())
833
+ return;
834
+ val = ((val << 1) + carry) & mask;
835
+ } while (carry);
836
+ }
837
+ }
838
+ else {
839
+ do {
840
+ if (!readCtrlBit())
841
+ return;
842
+ val = ((val << 1) + carry) & mask;
843
+ if (!readCtrlBit())
844
+ return;
845
+ } while (carry);
846
+ }
847
+ let cl = dh;
848
+ while (cl > 0) {
849
+ if (!readCtrlBit())
850
+ return;
851
+ val = ((val << 1) + carry) & mask;
852
+ cl--;
853
+ }
854
+ let cl2 = 1;
855
+ if (val >= 4) {
856
+ cl2 = shift;
857
+ while (((1 << cl2) & val) === 0 && cl2 > 1)
858
+ cl2--;
859
+ }
860
+ dh = (dh + cl2) >> 1;
861
+ carry = val & 1;
862
+ val >>= 1;
863
+ if (carry === 0)
864
+ val = (~val) & mask;
865
+ val = (val + previous) & mask;
866
+ // write
867
+ const signed = is16
868
+ ? (val >= 0x8000 ? val - 0x10000 : val)
869
+ : (val >= 0x80 ? val - 0x100 : val);
870
+ dst[p] = signed;
871
+ previous = signed;
872
+ p += numChannels;
873
+ }
874
+ }
875
+ }
876
+ // UnpackMO3DeltaPredictionSample (Load_mo3.cpp:663-715)
877
+ function unpackMo3DeltaPredictionSample(data, off, dst, length, numChannels, is16) {
878
+ let dh = is16 ? 8 : 4;
879
+ let carry = 0;
880
+ let dataAcc = 0;
881
+ let pos = off;
882
+ const end = data.length;
883
+ const readCtrlBit = () => {
884
+ dataAcc = (dataAcc << 1) & 0x1ff;
885
+ carry = dataAcc > 0xff ? 1 : 0;
886
+ dataAcc &= 0xff;
887
+ if (dataAcc === 0) {
888
+ if (pos >= end)
889
+ return false;
890
+ dataAcc = data[pos++];
891
+ dataAcc = ((dataAcc << 1) + 1) & 0x1ff;
892
+ carry = dataAcc > 0xff ? 1 : 0;
893
+ dataAcc &= 0xff;
894
+ }
895
+ return true;
896
+ };
897
+ const shift = is16 ? 15 : 7;
898
+ const mask = is16 ? 0xffff : 0xff;
899
+ const minV = is16 ? -0x8000 : -0x80;
900
+ const maxV = is16 ? 0x7fff : 0x7f;
901
+ for (let chn = 0; chn < numChannels; chn++) {
902
+ let p = chn;
903
+ const pEnd = length * numChannels;
904
+ let next = 0;
905
+ let previous = 0;
906
+ while (p < pEnd) {
907
+ let val = 0;
908
+ if (is16) {
909
+ if (dh < 5) {
910
+ do {
911
+ if (!readCtrlBit())
912
+ return;
913
+ val = ((val << 1) + carry) & mask;
914
+ if (!readCtrlBit())
915
+ return;
916
+ val = ((val << 1) + carry) & mask;
917
+ if (!readCtrlBit())
918
+ return;
919
+ val = ((val << 1) + carry) & mask;
920
+ } while (carry);
921
+ }
922
+ else {
923
+ do {
924
+ if (!readCtrlBit())
925
+ return;
926
+ val = ((val << 1) + carry) & mask;
927
+ if (!readCtrlBit())
928
+ return;
929
+ val = ((val << 1) + carry) & mask;
930
+ } while (carry);
931
+ }
932
+ }
933
+ else {
934
+ do {
935
+ if (!readCtrlBit())
936
+ return;
937
+ val = ((val << 1) + carry) & mask;
938
+ if (!readCtrlBit())
939
+ return;
940
+ } while (carry);
941
+ }
942
+ let cl = dh;
943
+ while (cl > 0) {
944
+ if (!readCtrlBit())
945
+ return;
946
+ val = ((val << 1) + carry) & mask;
947
+ cl--;
948
+ }
949
+ let cl2 = 1;
950
+ if (val >= 4) {
951
+ cl2 = shift;
952
+ while (((1 << cl2) & val) === 0 && cl2 > 1)
953
+ cl2--;
954
+ }
955
+ dh = (dh + cl2) >> 1;
956
+ carry = val & 1;
957
+ val >>= 1;
958
+ if (carry === 0)
959
+ val = (~val) & mask;
960
+ const delta = val >= 0x8000 ? val - 0x10000 : val >= 0x80 ? val - 0x100 : val;
961
+ val = (val + next) & mask;
962
+ const out = val >= 0x8000 ? val - 0x10000 : val >= 0x80 ? val - 0x100 : val;
963
+ dst[p] = out;
964
+ p += numChannels;
965
+ const sval = out;
966
+ next = sval * 2 + (delta >> 1) - previous;
967
+ if (next < minV)
968
+ next = minV;
969
+ if (next > maxV)
970
+ next = maxV;
971
+ previous = sval;
972
+ }
973
+ }
974
+ }
975
+ /** Decode MP3 sample data with the minimp3 port (SampleFormatMP3 path). */
976
+ function decodeMp3Sample(data, off, size, encoderDelay, hdrLength, stereo, is16) {
977
+ const dec = {
978
+ mdctOverlap: [], qmfState: new Float32Array(0), reserv: 0,
979
+ freeFormatBytes: 0, header: new Uint8Array(4), reservBuf: new Uint8Array(0),
980
+ };
981
+ mp3decInit(dec);
982
+ const mp3 = data.subarray(off, off + size);
983
+ const pcm = new Int16Array(1152 * 2);
984
+ const info = { frameBytes: 0, channels: 0, hz: 0, layer: 0, bitrateKbps: 0 };
985
+ // LAME info frame skip (Load_mo3.cpp:2007-2023)
986
+ let mpegData = mp3;
987
+ const frame = parseMPEGFrame(mp3, 0);
988
+ let encoderDelay2 = encoderDelay;
989
+ if (frame && frame.isLAME) {
990
+ const frameDelay = frame.numSamples * 2;
991
+ if (encoderDelay >= frameDelay) {
992
+ encoderDelay2 -= frameDelay;
993
+ mpegData = mp3.subarray(frame.frameSize);
994
+ }
995
+ }
996
+ const chunks = [];
997
+ let pos = 0;
998
+ let total = 0;
999
+ let channels = 0;
1000
+ let hz = 0;
1001
+ let bytesLeft = mpegData.length;
1002
+ do {
1003
+ const n = mp3decDecodeFrame(dec, mpegData.subarray(pos), bytesLeft, pcm, info);
1004
+ if (n < 0 || info.frameBytes < 0)
1005
+ break;
1006
+ if (n > 0 && info.frameBytes === 0)
1007
+ break;
1008
+ if (n === 0 && info.frameBytes === 0)
1009
+ break;
1010
+ if (info.frameBytes > 0) {
1011
+ if (hz !== 0 && hz !== info.hz)
1012
+ break;
1013
+ if (channels !== 0 && channels !== info.channels)
1014
+ break;
1015
+ hz = info.hz;
1016
+ channels = info.channels;
1017
+ if (hz <= 0)
1018
+ break;
1019
+ if (channels !== 1 && channels !== 2)
1020
+ break;
1021
+ const advance = Math.min(Math.max(info.frameBytes, 0), bytesLeft);
1022
+ pos += advance;
1023
+ bytesLeft -= advance;
1024
+ if (n > 0) {
1025
+ chunks.push(pcm.slice(0, n * channels));
1026
+ total += n * channels;
1027
+ }
1028
+ }
1029
+ if (total / channels > 0x1000000)
1030
+ break;
1031
+ } while (bytesLeft > 0);
1032
+ if (total === 0)
1033
+ return null;
1034
+ // Concatenate + apply encoder delay
1035
+ const all = new Int16Array(total);
1036
+ let o = 0;
1037
+ for (const c of chunks) {
1038
+ all.set(c, o);
1039
+ o += c.length;
1040
+ }
1041
+ let delay = encoderDelay2;
1042
+ if (delay > 0 && delay < all.length) {
1043
+ all.copyWithin(0, delay);
1044
+ total -= delay;
1045
+ }
1046
+ const samplesPerCh = Math.min(Math.floor(total / (channels || 1)), hdrLength);
1047
+ void stereo;
1048
+ void is16;
1049
+ return { pcm: all.subarray(0, samplesPerCh * (channels || 1)), channels: channels || 1, hz };
1050
+ }
1051
+ /** MO3 sample loop flags → our SampleFlags (Load_mo3.cpp:293-299). */
1052
+ function mo3LoopFlags(flags) {
1053
+ const SMP_LOOP = 0x10, SMP_PINGPONG = 0x20, SMP_SUSTAIN = 0x100, SMP_SUSTAIN_PINGPONG = 0x200;
1054
+ return (((flags & SMP_LOOP) !== 0 ? 0x02 : 0) | // SampleFlags.LOOP
1055
+ ((flags & SMP_PINGPONG) !== 0 ? 0x02 | 0x04 : 0) | // LOOP | BIDIR
1056
+ ((flags & SMP_SUSTAIN) !== 0 ? 0x20 : 0) | // SUSTAIN
1057
+ ((flags & SMP_SUSTAIN_PINGPONG) !== 0 ? 0x20 | 0x40 : 0) // SUSTAIN | SUSTAIN_BIDIR
1058
+ );
1059
+ }
1060
+ export function mo3Load(bytes, ctx) {
1061
+ const fail = (msg) => {
1062
+ throw new ParseError(msg);
1063
+ };
1064
+ const parsed = parseMO3(bytes, fail);
1065
+ const { mod, sampleChunks, sampleHeaders, version } = parsed;
1066
+ const isXm = mod.readEventType === ReadEventType.FT2; // XM semantics (mod.format = 'mo3')
1067
+ // Sample data → RawSample (Load_mo3.cpp:1598-2039)
1068
+ const rawSamples = [];
1069
+ for (let smp = 0; smp < sampleHeaders.length; smp++) {
1070
+ const h = sampleHeaders[smp];
1071
+ const chunk = sampleChunks[smp];
1072
+ const stereo = (h.flags & SMP_STEREO) !== 0;
1073
+ const is16 = (h.flags & SMP_16BIT) !== 0;
1074
+ const numChannels = stereo ? 2 : 1;
1075
+ const compression = h.flags & SMP_COMPRESSION_MASK;
1076
+ let raw;
1077
+ if (h.compressedSize < 0 && smp + h.compressedSize > 0) {
1078
+ // Duplicate sample: handled after registration by copying data ref
1079
+ raw = {
1080
+ name: '', data: new Uint8Array(0), length: 0,
1081
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1082
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1083
+ finetune: 0, volume: Math.min(h.volume, 64), flags: mo3LoopFlags(h.flags), c5spd: C4_NTSC_RATE,
1084
+ };
1085
+ }
1086
+ else if (h.length === 0 || chunk.size === 0) {
1087
+ raw = {
1088
+ name: '', data: new Uint8Array(0), length: 0,
1089
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1090
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1091
+ finetune: 0, volume: Math.min(h.volume, 64), flags: mo3LoopFlags(h.flags), c5spd: C4_NTSC_RATE,
1092
+ };
1093
+ }
1094
+ else if (compression === 0 && h.compressedSize === 0) {
1095
+ // Uncompressed (signed PCM)
1096
+ const frames = h.length;
1097
+ const byteLen = frames * (is16 ? 2 : 1) * numChannels;
1098
+ raw = {
1099
+ name: '', data: bytes.subarray(chunk.off, chunk.off + byteLen),
1100
+ length: frames, loopStart: h.loopStart, loopEnd: h.loopEnd,
1101
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1102
+ finetune: 0, volume: Math.min(h.volume, 64),
1103
+ flags: mo3LoopFlags(h.flags) | (is16 ? 0x01 : 0) | (stereo ? 0x80 : 0),
1104
+ c5spd: C4_NTSC_RATE,
1105
+ };
1106
+ }
1107
+ else if (compression === SMP_DELTA_COMPRESSION || compression === SMP_DELTA_PREDICTION) {
1108
+ const total = h.length * numChannels;
1109
+ const buf = is16 ? new Int16Array(total) : new Int8Array(total);
1110
+ if (compression === SMP_DELTA_COMPRESSION) {
1111
+ unpackMo3DeltaSample(chunk.data, chunk.off, buf, h.length, numChannels, is16);
1112
+ }
1113
+ else {
1114
+ unpackMo3DeltaPredictionSample(chunk.data, chunk.off, buf, h.length, numChannels, is16);
1115
+ }
1116
+ // raw bytes = little-endian signed PCM
1117
+ const byteLen = total * (is16 ? 2 : 1);
1118
+ const out = new Uint8Array(byteLen);
1119
+ const dv = new DataView(out.buffer);
1120
+ for (let i = 0; i < total; i++) {
1121
+ if (is16)
1122
+ dv.setInt16(i * 2, buf[i], true);
1123
+ else
1124
+ out[i] = buf[i] & 0xff;
1125
+ }
1126
+ raw = {
1127
+ name: '', data: out, length: h.length,
1128
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1129
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1130
+ finetune: 0, volume: Math.min(h.volume, 64),
1131
+ flags: mo3LoopFlags(h.flags) | (is16 ? 0x01 : 0),
1132
+ c5spd: C4_NTSC_RATE,
1133
+ };
1134
+ if (stereo)
1135
+ raw.flags |= 0x80;
1136
+ }
1137
+ else if (compression === SMP_COMPRESSION_MPEG) {
1138
+ const res = decodeMp3Sample(chunk.data, chunk.off, chunk.size, h.encoderDelay, h.length, stereo, is16);
1139
+ if (!res) {
1140
+ raw = {
1141
+ name: '', data: new Uint8Array(0), length: 0,
1142
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1143
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1144
+ finetune: 0, volume: Math.min(h.volume, 64), flags: mo3LoopFlags(h.flags), c5spd: C4_NTSC_RATE,
1145
+ };
1146
+ }
1147
+ else {
1148
+ const ch = res.channels;
1149
+ const bytesPer = 2;
1150
+ const out = new Uint8Array(res.pcm.length * bytesPer);
1151
+ const dv = new DataView(out.buffer);
1152
+ for (let i = 0; i < res.pcm.length; i++)
1153
+ dv.setInt16(i * 2, res.pcm[i], true);
1154
+ raw = {
1155
+ name: '', data: out,
1156
+ length: Math.floor(res.pcm.length / ch),
1157
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1158
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1159
+ finetune: 0, volume: Math.min(h.volume, 64),
1160
+ flags: mo3LoopFlags(h.flags) | 0x01 | (ch === 2 ? 0x80 : 0),
1161
+ c5spd: res.hz,
1162
+ };
1163
+ }
1164
+ }
1165
+ else if (compression === SMP_COMPRESSION_OGG || compression === SMP_SHARED_OGG) {
1166
+ // Ogg Vorbis sample (Load_mo3.cpp:1688-1990). For shared headers
1167
+ // (MO3 v5) the vorbis identification/comment/setup pages live in
1168
+ // another sample's chunk; remux them with this chunk's serials and
1169
+ // stitch the two buffers together (C optimized path).
1170
+ let merged = null;
1171
+ let sharedHeader = false;
1172
+ if (compression === SMP_SHARED_OGG) {
1173
+ const sharedHeaderSize = h.encoderDelay;
1174
+ const sharedOggHeader = smp + h.sharedOggHeader > 0 ? smp + h.sharedOggHeader : smp;
1175
+ sharedHeader =
1176
+ sharedOggHeader !== smp && sharedOggHeader > 0 && sharedOggHeader <= sampleHeaders.length && sharedHeaderSize > 0;
1177
+ if (sharedHeader) {
1178
+ const head = sampleChunks[sharedOggHeader - 1];
1179
+ merged = mergeSharedOggHeader(head.data.subarray(head.off, head.off + sharedHeaderSize), sharedHeaderSize, chunk.data, chunk.off, chunk.size);
1180
+ }
1181
+ }
1182
+ if (!merged) {
1183
+ merged = chunk.data.subarray(chunk.off, chunk.off + chunk.size);
1184
+ }
1185
+ const pcm = decodeOggStream(merged, h.length, numChannels);
1186
+ if (!pcm || pcm.length === 0) {
1187
+ raw = {
1188
+ name: '', data: new Uint8Array(0), length: 0,
1189
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1190
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1191
+ finetune: 0, volume: Math.min(h.volume, 64), flags: mo3LoopFlags(h.flags), c5spd: C4_NTSC_RATE,
1192
+ };
1193
+ }
1194
+ else {
1195
+ // Interleaved float → 16-bit little-endian PCM (CopyAudio).
1196
+ const frames = Math.floor(pcm.length / numChannels);
1197
+ const out = new Uint8Array(pcm.length * 2);
1198
+ const dv = new DataView(out.buffer);
1199
+ for (let i = 0; i < pcm.length; i++) {
1200
+ // Convert<int16, somefloat32> (SampleConvert.hpp:414-424):
1201
+ // clamp, *32768, round-half-away, saturate.
1202
+ let v = pcm[i];
1203
+ v = v < -1 ? -1 : v > 1 ? 1 : v;
1204
+ v *= 32768;
1205
+ const r = v >= 0 ? Math.floor(v + 0.5) : Math.ceil(v - 0.5);
1206
+ dv.setInt16(i * 2, Math.max(-32768, Math.min(32767, r)), true);
1207
+ }
1208
+ raw = {
1209
+ name: '', data: out, length: frames,
1210
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1211
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1212
+ finetune: 0, volume: Math.min(h.volume, 64),
1213
+ flags: mo3LoopFlags(h.flags) | 0x01 | (stereo ? 0x80 : 0),
1214
+ c5spd: C4_NTSC_RATE,
1215
+ };
1216
+ }
1217
+ }
1218
+ else {
1219
+ // OPL / unsupported — treat as empty
1220
+ raw = {
1221
+ name: '', data: new Uint8Array(0), length: 0,
1222
+ loopStart: h.loopStart, loopEnd: h.loopEnd,
1223
+ sustainStart: h.sustainStart, sustainEnd: h.sustainEnd,
1224
+ finetune: 0, volume: Math.min(h.volume, 64), flags: mo3LoopFlags(h.flags), c5spd: C4_NTSC_RATE,
1225
+ };
1226
+ }
1227
+ // XM sample fields: finetune/transpose (Load_mo3.cpp:305-312)
1228
+ if (isXm) {
1229
+ raw.finetune = ((h.finetune - 128) << 24) >> 24;
1230
+ raw.flags |= 0; // transpose handled via sub.xpo
1231
+ }
1232
+ ctx.addSample(raw);
1233
+ rawSamples.push(raw);
1234
+ }
1235
+ mod.samples = rawSamples;
1236
+ // Wire instruments to samples + per-sample fields (XM semantics).
1237
+ // map values are 0-based SAMPLE ids; subs are built per distinct sample
1238
+ // and map[note] rewritten to the SUB index (libxmp xxi.map[k].ins
1239
+ // semantics — getSubinstrument(mod, ins, map[k])).
1240
+ if (isXm) {
1241
+ for (const ins of mod.instruments) {
1242
+ if (ins.sub.length !== 0)
1243
+ continue;
1244
+ const used = new Set();
1245
+ for (let k = 12; k < 108; k++) {
1246
+ const sid = ins.map[k];
1247
+ if (sid !== 0xff && sid !== 0)
1248
+ used.add(sid);
1249
+ }
1250
+ const subIndexBySid = new Map();
1251
+ let n = 0;
1252
+ for (const sid of used) {
1253
+ const h = sampleHeaders[sid];
1254
+ if (!h)
1255
+ continue;
1256
+ ins.sub.push({
1257
+ vol: Math.min(h.volume, 64),
1258
+ gvl: 0x40,
1259
+ pan: h.panning <= 256 ? h.panning : -1,
1260
+ xpo: h.transpose,
1261
+ fin: ((h.finetune - 128) << 24) >> 24,
1262
+ vwf: 0, vde: 0, vra: 0, vsw: 0,
1263
+ sid,
1264
+ rvv: 0, nna: 0, dct: 0, dca: 0, ifc: 0, ifr: 0,
1265
+ });
1266
+ subIndexBySid.set(sid, n);
1267
+ n++;
1268
+ }
1269
+ if (n === 0) {
1270
+ ins.sub.push({
1271
+ vol: 0, gvl: 0x40, pan: -1, xpo: 0, fin: 0, vwf: 0, vde: 0, vra: 0, vsw: 0,
1272
+ sid: 0, rvv: 0, nna: 0, dct: 0, dca: 0, ifc: 0, ifr: 0,
1273
+ });
1274
+ }
1275
+ else {
1276
+ for (let k = 12; k < 108; k++) {
1277
+ const sid = ins.map[k];
1278
+ ins.map[k] = subIndexBySid.get(sid) ?? 0xff;
1279
+ }
1280
+ }
1281
+ ins.nsm = ins.sub.length;
1282
+ }
1283
+ }
1284
+ void version;
1285
+ return mod;
1286
+ }
1287
+ export const plugin = {
1288
+ name: 'mo3',
1289
+ test: mo3Test,
1290
+ load: mo3Load,
1291
+ readEvent(core, chn, row) {
1292
+ // dispatch through the inner format's reader via readEventType
1293
+ const mod = core.module;
1294
+ void mod;
1295
+ void chn;
1296
+ void row;
1297
+ // The core dispatches on mod.readEventType; we reuse the shared readers
1298
+ // by deferring to the core's dispatch (same as fmt-mod's dispatch).
1299
+ const e = core.readEventScratch(chn) ?? core.readEventAt(mod.xxo[core.ctx.p.ord] ?? 0, chn, row);
1300
+ void e;
1301
+ // Call the appropriate reader via the registry: simplest is to import
1302
+ // the shared dispatch from fmt-mod at registration time.
1303
+ dispatchReadEvent(core, chn, row);
1304
+ },
1305
+ };
1306
+ import { readEvent as fmtModReadEvent } from '@modplayjs/fmt-mod';
1307
+ function dispatchReadEvent(core, chn, row) {
1308
+ fmtModReadEvent(core, chn, row);
1309
+ }
1310
+ //# sourceMappingURL=mo3.js.map