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
JavaScript
"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 = {}));