From 605c44de79eb45b53e7fd802e1075eb209de4032 Mon Sep 17 00:00:00 2001 From: Glenn Forbes Date: Tue, 17 Mar 2020 19:40:39 +0000 Subject: [PATCH] Initial mess. --- .gitignore | 4 ++ msadpcm.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++ popnchart.js | 100 ++++++++++++++++++++++++++ popntowav.js | 121 +++++++++++++++++++++++++++++++ twodx.js | 64 +++++++++++++++++ 5 files changed, 487 insertions(+) create mode 100644 .gitignore create mode 100644 msadpcm.js create mode 100644 popnchart.js create mode 100644 popntowav.js create mode 100644 twodx.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cde1834 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +ffmpeg.exe +ifstools.exe +.vscode/launch.json +package-lock.json diff --git a/msadpcm.js b/msadpcm.js new file mode 100644 index 0000000..d590636 --- /dev/null +++ b/msadpcm.js @@ -0,0 +1,198 @@ +const ADAPTATION_TABLE = [ + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230, +]; + +function clamp(val, min, max) { + if(val < min) return min; + else if(val > max) return max; + else return val; +} + +function expandNibble(nibble, state, channel) { + const signed = 8 <= nibble ? nibble - 16 : nibble; + + let predictor = (( + state.sample1[channel] * state.coeff1[channel] + + state.sample2[channel] * state.coeff2[channel] + ) >> 8) + (signed * state.delta[channel]); + + predictor = clamp(predictor, -0x8000, 0x7fff); + + state.sample2[channel] = state.sample1[channel]; + state.sample1[channel] = predictor; + + state.delta[channel] = Math.floor(ADAPTATION_TABLE[nibble] * state.delta[channel] / 256); + if(state.delta[channel] < 16) state.delta[channel] = 16; + + return predictor; +} + +/** + * Decode a block of MS-ADPCM data + * @param {Buffer} buf one block of MS-ADPCM data + * @param {number} channels number of channels (usually 1 or 2, never tested on upper values) + * @param {number[]} coefficient1 array of 7 UInt8 coefficient values + * usually, [ 256, 512, 0, 192, 240, 460, 392 ] + * @param {number[]} coefficient2 array of 7 UInt8 coefficient values + * usually, [ 0, -256, 0, 64, 0, -208, -232 ] + * @return {Buffer[]} array of decoded PCM buffer for each channels + */ +function decode(buf, channels, coefficient1, coefficient2) { + const state = { + coefficient: [ coefficient1, coefficient2 ], + coeff1: [], + coeff2: [], + delta: [], + sample1: [], + sample2: [], + }; + + let offset = 0; + + // Read MS-ADPCM header + for(let i = 0 ; i < channels ; i++) { + const predictor = clamp(buf.readUInt8(offset), 0, 6); + offset += 1; + + state.coeff1[i] = state.coefficient[0][predictor]; + state.coeff2[i] = state.coefficient[1][predictor]; + } + + for(let i = 0 ; i < channels ; i++) { state.delta.push(buf.readInt16LE(offset)); offset += 2; } + for(let i = 0 ; i < channels ; i++) { state.sample1.push(buf.readInt16LE(offset)); offset += 2; } + for(let i = 0 ; i < channels ; i++) { state.sample2.push(buf.readInt16LE(offset)); offset += 2; } + + // Decode + const output = []; + + for(let i = 0 ; i < channels ; i++) + output[i] = [ state.sample2[i], state.sample1[i] ]; + + let channel = 0; + while(offset < buf.length) { + const byte = buf.readUInt8(offset); + offset += 1; + + output[channel].push(expandNibble(byte >> 4, state, channel)); + channel = (channel + 1) % channels; + + output[channel].push(expandNibble(byte & 0xf, state, channel)); + channel = (channel + 1) % channels; + } + + //Converting all sound to stereo since it'll be easier later on. + if (channels == 1) { + output.push(output[0]); + } + + return output; +} + +function readWav(buf) { + + let offset = 0; + + // 'RIFF' + const magic = buf.readUInt32BE(offset); offset += 4; + if(magic !== 0x52494646) { + console.log(magic); + throw "0x0000:0x0004 != 52:49:46:46"; + } + + const dataSize = buf.readUInt32LE(offset); offset += 4; + + // 'WAVE' + const format = buf.readUInt32BE(offset); offset += 4; + if(format !== 0x57415645) throw "0x0008:0x000B != 57:41:56:45"; + + let wavFormat, wavData; + + while(offset < buf.length) { + const name = buf.readUInt32BE(offset); offset += 4; + const blockSize = buf.readUInt32LE(offset); offset += 4; + + // 'fmt ' + if(name === 0x666D7420) { + wavFormat = { + format: buf.readUInt16LE(offset + 0), + channels: buf.readUInt16LE(offset + 2), + sampleRate: buf.readUInt32LE(offset + 4), + byteRate: buf.readUInt32LE(offset + 8), + blockAlign: buf.readUInt16LE(offset + 12), + bitsPerSample: buf.readUInt16LE(offset + 14), + }; + + offset += 16; + + if(wavFormat.format === 0x01) { + // console.log(`${filename} is PCM file`); + continue; + } + else if(wavFormat.format === 0x02) { + // console.log(`${filename} is MS-ADPCM file`); + + const extraSize = buf.readUInt16LE(offset); offset += 2; + wavFormat.extraSize = extraSize; + wavFormat.extra = { + samplesPerBlock: buf.readUInt16LE(offset + 0), + coefficientCount: buf.readUInt16LE(offset + 2), + coefficient: [ [], [] ], + }; + + offset += 4; + + for(let i = 0 ; i < wavFormat.extra.coefficientCount ; i++) { + wavFormat.extra.coefficient[0].push(buf.readInt16LE(offset + 0)); + wavFormat.extra.coefficient[1].push(buf.readInt16LE(offset + 2)); + offset += 4; + } + } + else throw `WAVE format ${wavFormat.format} is unknown`; + } + // 'data' + else if(name === 0x64617461) { + wavData = buf.slice(offset, offset + blockSize); + offset += blockSize; + } + else { + offset += blockSize; + } + } + + if(wavFormat && wavData) return { format: wavFormat, data: wavData }; + else throw "'fmt ' or/and 'data' block not found"; +} + +exports.decodeKeysoundOut = (buff, vol) => { + const adpcmData = readWav(buff); + const blockSize = adpcmData.format.blockAlign; + + let totalBuff = Buffer.alloc(1); + const totalBlocks = adpcmData.data.length / blockSize; + let totalOffset = 0; + + for(let i = 0 ; i < adpcmData.data.length ; i += blockSize) { + const adpcmBlock = adpcmData.data.slice(i, i + blockSize); + const decoded = decode( + adpcmBlock, + adpcmData.format.channels, + adpcmData.format.extra.coefficient[0], + adpcmData.format.extra.coefficient[1] + ); + + const pcmBlockSize = decoded[0].length * 2; + if (totalBuff.length == 1) { + totalBuff = Buffer.alloc(pcmBlockSize * totalBlocks * 2); + } + + for(let s = 0 ; s < pcmBlockSize/2; s++) { + for(let c = 0 ; c < decoded.length ; c++) { + totalBuff.writeInt16LE(decoded[c][s], totalOffset); + totalOffset += 2; + } + } + } + + return {data: totalBuff, channels: adpcmData.format.channels, samplingRate: adpcmData.format.sampleRate, volume: vol}; +} \ No newline at end of file diff --git a/popnchart.js b/popnchart.js new file mode 100644 index 0000000..37fff70 --- /dev/null +++ b/popnchart.js @@ -0,0 +1,100 @@ +const fs = require("fs"); + +class PopnChart { + + 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."; + } + + this.events = []; + + let offset = 0; + while (offset < this.data.length) { + const eventOffset = this.data.readInt32LE(offset); + offset += 5; + const eventFlag = this.data.readInt8(offset); + offset += 1; + + let eventParam = 0; + let eventValue = 0; + + let joined = this.data.slice(offset, offset+2); + offset += 2; + if (eventFlag === 2 || eventFlag === 7) { + joined.swap16(); + const hx = joined.toString("hex"); + + eventParam = parseInt(hx.slice(1, 4), 16); + eventValue = parseInt(hx.slice(0, 1), 16); + } else { + eventParam = joined.readUInt8(0); + eventValue = joined.readUInt8(1); + } + + if (newFormat) { + const longNoteData = this.data.readInt32LE(offset); + offset += 4; + } + + this.events.push([eventOffset, eventFlag, eventParam, eventValue]); + } + + this.bpm = 0; + this.bpmTransitions = []; + + this.playEvents = []; + this.uniqueKeysounds = []; + + this.notecount = 0; + + const sampleColumns = [0, 0, 0, 0, 0, 0, 0, 0, 0]; + + for (const event of this.events) { + let [offset, eventType, param, value] = event; + + if (eventType == 7 || eventType == 2) { + if (this.uniqueKeysounds.indexOf(param) == -1) { + this.uniqueKeysounds.push(param); + } + } + + switch (eventType) { + case 1: + if (sampleColumns[param] != 0) { + this.playEvents.push([offset, sampleColumns[param]]); + } + this.notecount += 1; + break; + case 2: + if (offsetKeysounds) { + param -= 1; + } + sampleColumns[value] = param; + break; + case 3: + this.playEvents.push([offset, 0]); + break; + case 4: + this.bpm = param; + this.bpmTransitions.push(param); + break; + case 7: + if (offsetKeysounds) { + param -= 1; + } + this.playEvents.push([offset, param]); + } + } + } +} + +module.exports = PopnChart; \ No newline at end of file diff --git a/popntowav.js b/popntowav.js new file mode 100644 index 0000000..a42f251 --- /dev/null +++ b/popntowav.js @@ -0,0 +1,121 @@ +const MSADPCM = require("./msadpcm"); +const Popnchart = require("./popnchart"); +const Twodx = require("./twodx"); + +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const SampleRate = require("node-libsamplerate"); +const wav = require("wav"); + +if (process.argv.length < 3) { + console.log("Usage: node popntowav ifs_file"); + process.exit(); +} + +let arg1 = process.argv[2]; +let outputFilename = process.argv[3]; + +child_process.execSync(`ifstools ${arg1}`); +const ifsname = path.basename(arg1).slice(0, -4); +let twodxPath = `${ifsname}_ifs/${ifsname}.2dx`; +let chartPath = `${ifsname}_ifs/${ifsname}_op.bin`; + +if (!fs.existsSync(chartPath)) { + chartPath = `${ifsname}_ifs/${ifsname}_hp.bin`; +} + +let cleanUp = true; + +let soundContainer = new Twodx(twodxPath); +let chart = new Popnchart(chartPath, !soundContainer.late_bg); +//The sound container is full of MSADPCM keysounds, so each one needs decoded. +let decodedKeysounds = soundContainer.keysounds.map((keysound) => MSADPCM.decodeKeysoundOut(keysound.data, keysound.unk2)); + +if (cleanUp) fs.rmdirSync(path.basename(arg1).slice(0, -4)+"_ifs", {recursive: true}); + +let highestSample = 0; +//Outputting stereo 44.1Khz regardless. +const channels = 2; +const samplingRate = 44100; +//Because Int32. +const bytes = 4; +let lowestVolume = 100; + +for (var i = 0; i buffSize) { + buffSize = off + (keysound.data.length*2); + } + } +} + +//Creating a buffer to store Int32s. +//This is overcompensating to deal with overflow from digital summing. +//Final Timestamp in milliseconds * sampling rate * 2 channels * 4 bytes. +const finalBuffer = Buffer.alloc(buffSize); + +chart.playEvents.forEach((event) => { + const [offset, keysoundNo] = event; + //Grabbing the relevant offset for the buffer. + const convertedOffset = parseInt((offset*samplingRate)/1000)*channels*bytes; + const keysound = decodedKeysounds[keysoundNo]; + + if (keysound) { + const keysoundData = keysound.data; + for (var i = 0; i { + const ind = data.readUInt32LE(offset); + offset += 4; + return ind; + }); + + for (let i = 0; i