UNPKG

furnace-module-interface

Version:

An interface for loading, processing and saving furnace modules in an object-oriented manner. Fully typed via TypeScript.

436 lines (435 loc) 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FurnaceLoopModality = exports.FurnacePitchMode = exports.FurnaceInfo = void 0; const channels_1 = require("./channels"); const chips_1 = require("./chips"); const defaults_1 = require("./defaults"); const instrument_1 = require("./instrument"); const pattern_1 = require("./pattern"); const sample_1 = require("./sample"); const song_1 = require("./song"); const wavetable_1 = require("./wavetable"); class FurnaceInfo { constructor() { this.songs = [song_1.FurnaceSong.create(),]; this.systems = []; this.metadata = { songName: defaults_1.defaults.moduleName, songAuthor: defaults_1.defaults.author, systemName: defaults_1.defaults.systemName, gameName: defaults_1.defaults.gameName, }; this.metadataJP = { songName: defaults_1.defaults.moduleName, songAuthor: defaults_1.defaults.author, systemName: defaults_1.defaults.systemName, gameName: defaults_1.defaults.gameName, }; this.A4Tuning = defaults_1.defaults.A4Tuning; this.flags = Object.assign({}, defaults_1.defaultFlags); this.instruments = []; this.wavetables = []; this.samples = []; this.masterVolume = defaults_1.defaults.masterVolume; this.comment = defaults_1.defaults.moduleComment; } static create() { return new FurnaceInfo(); } static fromHelper(helper, version) { const info = FurnaceInfo.create(); if (FurnaceInfo.magic !== helper.getASCII(4)) { throw new Error("Expected format magic string \"" + FurnaceInfo.magic + "\" when loading the info block."); } helper.skipBytes(4); info.songs[0].timebase = helper.readUInt8() + 1; info.songs[0].speed1 = helper.readUInt8(); info.songs[0].speed2 = helper.readUInt8(); info.songs[0].initialArpeggioTime = helper.readUInt8(); info.songs[0].ticksPerSecond = helper.readFloat32LE(); info.songs[0].rows = helper.readUInt16LE(); const numOrders = helper.readUInt16LE(); if (info.songs[0].rows > 256) { throw new Error("Expected at most 256 rows in a pattern, but " + info.songs[0].rows + " were loaded."); } if (numOrders > (version < 80 ? 127 : 256)) { throw new Error("Expected at most " + (version < 80 ? 127 : 256) + " orders in the main song, but " + numOrders + " were loaded."); } info.songs[0].highlightA = helper.readUInt8(); info.songs[0].highlightB = helper.readUInt8(); const numInstruments = helper.readUInt16LE(); const numWavetables = helper.readUInt16LE(); const numSamples = helper.readUInt16LE(); const numPatterns = helper.readUInt32LE(); if (numInstruments > 256) { throw new Error("Expected at most 256 instruments in module, but " + numInstruments + " were loaded."); } if (numWavetables > 256) { throw new Error("Expected at most 256 wavetables in module, but " + numWavetables + " were loaded."); } if (numSamples > 256) { throw new Error("Expected at most 256 samples in module, but " + numSamples + " were loaded."); } const tempChips = new Array(32); let pos = 0; for (let i = 0; i < 32; i++) { const system = (0, chips_1.loadSystem)(helper.readUInt8()); if (system === null) { helper.skipBytes(32 - 1 - i); break; } info.systems[i] = system; for (const chip of system.chips) { if (pos >= 32) { throw new Error("Expected at most 32 chips in module!"); } tempChips[pos++] = chip; } } for (let i = 0; i < 32; i++) { const volume = helper.readUInt8(); if (tempChips[i]) { tempChips[i].volume = volume; } } for (let i = 0; i < 32; i++) { const panning = helper.readUInt8(); if (tempChips[i]) { tempChips[i].panning = panning; } } for (let i = 0; i < 32; i++) { const flags = helper.readUInt32LE(); if (tempChips[i]) { tempChips[i].flags = flags; } } info.metadata.songName = helper.readString("utf-8"); info.metadata.songAuthor = helper.readString("utf-8"); info.A4Tuning = helper.readFloat32LE(); const loadValue = (minver) => { const value = helper.readUInt8(); if (version < minver) { return undefined; } return value; }; const loadFlag = (minver) => { const value = helper.readUInt8(); if (version < minver) { return undefined; } return value !== 0; }; info.flags.limitSlides = loadFlag(36); info.flags.pitchMode = loadValue(36); info.flags.loopModality = loadValue(36); info.flags.properNoiseLayout = loadFlag(42); info.flags.waveDutyIsVolume = loadFlag(42); info.flags.resetMacroOnPorta = loadFlag(45); info.flags.legacyVolumeSlides = loadFlag(45); info.flags.compatibleArpeggio = loadFlag(45); info.flags.noteOffResetsSlides = loadFlag(45); info.flags.targetResetsSlides = loadFlag(45); info.flags.arpeggioInhibitsPortamento = loadFlag(47); info.flags.wackAlgorithmMacro = loadFlag(47); info.flags.brokenShortcutSlides = loadFlag(49); info.flags.ignoreDuplicateSlids = loadFlag(50); info.flags.stopPortamentoOnNoteOff = loadFlag(62); info.flags.continuousVibrato = loadFlag(62); info.flags.brokenDacMode = loadFlag(64); info.flags.oneTickCut = loadFlag(65); info.flags.instrumentChangeDuringPorta = loadFlag(66); info.flags.resetNoteBaseOnArpeggioEffectStop = loadFlag(69); const insPtrs = []; const wavePtrs = []; const sampPtrs = []; const patPtrs = []; for (let i = 0; i < numInstruments; i++) { insPtrs.push(helper.readUInt32LE()); } for (let i = 0; i < numWavetables; i++) { wavePtrs.push(helper.readUInt32LE()); } for (let i = 0; i < numSamples; i++) { sampPtrs.push(helper.readUInt32LE()); } for (let i = 0; i < numPatterns; i++) { patPtrs.push(helper.readUInt32LE()); } info.songs[0].fetchChannels(helper, version, (0, channels_1.computeChannelCount)(info), numOrders); info.comment = helper.readString("utf-8"); info.masterVolume = version < 59 ? 2.0 : helper.readFloat32LE(); if (version >= 70) { info.flags.brokenSpeedSelection = loadFlag(70); info.flags.noSlideOnFirstTick = loadFlag(71); info.flags.nextRowResetArpPos = loadFlag(71); info.flags.ignoreJumpAtEnd = loadFlag(71); info.flags.buggyPortaAfterSlide = loadFlag(72); info.flags.newInsAffectsEnvelope = loadFlag(72); info.flags.extChStateIsShared = loadFlag(78); info.flags.ignoreDacOutsideOfCh = loadFlag(83); info.flags.E1xxE2xxPriorityOverSlide00 = loadFlag(83); info.flags.newSegaPCM = loadFlag(84); info.flags.weirdFnumSlides = loadFlag(85); info.flags.SNDutyMacroAlwaysResetsPhase = loadFlag(86); info.flags.pitchMacroIsLinear = loadFlag(90); info.flags.pitchSlideSpeedInFullLinear = loadValue(94); info.flags.oldFMOctaveBehavior = loadFlag(97); info.flags.disableDACVolume = loadFlag(98); info.flags.newVolumeScaling = loadFlag(99); info.flags.volumeMacroIsHeld = loadFlag(99); info.flags.brokenOutVol = loadFlag(99); info.flags.slideStopsOnRepeatedNote = loadFlag(100); info.flags.brokenPortaPositionAfterArp = loadFlag(101); helper.skipBytes(7); } if (version < 96) { helper.skipBytes(4); } else { info.songs[0].virtualTempoNumerator = helper.readUInt16LE(); info.songs[0].virtualTempoDenominator = helper.readUInt16LE(); } if (version >= 95) { info.songs[0].name = helper.readString("utf-8"); info.songs[0].comment = helper.readString("utf-8"); const subCount = helper.readUInt8(); helper.skipBytes(3); const subPtrs = []; for (let i = 0; i < subCount; i++) { subPtrs.push(helper.readUInt32LE()); } info.songs.push(...subPtrs.map((position) => { const pos = helper.getPosition(0); helper.setPosition(position); const song = song_1.FurnaceSong.fromHelper(helper, info, version); helper.setPosition(pos); return song; })); } else { info.songs[0].name = info.metadata.songName; info.songs[0].comment = info.comment; } if (version >= 103) { info.metadata.systemName = helper.readString("utf-8"); info.metadata.gameName = helper.readString("utf-8"); info.metadataJP.songName = helper.readString("utf-8"); info.metadataJP.songAuthor = helper.readString("utf-8"); info.metadataJP.systemName = helper.readString("utf-8"); info.metadataJP.gameName = helper.readString("utf-8"); } info.instruments = insPtrs.map((position) => { const pos = helper.getPosition(0); helper.setPosition(position); const ins = instrument_1.FurnaceInstrument.fromHelper(helper); helper.setPosition(pos); return ins; }); info.wavetables = wavePtrs.map((position) => { const pos = helper.getPosition(0); helper.setPosition(position); const ins = wavetable_1.FurnaceWavetable.fromHelper(helper); helper.setPosition(pos); return ins; }); info.samples = sampPtrs.map((position) => { const pos = helper.getPosition(0); helper.setPosition(position); const ins = sample_1.FurnaceSample.fromHelper(helper, version); helper.setPosition(pos); return ins; }); patPtrs.forEach((position) => { const pos = helper.getPosition(0); helper.setPosition(position); const data = pattern_1.FurnacePattern.fromHelper(helper, version); const song = info.songs[data.song]; if (!song) { throw new Error("Pattern can not be loaded at song id " + data.song.toString(16) + " because it does not exist!"); } const channel = song.channelInfo[data.channel]; if (!channel) { throw new Error("Pattern can not be loaded at song id " + data.song.toString(16) + " channel id " + data.channel + " because it does not exist!"); } if (data.index > 256) { throw new Error("Pattern can not be loaded at index " + data.index.toString(16) + " because it's'invalid!"); } channel.patterns[data.index] = data.pattern; data.pattern.loadData(helper, version, song.rows, channel.effects); helper.setPosition(pos); }); return info; } writeToBuffer(helper, version) { var _a, _b; const _begin = helper.getPosition(8); helper.writeString(FurnaceInfo.magic, "ascii", false); helper.skipBytes(4); helper.writeUInt8(this.songs[0].timebase - 1); helper.writeUInt8(this.songs[0].speed1); helper.writeUInt8(this.songs[0].speed2); helper.writeUInt8(this.songs[0].initialArpeggioTime); helper.writeFloat32LE(this.songs[0].ticksPerSecond); helper.writeUInt16LE(this.songs[0].rows); helper.writeUInt16LE(this.songs[0].channelInfo[0].orders.length); helper.writeUInt8(this.songs[0].highlightA); helper.writeUInt8(this.songs[0].highlightB); const patterns = []; this.songs.forEach((s, song) => { s.channelInfo.forEach((c, channel) => { c.patterns.forEach((pattern, index) => { if (!pattern) { return; } patterns.push({ song, channel, index, pattern, }); }); }); }); helper.writeUInt16LE(this.instruments.length); helper.writeUInt16LE(this.wavetables.length); helper.writeUInt16LE(this.samples.length); helper.writeUInt32LE(patterns.length); const tempChips = new Array(32).fill(null); const sysIds = new Array(32).fill(0); let pos = 0; for (let i = 0; i < 32; i++) { const system = this.systems[i]; if (!system) { break; } sysIds[i] = system.systemId; for (const chip of system.chips) { if (pos >= 32) { throw new Error("Expected at most 32 chips in module!"); } tempChips[pos++] = chip; } } helper.writeBuffer(Buffer.from(sysIds), 0, 32); helper.writeBuffer(Buffer.from(tempChips.map((c) => { var _a; return (_a = c === null || c === void 0 ? void 0 : c.volume) !== null && _a !== void 0 ? _a : 64; })), 0, 32); helper.writeBuffer(Buffer.from(tempChips.map((c) => { var _a; return (_a = c === null || c === void 0 ? void 0 : c.panning) !== null && _a !== void 0 ? _a : 0; })), 0, 32); for (let i = 0; i < 32; i++) { helper.writeUInt32LE((_b = (_a = tempChips[i]) === null || _a === void 0 ? void 0 : _a.flags) !== null && _b !== void 0 ? _b : 0); } helper.writeString(this.metadata.songName, "utf-8", true); helper.writeString(this.metadata.songAuthor, "utf-8", true); helper.writeFloat32LE(this.A4Tuning); const writeValue = (minver, flag) => { helper.writeUInt8(version < minver ? 0 : flag !== null && flag !== void 0 ? flag : 0); }; const writeFlag = (minver, flag) => { helper.writeUInt8(version < minver || flag !== true ? 0 : 1); }; writeFlag(36, this.flags.limitSlides); writeValue(36, this.flags.pitchMode); writeValue(36, this.flags.loopModality); writeFlag(42, this.flags.properNoiseLayout); writeFlag(42, this.flags.waveDutyIsVolume); writeFlag(45, this.flags.resetMacroOnPorta); writeFlag(45, this.flags.legacyVolumeSlides); writeFlag(45, this.flags.compatibleArpeggio); writeFlag(45, this.flags.noteOffResetsSlides); writeFlag(45, this.flags.targetResetsSlides); writeFlag(47, this.flags.arpeggioInhibitsPortamento); writeFlag(47, this.flags.wackAlgorithmMacro); writeFlag(49, this.flags.brokenShortcutSlides); writeFlag(50, this.flags.ignoreDuplicateSlids); writeFlag(62, this.flags.stopPortamentoOnNoteOff); writeFlag(62, this.flags.continuousVibrato); writeFlag(64, this.flags.brokenDacMode); writeFlag(65, this.flags.oneTickCut); writeFlag(66, this.flags.instrumentChangeDuringPorta); writeFlag(69, this.flags.resetNoteBaseOnArpeggioEffectStop); const insPtrs = helper.getPosition(0); helper.skipBytes(4 * this.instruments.length); const wavePtrs = helper.getPosition(0); helper.skipBytes(4 * this.wavetables.length); const sampPtrs = helper.getPosition(0); helper.skipBytes(4 * this.samples.length); const patPtrs = helper.getPosition(0); helper.skipBytes(4 * patterns.length); this.songs[0].writeChannels(helper); helper.writeString(this.comment, "ascii", true); if (version >= 59) { helper.writeFloat32LE(this.masterVolume); } if (version >= 70) { writeFlag(70, this.flags.brokenSpeedSelection); writeFlag(71, this.flags.noSlideOnFirstTick); writeFlag(71, this.flags.nextRowResetArpPos); writeFlag(71, this.flags.ignoreJumpAtEnd); writeFlag(72, this.flags.buggyPortaAfterSlide); writeFlag(72, this.flags.newInsAffectsEnvelope); writeFlag(78, this.flags.extChStateIsShared); writeFlag(83, this.flags.ignoreDacOutsideOfCh); writeFlag(83, this.flags.E1xxE2xxPriorityOverSlide00); writeFlag(84, this.flags.newSegaPCM); writeFlag(85, this.flags.weirdFnumSlides); writeFlag(86, this.flags.SNDutyMacroAlwaysResetsPhase); writeFlag(90, this.flags.pitchMacroIsLinear); writeValue(94, this.flags.pitchSlideSpeedInFullLinear); writeFlag(97, this.flags.oldFMOctaveBehavior); writeFlag(98, this.flags.disableDACVolume); writeFlag(99, this.flags.newVolumeScaling); writeFlag(99, this.flags.volumeMacroIsHeld); writeFlag(99, this.flags.brokenOutVol); writeFlag(100, this.flags.slideStopsOnRepeatedNote); writeFlag(101, this.flags.brokenPortaPositionAfterArp); helper.skipBytes(9); } if (version >= 96) { helper.writeUInt16LE(this.songs[0].virtualTempoNumerator); helper.writeUInt16LE(this.songs[0].virtualTempoDenominator); } else if (version >= 70) { helper.skipBytes(4); } let subPtrs = 0; if (version >= 95) { helper.writeString(this.songs[0].name, "utf-8", true); helper.writeString(this.songs[0].comment, "utf-8", true); helper.writeUInt8(this.songs.length - 1); helper.skipBytes(3); subPtrs = helper.getPosition(0); helper.skipBytes(4 * (this.songs.length - 1)); } if (version >= 103) { helper.writeString(this.metadata.systemName, "utf-8", true); helper.writeString(this.metadata.gameName, "utf-8", true); helper.writeString(this.metadataJP.songName, "utf-8", true); helper.writeString(this.metadataJP.songAuthor, "utf-8", true); helper.writeString(this.metadataJP.systemName, "utf-8", true); helper.writeString(this.metadataJP.gameName, "utf-8", true); } helper.writeUInt32LE(helper.getPosition() - _begin, _begin - 4); this.songs.forEach((item, ix) => { if (ix === 0) { return; } helper.writeUInt32LE(helper.getPosition(0), subPtrs + ((ix - 1) * 4)); item.writeToBuffer(helper); }); this.instruments.forEach((item, ix) => { helper.writeUInt32LE(helper.getPosition(0), insPtrs + (ix * 4)); item.writeToBuffer(helper); }); this.wavetables.forEach((item, ix) => { helper.writeUInt32LE(helper.getPosition(0), wavePtrs + (ix * 4)); item.writeToBuffer(helper); }); this.samples.forEach((item, ix) => { helper.writeUInt32LE(helper.getPosition(0), sampPtrs + (ix * 4)); item.writeToBuffer(helper, version); }); patterns.forEach((item, ix) => { helper.writeUInt32LE(helper.getPosition(0), patPtrs + (ix * 4)); item.pattern.writeToBuffer(helper, version, item.channel, item.index, item.song); }); } } exports.FurnaceInfo = FurnaceInfo; FurnaceInfo.magic = "INFO"; var FurnacePitchMode; (function (FurnacePitchMode) { FurnacePitchMode[FurnacePitchMode["nonLinear"] = 0] = "nonLinear"; FurnacePitchMode[FurnacePitchMode["partialLinear"] = 1] = "partialLinear"; FurnacePitchMode[FurnacePitchMode["fullLinear"] = 2] = "fullLinear"; })(FurnacePitchMode = exports.FurnacePitchMode || (exports.FurnacePitchMode = {})); var FurnaceLoopModality; (function (FurnaceLoopModality) { FurnaceLoopModality[FurnaceLoopModality["item0"] = 0] = "item0"; FurnaceLoopModality[FurnaceLoopModality["item1"] = 1] = "item1"; FurnaceLoopModality[FurnaceLoopModality["item2"] = 2] = "item2"; })(FurnaceLoopModality = exports.FurnaceLoopModality || (exports.FurnaceLoopModality = {}));