@modplayjs/fmt-mo3 0.0.0-stage → 0.2.2
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/README.md +38 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/minimp3.d.ts +18 -0
- package/dist/minimp3.d.ts.map +1 -0
- package/dist/minimp3.js +1068 -0
- package/dist/minimp3.js.map +1 -0
- package/dist/mo3.d.ts +5 -0
- package/dist/mo3.d.ts.map +1 -0
- package/dist/mo3.js +1310 -0
- package/dist/mo3.js.map +1 -0
- package/dist/mo3LZ.d.ts +4 -0
- package/dist/mo3LZ.d.ts.map +1 -0
- package/dist/mo3LZ.js +176 -0
- package/dist/mo3LZ.js.map +1 -0
- package/dist/mp3tables.d.ts +22 -0
- package/dist/mp3tables.d.ts.map +1 -0
- package/dist/mp3tables.js +61 -0
- package/dist/mp3tables.js.map +1 -0
- package/dist/mpegFrame.d.ts +10 -0
- package/dist/mpegFrame.d.ts.map +1 -0
- package/dist/mpegFrame.js +80 -0
- package/dist/mpegFrame.js.map +1 -0
- package/dist/ogg.d.ts +12 -0
- package/dist/ogg.d.ts.map +1 -0
- package/dist/ogg.js +206 -0
- package/dist/ogg.js.map +1 -0
- package/package.json +52 -4
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
|