From f101de695b26944f82c158daf65b89b6cfde3983 Mon Sep 17 00:00:00 2001 From: Glenn Forbes Date: Tue, 17 Mar 2020 20:41:05 +0000 Subject: [PATCH] Tidied up and added some comments. --- msadpcm.js | 5 +++++ package.json | 24 ++++++++++++++++++++++ popnchart.js | 40 ++++++++++++++++++++++++++++-------- popntowav.js | 18 ++++++++-------- twodx.js | 58 +++++++++++++++++++++++++++++++++++++--------------- 5 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 package.json diff --git a/msadpcm.js b/msadpcm.js index d590636..44edf92 100644 --- a/msadpcm.js +++ b/msadpcm.js @@ -1,3 +1,8 @@ +//This is adapted from https://github.com/Snack-X/node-ms-adpcm +//I tried to find a fast decoder for MSADPCM in nodejs and came up short. +//Maybe I didn't look hard enough. +//With some work, this did the job well for me. + const ADAPTATION_TABLE = [ 230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230, diff --git a/package.json b/package.json new file mode 100644 index 0000000..e479a65 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "popntowav", + "version": "1.0.0", + "description": "Tool for rendering pop'n music IFS/chart files to 16-bit PCM wav.", + "main": "popntowav.js", + "dependencies": { + "node-libsamplerate": "^1.0.0", + "wav": "^1.0.2" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Gi-z/popntowav.git" + }, + "author": "Giz", + "license": "ISC", + "bugs": { + "url": "https://github.com/Gi-z/popntowav/issues" + }, + "homepage": "https://github.com/Gi-z/popntowav#readme" +} diff --git a/popnchart.js b/popnchart.js index 37fff70..57a69d1 100644 --- a/popnchart.js +++ b/popnchart.js @@ -2,21 +2,22 @@ const fs = require("fs"); class PopnChart { + //offsetKeysounds indicates that any keysound index references + //within the chart may need to be decremented by one to account + //for the bgtrack not being at the start of the 2dx container. + constructor(filename, offsetKeysounds=false) { this.filename = filename; this.data = fs.readFileSync(filename); - let newFormat = false; - if (this.data.readInt8(16) == 69) { - newFormat = true; - } else if (this.data.readInt8(12) == 69) { - newFormat = false; - } else { - throw "Chart format not supported."; - } + //Check if the chart is newstyle or old> + //Needed to know event lengths. + let newFormat = this.checkFormat(); this.events = []; + //This loop reads through the entire file, + //rather than ending on an endofsong event. let offset = 0; while (offset < this.data.length) { const eventOffset = this.data.readInt32LE(offset); @@ -27,9 +28,15 @@ class PopnChart { let eventParam = 0; let eventValue = 0; + //In regular events, param and value are 1 byte. + //However on keysound events, the first 4 bits + //are used for the param, while the proceeding + //12 bits are used for the value. let joined = this.data.slice(offset, offset+2); offset += 2; if (eventFlag === 2 || eventFlag === 7) { + //Endianness needs flipped. + //This is a terrible way of doing this, I think. joined.swap16(); const hx = joined.toString("hex"); @@ -40,6 +47,7 @@ class PopnChart { eventValue = joined.readUInt8(1); } + //Long note data isn't needed for GSTs, however it's here. if (newFormat) { const longNoteData = this.data.readInt32LE(offset); offset += 4; @@ -69,25 +77,31 @@ class PopnChart { switch (eventType) { case 1: + //Playable note event. + //This if is overzealous, just trying to stop BG tracks from being played twice. if (sampleColumns[param] != 0) { this.playEvents.push([offset, sampleColumns[param]]); } this.notecount += 1; break; case 2: + //Sample change event. if (offsetKeysounds) { param -= 1; } sampleColumns[value] = param; break; case 3: + //BG track start event. this.playEvents.push([offset, 0]); break; case 4: + //BPM change event. this.bpm = param; this.bpmTransitions.push(param); break; case 7: + //BG sample event. if (offsetKeysounds) { param -= 1; } @@ -95,6 +109,16 @@ class PopnChart { } } } + + checkFormat() { + if (this.data.readInt8(16) == 69) { + return true; + } else if (this.data.readInt8(12) == 69) { + return false; + } else { + throw "Chart format not supported."; + } + } } module.exports = PopnChart; \ No newline at end of file diff --git a/popntowav.js b/popntowav.js index a42f251..171ab47 100644 --- a/popntowav.js +++ b/popntowav.js @@ -40,8 +40,11 @@ const channels = 2; const samplingRate = 44100; //Because Int32. const bytes = 4; -let lowestVolume = 100; +//After loading in all the keysounds, we need to find ones that +//aren't 44.1KHz, since they'll mess everything up. +//Best resampling option I could find was node-libsamplerate. +//I'm sure other people have better suggestions. for (var i = 0; i { +for (const event of chart.playEvents) { const [offset, keysoundNo] = event; //Grabbing the relevant offset for the buffer. const convertedOffset = parseInt((offset*samplingRate)/1000)*channels*bytes; @@ -100,12 +101,12 @@ chart.playEvents.forEach((event) => { finalBuffer.writeInt32LE(mixedBytes, convertedOffset+(i*2)); } } -}); +} -//We've got summed 16bit values, but they need normalising so we can hear them, -//from a 32bit buffer. +//We've got summed 16bit values, which means they won't fit into a 16bit buffer. +//We also can't just shove them into a 32bit buffer, since they're 16bit scale. +//Instead, we'll have to normalise them first using the peak observed volume. //2147483647 is just so I don't have to import a MAX_INT32 module. -//We're normaslising against the highest volume seen. //After normalising, these values will be scaled correctly from 16bit to 32bit. const normaliseFactor = parseInt(2147483647/highestSample); for (var i = 0; i { - const ind = data.readUInt32LE(offset); - offset += 4; + generateOffsets() { + return [...Array(this.file_count).keys()].map((_) => { + const ind = this.data.readUInt32LE(this.offset); + this.offset += 4; return ind; }); + } - for (let i = 0; i