UNPKG

meadowbrook

Version:

Alternative meyda cli

306 lines (293 loc) 8.53 kB
#!/usr/bin/env node const Meyda = require("meyda"); const audioCtx = new (require("web-audio-api")).AudioContext(); const fsProm = require("then-fs"); const merge = require("deepmerge"); const ProgressBar = require("progress"); const parseArgs = require("minimist"); const path = require("path"); const chalk = require("chalk"); const runLog = new Set(); const err = msg => { throw new Error(msg); }; const arrayInd = (ind, val) => { const retVal = []; retVal[ind] = val; return retVal; }; const objInd = (ind, val) => { const retVal = {}; retVal[ind] = val; return retVal; }; const AllFeatures = [ "rms", "energy", "zcr", "amplitudeSpectrum", "powerSpectrum", "spectralCentroid", "spectralFlatness", "spectralSlope", "spectralRolloff", "spectralSpread", "spectralSkewness", "spectralKurtosis", "loudness", "perceptualSpread", "perceptualSharpness", "mfcc" ]; const WindowingFunctions = new Set(["blackman", "sine", "hanning", "hamming"]); /* - load audio into buffer - buffer is array of float32 samples - calculate index which represent sample at each 1/60 of a second - for each index slice Meyda.bufferSize sample and pass to Meyda.extract - encode data as base64 - stream out as json - structure: fps: sampleRate: duration: numberOfChannels: bufferSize: windowingFunction: audioFile: extracted: feature-name: [channel-num]: [index-of-frame]: encoded-string - base64 to buffer const buf = Buffer.from(encodedString, 'base64'); - buffer to base64 const encodedString = Buffer.toString('base64'); - buffer to float32Array var b = new Buffer(512); var ui32 = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / Uint32Array.BYTES_PER_ELEMENT); - float32Array to buffer Buffer.from(arr.buffer); */ const processArgs = async () => { const parsed = parseArgs(process.argv.slice(2)); const help = await fsProm.readFile( path.join(__dirname, "docs/help.yml"), "utf8" ); if (parsed.h) { console.warn(help); process.exit(0); } const retVal = { features: parsed._.slice(1) || err("No features defined"), inputPath: path.resolve( process.cwd(), parsed._[0] || err("No input file defined") ), outputPath: parsed.o || null, bufferSize: +parsed.bs || 512, windowing: parsed.w || "hanning", fps: +parsed.f || 60, collectionFormat: parsed.format || "array" }; return retVal; }; const decodeAudio = async file => { const fileBuffer = await fsProm.readFile(file); debugger; return new Promise((resolve, reject) => audioCtx.decodeAudioData(fileBuffer, resolve, reject) ); }; const extract = (features, signal) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(Meyda.extract(features, signal)); }, 0); }); }; const extractFeatures = async ( audioBuffer, features, { fps = 60, bufferSize = 512, windowing = "hanning", collectionFormat = "array" } ) => { const frameBorders = []; let result = {}; let currTime = 0; while (currTime < audioBuffer.duration - 1 / fps) { frameBorders.push(Math.floor(currTime * audioBuffer.sampleRate)); currTime += 1 / fps; } features = Array.isArray(features) ? features : [features]; const bar = new ProgressBar( "processing [:bar] :current/:total time left: :etas", { total: audioBuffer.numberOfChannels * frameBorders.length * features.length, complete: "=", incomplete: " ", width: 20 } ); const tickBar = () => { bar.tick(); if (bar.complete) { console.warn(chalk.cyan("\ncomplete\n")); } }; Meyda.bufferSize = bufferSize; if (!WindowingFunctions.has(windowing)) { throw "Invalid windowing function"; } Meyda.windowingFunction = windowing; const indexCollection = collectionFormat == "array" ? arrayInd : collectionFormat == "object" ? objInd : err(`Invalid collection format: ${collectionFormat}`); for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) { const channelData = audioBuffer.getChannelData(channel); for ( let frameBorderInd = 0; frameBorderInd < frameBorders.length; frameBorderInd++ ) { const frameBorder = frameBorders[frameBorderInd]; const signal = channelData.slice( frameBorder, frameBorder + Meyda.bufferSize ); // console.log("Buffer size",signal.length); const extracted = await extract(features, signal); // console.log("Extracted",extracted); for (const feature of features) { const extractedFeature = extracted[feature]; if ( typeof extractedFeature.buffer !== "undefined" && extractedFeature instanceof Float32Array ) { const encodedFeatureBuffer = Buffer.from( extractedFeature.buffer ).toString("base64"); // result = merge(result,{ // "extracted":{ // [feature]:{ // [channel]:{ // [frameBorderInd]:encodedFeatureBuffer // } // } // } // }); result = merge(result, { extracted: { [feature]: indexCollection( channel, indexCollection(frameBorderInd, encodedFeatureBuffer) ) } }); } else if (typeof extractedFeature === "number") { // result = merge(result,{ // "extracted":{ // [feature]:{ // [channel]:{ // [frameBorderInd]:isNaN(extractedFeature)?0:extractedFeature, // } // } // } // }); result = merge(result, { extracted: { [feature]: indexCollection( channel, indexCollection( frameBorderInd, isNaN(extractedFeature) ? 0 : extractedFeature ) ) } }); } else if (Array.isArray(extractedFeature)) { result = merge(result, { extracted: { [feature]: indexCollection( channel, indexCollection(frameBorderInd, extractedFeature) ) } }); } else if (feature === "loudness") { result = merge(result, { extracted: { [feature]: indexCollection( channel, indexCollection(frameBorderInd, { specific: Array.from(extractedFeature.specific), // specific:Buffer.from(extractedFeature.specific.buffer).toString('base64'), total: extractedFeature.total }) ) } }); } else { // console.log(feature,extractedFeature); runLog.add(chalk.red(`Result of ${feature} could not be encoded`)); } tickBar(); } } } result = merge(result, { fps: fps, sampleRate: audioBuffer.sampleRate, duration: audioBuffer.duration, numberOfChannels: audioBuffer.numberOfChannels, bufferSize: bufferSize, windowingFunction: windowing }); return result; }; const main = async () => { const configs = await processArgs(); // console.log("Args processed"); const decodedAudio = await decodeAudio(configs.inputPath).then( i => i, () => { throw "Not a valid input file"; } ); const features = configs.features.includes("all") ? AllFeatures : configs.features; if (features.some(i => !AllFeatures.includes(i))) { throw "Not a valid feature"; } const result = await extractFeatures(decodedAudio, features, configs); const jsonEncoded = JSON.stringify(result, null, 4); if (configs.outputPath) { await fsProm.writeFile(configs.outputPath, jsonEncoded); } else { console.log(jsonEncoded); } }; main() .then(() => { if (runLog.size) { console.warn(chalk.bgYellow.black(" WARN ")); for (const msg of runLog) { console.warn(chalk.yellow(msg)); } } }) .catch(e => { console.error(chalk.red(e.stack || e)); process.exit(1); });