UNPKG

tinyvgm

Version:
321 lines (259 loc) 8.9 kB
import { VGM_CMD_LENGTH_TABLE, TinyVGMHeaderField, TinyVGMMetadataType, } from './header.js' const HEADER_IDENTITY = TinyVGMHeaderField.Identity * 4 const HEADER_EOF_OFFSET = TinyVGMHeaderField.EoF_Offset * 4 const HEADER_VER_OFFSET = TinyVGMHeaderField.Version * 4 const HEADER_GD3_OFFSET = TinyVGMHeaderField.GD3_Offset * 4 const HEADER_DAT_OFFSET = TinyVGMHeaderField.Data_Offset * 4 const HEADER_LOOP_OFFSET = TinyVGMHeaderField.Loop_Offset * 4 const HEADER_LOOP_SAMPLES = TinyVGMHeaderField.Loop_Samples * 4 const HEADER_TOTAL_SAMPLES = TinyVGMHeaderField.Total_Samples * 4 const HEADER_EXTRA_OFFSET = TinyVGMHeaderField.ExtraHeader_Offset * 4 let _print = (msg) => console.log(msg) export const setPrint = (fn) => { _print = fn } const parseHeader = function *(view, end) { let type = TinyVGMHeaderField.Identity while (type < end) { const data = view.getUint32(type * 4, true) const ret = { type, data } yield ret type += 1 } } const parseExtraHeader = function (view, startOffset) { const size = view.getUint32(startOffset, true) const clock = [] const volume = [] if (size >= 8) { const clockRelOffset = view.getUint32(startOffset + 4, true) if (clockRelOffset >= 4) { const count = view.getUint8(startOffset + 4 + clockRelOffset) if (count) { const baseOffset = startOffset + 4 + clockRelOffset + 1 for (let i = 0; i < count; i++) { const itemOffset = baseOffset + i * 5 const chipID = view.getUint8(itemOffset) const clk = view.getUint32(itemOffset + 1, true) clock.push({ chipID, clk }) } } } if (size >= 12 && (clockRelOffset && clockRelOffset >= 8) || !clockRelOffset) { const volRelOffset = view.getUint32(startOffset + 8, true) if (volRelOffset >= 4) { const count = view.getUint8(startOffset + 8 + clockRelOffset) if (count) { const baseOffset = startOffset + 8 + clockRelOffset + 1 // eslint-disable-next-line max-depth for (let i = 0; i < count; i++) { const itemOffset = baseOffset + i * 4 const chipID = view.getUint8(itemOffset) const flags = view.getUint8(itemOffset + 1) const volRaw = view.getUint16(itemOffset + 2, true) const relative = !!(volRaw & 0x8000) const vol = volRaw & 0x7FFF volume.push({ chipID, flags, relative, vol }) } } } } } return { clock, volume } } const parseMetadata = function *(view, startOffset) { let cursor = startOffset if (view.getUint32(cursor, true) !== 0x20336447) { throw new TypeError(`Invalid GD3 Identity: 0x${view.getUint32(cursor).toString(16)}`) } cursor += 4 const version = view.getUint32(cursor, true) _print(`GD3 version: 0x${version.toString(16).padStart(8, '0')}`) cursor += 4 const length = view.getUint32(cursor, true) if (!length) return cursor += 4 const end = cursor + length let type = TinyVGMMetadataType.Title_EN while (cursor < end) { if (type >= TinyVGMMetadataType.MAX) { if (view.getUint16(cursor, true)) throw new TypeError(`Unkonwn VGM GD3 metadata type: 0x${type.toString(16).padStart(2, '0')}`) return } let readLength = 0 while (view.getUint16(cursor + readLength, true)) { readLength += 2 } const ret = { type, data: new Uint8Array(view.buffer, cursor, readLength) } yield ret cursor += readLength + 2 type += 1 } } // eslint-disable-next-line complexity const parseCommands = function *(view, { commandOffset, eofOffset, totalSamples, loopOffset, loopSamples }, ctx) { let cursor = commandOffset let samplesPlayed = 0 let loopSamplesPlayed = 0 while (cursor < eofOffset && samplesPlayed < totalSamples) { const cmd = view.getUint8(cursor) if (cmd === 0x66) return const cmdLength = VGM_CMD_LENGTH_TABLE[cmd] if (cmdLength === -1) { _print('play', samplesPlayed, loopSamplesPlayed, loopSamples) const errorMsg = `Unknown VGM command 0x${cmd.toString(16).padStart(2, '0')} at 0x${cursor.toString(16)}` if (ctx.skipUnknownCommand) { cursor += 1 _print(`${errorMsg}, skipped`) // eslint-disable-next-line no-continue continue } else { throw new TypeError(errorMsg) } } const _cursor = cursor if (cmd === 0x67) { cursor += 7 if (cursor >= eofOffset) throw new RangeError('Unexpected end of VGM datablock') const type = view.getUint8(cursor - 5) const dataLength = view.getUint32(cursor - 4, true) const cursorNext = cursor + dataLength if (cursorNext >= eofOffset) throw new RangeError('Unexpected end of VGM datablock') const data = new Uint8Array(view.buffer, _cursor, dataLength + 7) const ret = { cmd, type, data } yield ret cursor = cursorNext } else { const ret = { cmd } if (cmdLength > 0) { const cursorNext = cursor + 1 + cmdLength if (cursorNext >= eofOffset) throw new RangeError('Unexpected end of VGM commands') const data = new Uint8Array(view.buffer, cursor + 1, cmdLength) ret.data = data cursor = cursorNext } else { cursor += 1 } let sampleIncrement = 0 switch (cmd) { case 0x61: { sampleIncrement = ret.data[0] | (ret.data[1] << 8) break } case 0x62: { sampleIncrement = 735 break } case 0x63: { sampleIncrement = 882 break } default: { // eslint-disable-next-line max-depth if (cmd >= 0x70 && cmd < 0x90) { sampleIncrement = (cmd & 0x0F) + 1 } } } if (sampleIncrement) { samplesPlayed += sampleIncrement ret.sampleIncrement = sampleIncrement if (loopSamples && _cursor >= loopOffset) { loopSamplesPlayed += sampleIncrement // eslint-disable-next-line max-depth if (ctx.loopCount > 0 && loopSamplesPlayed >= loopSamples && loopSamples) { samplesPlayed -= loopSamplesPlayed loopSamplesPlayed = 0 cursor = loopOffset ctx.loopCount -= 1 // eslint-disable-next-line max-depth if (ctx.onLoop) ctx.onLoop(ctx.loopCount) } } } ctx.samplesPlayed = samplesPlayed ctx.loopSamplesPlayed = loopSamplesPlayed yield ret } } } const toDataView = (buf) => { if (buf instanceof DataView) return return buf.buffer ? new DataView(buf.buffer, buf.byteOffset, buf.byteOffset + buf.byteLength) : new DataView(buf) } export const parseVGM = (buf, options = {}) => { const vgmView = toDataView(buf) if (vgmView.getUint32(HEADER_IDENTITY, true) !== 0x206d6756) { throw new TypeError('Invalid VGM Identity') } if (buf.byteLength < 0x40) { throw new RangeError('Corrupted VGM file') } const eofOffset = vgmView.getUint32(HEADER_EOF_OFFSET, true) + HEADER_EOF_OFFSET const version = vgmView.getUint32(HEADER_VER_OFFSET, true) let headerSize = TinyVGMHeaderField.MAX if (version < 0x00000101) { headerSize = TinyVGMHeaderField.Rate } else if (version < 0x00000110) { headerSize = TinyVGMHeaderField.YM2612_Clock } else if (version < 0x00000150) { headerSize = TinyVGMHeaderField.Data_Offset } else if (version < 0x00000151) { headerSize = TinyVGMHeaderField.SegaPCM_Clock } const gd3Offset = vgmView.getUint32(HEADER_GD3_OFFSET, true) + HEADER_GD3_OFFSET const totalSamples = vgmView.getUint32(HEADER_TOTAL_SAMPLES, true) let loopOffset = vgmView.getUint32(HEADER_LOOP_OFFSET, true) + HEADER_LOOP_OFFSET // Handle malformed loop offset for files from some VGM generators if (loopOffset >= eofOffset) loopOffset = 0 const hasLoop = loopOffset - HEADER_LOOP_OFFSET > 0 let loopSamples = vgmView.getUint32(HEADER_LOOP_SAMPLES, true) let commandOffset = 0x40 if (version >= 0x0150) { commandOffset = vgmView.getUint32(HEADER_DAT_OFFSET, true) + HEADER_DAT_OFFSET if (commandOffset < 0x40) throw RangeError('Invalid VGM data offset') } if (commandOffset < headerSize * 4) { headerSize = Math.floor(commandOffset / 4) } let extraHeaderOffset = 0 if (version >= 0x00000170 && headerSize > TinyVGMHeaderField.ExtraHeader_Offset) { extraHeaderOffset = vgmView.getUint32(HEADER_EXTRA_OFFSET, true) + HEADER_EXTRA_OFFSET } const ctx = { version, loopCount: 0, hasLoop, totalSamples, loopSamples, samplesPlayed: 0, loopSamplesPlayed: 0, skipUnknownCommand: false, ...options } // Invalid or no loop if (loopOffset < commandOffset) { if (hasLoop) _print(`Invalid loop offset 0x${loopOffset.toString(16)}! Changing to data offset...`) loopOffset = commandOffset loopSamples = totalSamples } if (!loopSamples) { loopSamples = totalSamples } ctx.header = () => parseHeader(vgmView, headerSize) ctx.extraHeader = extraHeaderOffset && (() => parseExtraHeader(vgmView, extraHeaderOffset)) || null ctx.metadata = gd3Offset && (() => parseMetadata(vgmView, gd3Offset)) || null ctx.commands = (loops = ctx.loopCount) => { ctx.loopCount = loops return parseCommands(vgmView, { commandOffset, eofOffset, totalSamples, loopOffset, loopSamples }, ctx) } return ctx } export * from './header.js'