meadowbrook
Version:
Alternative meyda cli
306 lines (293 loc) • 8.53 kB
JavaScript
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);
});