All files / src/shared/lib/uge/mod2uge parseMod.ts

100% Statements 63/63
100% Branches 3/3
100% Functions 4/4
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                3x 3x 3x           3x 21x   21x 21x   21x 21x 651x 651x 651x 651x 651x 651x 651x 651x 651x 651x 651x                   21x 21x 21x 21x 2688x   21x 21x   21x 21x 2688x     21x 21x   21x 74x 74x 4736x 4736x 18944x 18944x   4736x   74x     21x                       3x 18944x 18944x 18944x 18944x   18944x 18944x 18944x 18944x   18944x                   3x         672x 672x               3x 18944x    
import type {
  MODFile,
  MODSample,
  MODCell,
  MODPattern,
  EffectCode,
} from "./types";
 
const SAMPLE_COUNT = 31;
const ROWS_PER_PATTERN = 64;
const CHANNELS = 4;
 
/**
 * Parse a raw MOD binary buffer into a typed MODFile structure.
 * MOD format uses big-endian for multi-byte values.
 */
export function parseMod(buf: Buffer): MODFile {
  let offset = 0;
 
  const name = readFixedString(buf, offset, 20);
  offset += 20;
 
  const samples: MODSample[] = [];
  for (let i = 0; i < SAMPLE_COUNT; i++) {
    const sampleName = readFixedString(buf, offset, 22);
    offset += 22;
    const sampleLength = buf.readUInt16BE(offset);
    offset += 2;
    const finetune = buf.readUInt8(offset++);
    const volume = buf.readUInt8(offset++);
    const repeatPoint = buf.readUInt16BE(offset);
    offset += 2;
    const repeatLength = buf.readUInt16BE(offset);
    offset += 2;
    samples.push({
      name: sampleName,
      sampleLength,
      finetune,
      volume,
      repeatPoint,
      repeatLength,
    });
  }
 
  const songLen = buf.readUInt8(offset++);
  const _numPatternsMagic = buf.readUInt8(offset++); // unused
  const positions: number[] = [];
  for (let i = 0; i < 128; i++) {
    positions.push(buf.readUInt8(offset++));
  }
  const mkMagic = buf.subarray(offset, offset + 4).toString("ascii");
  offset += 4;
 
  let maxOrder = 0;
  for (const p of positions) {
    if (p > maxOrder) maxOrder = p;
  }
 
  const patternCount = maxOrder + 1;
  const patterns: MODPattern[] = [];
 
  for (let i = 0; i < patternCount; i++) {
    const pattern: MODPattern = [];
    for (let row = 0; row < ROWS_PER_PATTERN; row++) {
      const rowData: MODCell[] = [];
      for (let ch = 0; ch < CHANNELS; ch++) {
        rowData.push(parseModCell(buf, offset));
        offset += 4;
      }
      pattern.push(rowData);
    }
    patterns.push(pattern);
  }
 
  return { name, samples, songLen, positions, mkMagic, patterns };
}
 
/**
 * Convert 4 raw MOD bytes into a parsed MODCell.
 *
 * MOD pattern cell layout:
 *   byte[0]: IIIINNNN  (instrument high nibble | note period bits 8–11)
 *   byte[1]: NNNNNNNN  (note period bits 0–7)
 *   byte[2]: IIIIEEEE  (instrument low nibble | effect code)
 *   byte[3]: PPPPPPPP  (effect parameters)
 */
const parseModCell = (buf: Buffer, offset: number): MODCell => {
  const b0 = buf[offset];
  const b1 = buf[offset + 1];
  const b2 = buf[offset + 2];
  const b3 = buf[offset + 3];
 
  const note = ((b0 & 0x0f) << 8) | b1;
  const instrument = (b0 & 0xf0) | ((b2 & 0xf0) >> 4);
  const effectCode = toEffectCode(b2);
  const effectParams = b3;
 
  return {
    note,
    instrument,
    effect: { code: effectCode, params: effectParams },
  };
};
 
/**
 * Read a fixed-length ASCII string from the buffer, stopping at the first null byte.
 */
const readFixedString = (
  buf: Buffer,
  offset: number,
  length: number,
): string => {
  const end = buf.subarray(offset, offset + length).indexOf(0);
  return buf
    .subarray(offset, offset + (end === -1 ? length : end))
    .toString("ascii");
};
 
/**
 * Convert a MOD effect code to be strongly typed as EffectCode (0-F).
 */
const toEffectCode = (code: number): EffectCode => {
  return (code & 0x0f) as EffectCode;
};