Converted script to typescript. Replaced minimalistic shuffle function with a more readable fisher-yates implimentation (same algo, expanded syntax)
This commit is contained in:
parent
6e2bc5b1b1
commit
23c465b57f
878
audioloom
878
audioloom
|
@ -1,21 +1,16 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
const execRaw = require('child_process').exec
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const program = require('commander')
|
||||
const fs = require('fs-extra')
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
let TMPDIR = os.tmpdir() || '/tmp'
|
||||
let TMPPATH
|
||||
|
||||
let EXE = `sox`
|
||||
let IDENTIFY = `soxi`
|
||||
let SLICE = (1000 / 24) + ''
|
||||
|
||||
'use strict';
|
||||
const execRaw = require('child_process').exec;
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const program = require('commander');
|
||||
const fs = require('fs-extra');
|
||||
const packageJson = require('./package.json');
|
||||
let TMPDIR = os.tmpdir() || '/tmp';
|
||||
let TMPPATH;
|
||||
let EXE = `sox`;
|
||||
let IDENTIFY = `soxi`;
|
||||
let SLICE = (1000 / 24) + '';
|
||||
/**
|
||||
* Shells out to execute a command with async/await.
|
||||
* Async wrapper to exec module.
|
||||
|
@ -24,13 +19,14 @@ let SLICE = (1000 / 24) + ''
|
|||
*
|
||||
* @returns {Promise} Promise containing the complete stdio
|
||||
**/
|
||||
async function exec (cmd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return execRaw(cmd, (err, stdio, stderr) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(stdio)
|
||||
})
|
||||
})
|
||||
async function exec(cmd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return execRaw(cmd, (err, stdio, stderr) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
return resolve(stdio);
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Delays process for specified amount of time in milliseconds.
|
||||
|
@ -39,10 +35,10 @@ async function exec (cmd) {
|
|||
*
|
||||
* @returns {Promise} Promise that resolves after set time
|
||||
**/
|
||||
async function delay (ms) {
|
||||
return new Promise((resolve, reject) =>{
|
||||
return setTimeout(resolve, ms)
|
||||
})
|
||||
async function delay(ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Pads a numerical value with preceding zeros to make strings same length.
|
||||
|
@ -52,75 +48,80 @@ async function delay (ms) {
|
|||
*
|
||||
* @returns {string} Padded number as a string
|
||||
**/
|
||||
function zeroPad (i, max = 5) {
|
||||
let len = (i + '').length
|
||||
let str = i + ''
|
||||
for (let x = 0; x < max - len; x++) {
|
||||
str = '0' + str
|
||||
}
|
||||
return str
|
||||
function zeroPad(i, max = 5) {
|
||||
const len = (i + '').length;
|
||||
let str = i + '';
|
||||
for (let x = 0; x < max - len; x++) {
|
||||
str = '0' + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
* Shuffles an array into a random state.
|
||||
*
|
||||
* @param {array} a Array to randomize
|
||||
**/
|
||||
function shuffle (a) {
|
||||
for (let i = a.length; i; i--) {
|
||||
let j = Math.floor(Math.random() * i);
|
||||
[a[i - 1], a[j]] = [a[j], a[i - 1]]
|
||||
function shuffle(array) {
|
||||
let j;
|
||||
let temp;
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1));
|
||||
temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clears the temporary directory of all files.
|
||||
* Clears the temporary directory of all files.
|
||||
* Establishes a directory if none exists.
|
||||
**/
|
||||
async function clear () {
|
||||
let cmd = `rm -r "${TMPPATH}"`
|
||||
let exists
|
||||
|
||||
try {
|
||||
exists = await fs.exists(TMPPATH)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
console.log(`Clearing temp directory "${TMPPATH}"`)
|
||||
try {
|
||||
await exec(cmd)
|
||||
} catch (err) {
|
||||
//suppress error
|
||||
console.dir(err)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(TMPPATH)
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
async function clear() {
|
||||
let cmd = `rm -r "${TMPPATH}"`;
|
||||
let exists;
|
||||
try {
|
||||
exists = await fs.exists(TMPPATH);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
if (exists) {
|
||||
console.log(`Clearing temp directory "${TMPPATH}"`);
|
||||
try {
|
||||
await exec(cmd);
|
||||
}
|
||||
catch (err) {
|
||||
//suppress error
|
||||
console.dir(err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.mkdir(TMPPATH);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Parses timecode string to float of total seconds
|
||||
*
|
||||
* @param {string} str Timecode string to parse
|
||||
*
|
||||
* @returns {float} Seconds at float
|
||||
* @returns {float} Seconds at float
|
||||
**/
|
||||
function parseTC (str) {
|
||||
const parts = str.split(':')
|
||||
let sec = 0
|
||||
if (parts[0] != 0) sec += parseFloat(parts[0]) * 60 * 60
|
||||
if (parts[1] != 0) sec += parseFloat(parts[1]) * 60
|
||||
if (parts[2] != 0) sec += parseFloat(parts[2])
|
||||
return sec
|
||||
function parseTC(str) {
|
||||
const parts = str.split(':');
|
||||
let sec = 0;
|
||||
if (parts[0] != 0)
|
||||
sec += parseFloat(parts[0]) * 60 * 60;
|
||||
if (parts[1] != 0)
|
||||
sec += parseFloat(parts[1]) * 60;
|
||||
if (parts[2] != 0)
|
||||
sec += parseFloat(parts[2]);
|
||||
return sec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns offset position of audio file to slice at.
|
||||
*
|
||||
|
@ -129,20 +130,19 @@ function parseTC (str) {
|
|||
*
|
||||
* @returns {str} New position cast as string
|
||||
**/
|
||||
async function audioLength (filePath) {
|
||||
const exe = IDENTIFY
|
||||
const cmd = `${exe} -d "${filePath}"`
|
||||
let str
|
||||
|
||||
try {
|
||||
str = await exec(cmd)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(11)
|
||||
}
|
||||
return parseTC(str)
|
||||
async function audioLength(filePath) {
|
||||
const exe = IDENTIFY;
|
||||
const cmd = `${exe} -d "${filePath}"`;
|
||||
let str;
|
||||
try {
|
||||
str = await exec(cmd);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
process.exit(11);
|
||||
}
|
||||
return parseTC(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns offset position of audio file to slice at.
|
||||
*
|
||||
|
@ -151,100 +151,102 @@ async function audioLength (filePath) {
|
|||
*
|
||||
* @returns {str} New position cast as string
|
||||
**/
|
||||
|
||||
function offset (i, slice) {
|
||||
return (i * (parseFloat(slice) / 1000)) + ''
|
||||
function offset(i, slice) {
|
||||
return (i * (parseFloat(slice) / 1000)) + '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all slices from audio file. Appends number to the string
|
||||
* to keep slices in alternating order to be quickly stitched together
|
||||
* or re-sorted.
|
||||
*
|
||||
* @param {string} file String representing path to audio file
|
||||
* @param {float} len Length of the slice to make
|
||||
* @param {integer} order Integer to be appended to pathname of file
|
||||
*
|
||||
* @returns {string} String with the export order, not sure why I did this
|
||||
**/
|
||||
async function slices (file, len, order) {
|
||||
let ext = 'wav'
|
||||
let exe = EXE
|
||||
let slice = SLICE
|
||||
let tmpoutput
|
||||
let cmd
|
||||
let i = 0
|
||||
let total = Math.floor((len * 1000) / parseFloat(slice))
|
||||
console.log(`Exporting ${file} as ${total} slices ${SLICE}ms long...`)
|
||||
|
||||
for (i = 0; i < total; i++) {
|
||||
tmpoutput = path.join(TMPPATH, `export-${zeroPad(i)}_${order}.${ext}`)
|
||||
cmd = `${exe} "${file}" "${tmpoutput}" trim ${offset(i, slice)} ${parseFloat(slice) / 1000}`
|
||||
try {
|
||||
console.log(cmd)
|
||||
await exec(cmd)
|
||||
} catch (err) {
|
||||
console.error('Error exporting file', err)
|
||||
return process.exit(3)
|
||||
}
|
||||
}
|
||||
return path.join(TMPPATH, `export-%05d_${order}`)
|
||||
}
|
||||
|
||||
async function slices(file, len, order) {
|
||||
let ext = 'wav';
|
||||
let exe = EXE;
|
||||
let slice = SLICE;
|
||||
let tmpoutput;
|
||||
let cmd;
|
||||
let i = 0;
|
||||
let total = Math.floor((len * 1000) / parseFloat(slice));
|
||||
console.log(`Exporting ${file} as ${total} slices ${SLICE}ms long...`);
|
||||
for (i = 0; i < total; i++) {
|
||||
tmpoutput = path.join(TMPPATH, `export-${zeroPad(i)}_${order}.${ext}`);
|
||||
cmd = `${exe} "${file}" "${tmpoutput}" trim ${offset(i, slice)} ${parseFloat(slice) / 1000}`;
|
||||
try {
|
||||
console.log(cmd);
|
||||
await exec(cmd);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Error exporting file', err);
|
||||
return process.exit(3);
|
||||
}
|
||||
}
|
||||
return path.join(TMPPATH, `export-%05d_${order}`);
|
||||
}
|
||||
/**
|
||||
* Re-arranges the slices into the order specified in the pattern.
|
||||
* Calls `patternSort()` to perform the rename and unlink actions
|
||||
*
|
||||
*
|
||||
* @param {array} pattern Pattern of the slices per input
|
||||
* @param {boolean} realtime Flag to turn on or off realtime behavior (drop slice / number of files)
|
||||
* @param {boolean} random Flag to turn on or off random behavior
|
||||
*
|
||||
* @returns {array} Array of frame paths
|
||||
**/
|
||||
async function weave (pattern, realtime, random) {
|
||||
let slices
|
||||
let old
|
||||
let seqFile
|
||||
let seq
|
||||
let ext = '.wav'
|
||||
let alt = false
|
||||
|
||||
console.log('Weaving slices...')
|
||||
|
||||
try {
|
||||
slices = await fs.readdir(TMPPATH)
|
||||
} catch (err) {
|
||||
console.error('Error reading tmp directory', err)
|
||||
}
|
||||
|
||||
//console.dir(slices)
|
||||
slices = slices.filter (file => {
|
||||
if (file.indexOf(ext) !== -1) return true
|
||||
})
|
||||
|
||||
for (let el of pattern) {
|
||||
if (el !== 1) alt = true
|
||||
}
|
||||
|
||||
if (random){
|
||||
try {
|
||||
seq = await randomSort(slices, realtime)
|
||||
} catch (err) {
|
||||
console.error('Error sorting slices')
|
||||
}
|
||||
} else if (!alt) {
|
||||
try {
|
||||
seq = await standardSort(slices, pattern, realtime)
|
||||
} catch (err) {
|
||||
console.error('Error sorting slices')
|
||||
}
|
||||
} else if (alt) {
|
||||
console.warn('This feature is not ready, please check https://github.com/sixteenmillimeter/audioloom.git')
|
||||
process.exit(10)
|
||||
try {
|
||||
seq = await altSort(slices, pattern, realtime)
|
||||
} catch (err) {
|
||||
console.error('Error sorting slices')
|
||||
}
|
||||
}
|
||||
//console.dir(seq)
|
||||
return seq
|
||||
async function weave(pattern, realtime, random) {
|
||||
let slices;
|
||||
let seq;
|
||||
let ext = '.wav';
|
||||
let alt = false;
|
||||
console.log('Weaving slices...');
|
||||
try {
|
||||
slices = await fs.readdir(TMPPATH);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Error reading tmp directory', err);
|
||||
}
|
||||
//console.dir(slices)
|
||||
slices = slices.filter(file => {
|
||||
if (file.indexOf(ext) !== -1)
|
||||
return true;
|
||||
});
|
||||
for (let el of pattern) {
|
||||
if (el !== 1)
|
||||
alt = true;
|
||||
}
|
||||
if (random) {
|
||||
try {
|
||||
seq = await randomSort(slices, pattern, realtime);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Error sorting slices');
|
||||
}
|
||||
}
|
||||
else if (!alt) {
|
||||
try {
|
||||
seq = await standardSort(slices, pattern, realtime);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Error sorting slices');
|
||||
}
|
||||
}
|
||||
else if (alt) {
|
||||
console.warn('This feature is not ready, please check https://github.com/sixteenmillimeter/audioloom.git');
|
||||
process.exit(10);
|
||||
try {
|
||||
seq = await altSort(slices, pattern, realtime);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Error sorting slices');
|
||||
}
|
||||
}
|
||||
//console.dir(seq)
|
||||
return seq;
|
||||
}
|
||||
/**
|
||||
* TODO
|
||||
|
@ -253,44 +255,43 @@ async function weave (pattern, realtime, random) {
|
|||
* @param {array} list List of slices to group
|
||||
* @param {array} pattern Array representing pattern
|
||||
* @param {boolean} realtime Flag to group with "realtime" behavior
|
||||
*
|
||||
* @returns {array} Sorted array of frames
|
||||
**/
|
||||
async function altSort (list, pattern, realtime) {
|
||||
let groups = []
|
||||
let newList = []
|
||||
let frameCount = 0
|
||||
let oldPath
|
||||
let newName
|
||||
let newPath
|
||||
let ext = path.extname(list[0])
|
||||
|
||||
for (let g of pattern) {
|
||||
groups.push([])
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
groups[i % pattern.length].push(list[i])
|
||||
}
|
||||
for (let x = 0; x < list.length; x++) {
|
||||
for (let g of pattern) {
|
||||
for (let i = 0; i < g; i++) {
|
||||
async function altSort(list, pattern, realtime) {
|
||||
let groups = [];
|
||||
let newList = [];
|
||||
let frameCount = 0;
|
||||
let oldPath;
|
||||
let newName;
|
||||
let newPath;
|
||||
let ext = path.extname(list[0]);
|
||||
for (let g of pattern) {
|
||||
groups.push([]);
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
groups[i % pattern.length].push(list[i]);
|
||||
}
|
||||
for (let x = 0; x < list.length; x++) {
|
||||
for (let g of pattern) {
|
||||
for (let i = 0; i < g; i++) {
|
||||
/*oldPath = path.join(TMPPATH, list[i]);
|
||||
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
||||
newPath = path.join(TMPPATH, newName);
|
||||
|
||||
/*oldPath = path.join(TMPPATH, list[i]);
|
||||
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
||||
newPath = path.join(TMPPATH, newName);
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`);
|
||||
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`);
|
||||
|
||||
try {
|
||||
//await fs.move(oldPath, newPath, { overwrite: true })
|
||||
newList.push(newName);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}*/
|
||||
|
||||
frameCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return newList
|
||||
try {
|
||||
//await fs.move(oldPath, newPath, { overwrite: true })
|
||||
newList.push(newName);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}*/
|
||||
frameCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
/**
|
||||
* Standard frame sorting method.
|
||||
|
@ -298,267 +299,254 @@ async function altSort (list, pattern, realtime) {
|
|||
* @param {array} list List of slices to group
|
||||
* @param {array} pattern Array representing pattern
|
||||
* @param {boolean} realtime Flag to group with "realtime" behavior
|
||||
*
|
||||
* @returns {array} Sorted array of frames
|
||||
**/
|
||||
async function standardSort (list, pattern, realtime) {
|
||||
let sliceCount = 0
|
||||
let stepCount
|
||||
let step
|
||||
let skipCount
|
||||
let skip
|
||||
let ext = path.extname(list[0])
|
||||
let oldPath
|
||||
let newName
|
||||
let newPath
|
||||
let newList = []
|
||||
|
||||
if (realtime) {
|
||||
skip = false
|
||||
skipCount = pattern.length + 1
|
||||
}
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (realtime) {
|
||||
skipCount--;
|
||||
if (skipCount === 0) {
|
||||
skip = !skip;
|
||||
skipCount = pattern.length
|
||||
}
|
||||
}
|
||||
|
||||
oldPath = path.join(TMPPATH, list[i])
|
||||
|
||||
if (skip) {
|
||||
console.log(`Skipping ${list[i]}`)
|
||||
try {
|
||||
await fs.unlink(oldPath)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
newName = `./render_${zeroPad(sliceCount)}${ext}`
|
||||
newPath = path.join(TMPPATH, newName)
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`)
|
||||
|
||||
try {
|
||||
await fs.move(oldPath, newPath)
|
||||
newList.push(newName)
|
||||
sliceCount++
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(10)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return newList
|
||||
async function standardSort(list, pattern, realtime) {
|
||||
let sliceCount = 0;
|
||||
let stepCount;
|
||||
let step;
|
||||
let skipCount;
|
||||
let skip;
|
||||
let ext = path.extname(list[0]);
|
||||
let oldPath;
|
||||
let newName;
|
||||
let newPath;
|
||||
let newList = [];
|
||||
if (realtime) {
|
||||
skip = false;
|
||||
skipCount = pattern.length + 1;
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (realtime) {
|
||||
skipCount--;
|
||||
if (skipCount === 0) {
|
||||
skip = !skip;
|
||||
skipCount = pattern.length;
|
||||
}
|
||||
}
|
||||
oldPath = path.join(TMPPATH, list[i]);
|
||||
if (skip) {
|
||||
console.log(`Skipping ${list[i]}`);
|
||||
try {
|
||||
await fs.unlink(oldPath);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
newName = `./render_${zeroPad(sliceCount)}${ext}`;
|
||||
newPath = path.join(TMPPATH, newName);
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`);
|
||||
try {
|
||||
await fs.move(oldPath, newPath);
|
||||
newList.push(newName);
|
||||
sliceCount++;
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(10);
|
||||
}
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
/**
|
||||
* Ramdomly sort frames for re-stitching.
|
||||
*
|
||||
*
|
||||
* @param {array} list List of frames to group
|
||||
* @param {array} pattern Array representing pattern
|
||||
* @param {boolean} realtime Flag to group with "realtime" behavior
|
||||
*
|
||||
* @returns {array} Sorted array of frames
|
||||
**/
|
||||
async function randomSort (list, pattern, realtime) {
|
||||
let sliceCount = 0
|
||||
let ext = path.extname(list[0])
|
||||
let oldPath
|
||||
let newName
|
||||
let newPath
|
||||
let newList = []
|
||||
let removeLen = 0
|
||||
let remove = []
|
||||
|
||||
shuffle(list)
|
||||
|
||||
if (realtime) {
|
||||
removeLen = Math.floor(list.length / pattern.length)
|
||||
remove = list.slice(removeLen, list.length)
|
||||
list = list.slice(0, removeLen)
|
||||
|
||||
console.log(`Skipping extra slices...`)
|
||||
for (let i = 0; i < remove.length; i++) {
|
||||
oldPath = path.join(TMPPATH, remove[i])
|
||||
console.log(`Skipping ${list[i]}`)
|
||||
try {
|
||||
await fs.unlink(oldPath)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
oldPath = path.join(TMPPATH, list[i])
|
||||
|
||||
newName = `./render_${zeroPad(sliceCount)}${ext}`
|
||||
newPath = path.join(TMPPATH, newName)
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`)
|
||||
|
||||
try {
|
||||
await fs.move(oldPath, newPath)
|
||||
newList.push(newName)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
sliceCount++
|
||||
}
|
||||
|
||||
return newList
|
||||
async function randomSort(list, pattern, realtime) {
|
||||
let sliceCount = 0;
|
||||
let ext = path.extname(list[0]);
|
||||
let oldPath;
|
||||
let newName;
|
||||
let newPath;
|
||||
let newList = [];
|
||||
let removeLen = 0;
|
||||
let remove = [];
|
||||
shuffle(list);
|
||||
if (realtime) {
|
||||
removeLen = Math.floor(list.length / pattern.length);
|
||||
remove = list.slice(removeLen, list.length);
|
||||
list = list.slice(0, removeLen);
|
||||
console.log(`Skipping extra slices...`);
|
||||
for (let i = 0; i < remove.length; i++) {
|
||||
oldPath = path.join(TMPPATH, remove[i]);
|
||||
console.log(`Skipping ${list[i]}`);
|
||||
try {
|
||||
await fs.unlink(oldPath);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
oldPath = path.join(TMPPATH, list[i]);
|
||||
newName = `./render_${zeroPad(sliceCount)}${ext}`;
|
||||
newPath = path.join(TMPPATH, newName);
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`);
|
||||
try {
|
||||
await fs.move(oldPath, newPath);
|
||||
newList.push(newName);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
sliceCount++;
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
/**
|
||||
* Render the frames into a video using ffmpeg.
|
||||
*
|
||||
* @param {string} output Path to export the video to
|
||||
**/
|
||||
async function render (allSlices, output) {
|
||||
let exe = EXE
|
||||
let cmd
|
||||
|
||||
allSlices = allSlices.map(file => {
|
||||
return path.join(TMPPATH, file)
|
||||
})
|
||||
|
||||
cmd = `${exe} ${allSlices.join(' ')} ${output}`
|
||||
|
||||
console.log(`Exporting audio ${output}`)
|
||||
console.log(cmd)
|
||||
|
||||
/*try {
|
||||
await exec(`ls "${TMPPATH}"`)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}*/
|
||||
|
||||
try {
|
||||
await exec(cmd)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
async function render(allSlices, output) {
|
||||
let ext = path.extname(allSlices[0]);
|
||||
let partSize = 500;
|
||||
let partCount = Math.ceil(allSlices.length / partSize);
|
||||
let partName;
|
||||
let partFile;
|
||||
let parts = [];
|
||||
allSlices = allSlices.map(file => {
|
||||
return path.join(TMPPATH, file);
|
||||
});
|
||||
if (partCount < 2) {
|
||||
return await arrToFile(allSlices, output);
|
||||
}
|
||||
for (let part = 0; part < partCount; part++) {
|
||||
partName = `./render_part_${zeroPad(part)}${ext}`;
|
||||
partFile = path.join(TMPPATH, partName);
|
||||
await arrToFile(allSlices.slice(part * partSize, (part + 1) * partSize), partFile);
|
||||
parts.push(partFile);
|
||||
//process.exit()
|
||||
}
|
||||
return await arrToFile(parts, output);
|
||||
}
|
||||
async function arrToFile(arr, output) {
|
||||
let exe = EXE;
|
||||
let cmd = `${exe} ${arr.join(' ')} ${output}`;
|
||||
console.log(`Exporting audio ${output}`);
|
||||
console.log(cmd);
|
||||
try {
|
||||
await exec(cmd);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the arguments and runs the process of exporting, sorting and then
|
||||
* "weaving" the slices back into an audio file
|
||||
*
|
||||
*
|
||||
* @param {object} arg Object containing all arguments
|
||||
**/
|
||||
async function main (arg) {
|
||||
let input = arg.input.split(':')
|
||||
let output = arg.output
|
||||
let pattern = []
|
||||
let realtime = false
|
||||
let random = false
|
||||
let allSlices
|
||||
let len
|
||||
console.time('audioloom')
|
||||
|
||||
if (input.length < 2) {
|
||||
console.error('Must provide more than 1 input')
|
||||
return process.exit(1)
|
||||
}
|
||||
|
||||
if (!output) {
|
||||
console.error('Must provide audio output path')
|
||||
return process.exit(2)
|
||||
}
|
||||
|
||||
if (arg.random) {
|
||||
random = true
|
||||
}
|
||||
|
||||
if (arg.tmp) {
|
||||
TMPDIR = arg.tmp
|
||||
}
|
||||
|
||||
if (arg.fps) {
|
||||
SLICE = (1000 / parseFloat(arg.fps)) + ''
|
||||
}
|
||||
|
||||
// ms overrides fps
|
||||
if (arg.ms) {
|
||||
SLICE = (1000 / parseFloat(arg.ms)) + ''
|
||||
}
|
||||
|
||||
if (arg.pattern) {
|
||||
pattern = arg.pattern.split(':')
|
||||
pattern = pattern.map(el =>{
|
||||
return parseInt(el);
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i <input.length; i++) {
|
||||
pattern.push(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (arg.realtime) realtime = true
|
||||
|
||||
TMPPATH = path.join(TMPDIR, 'audioloom')
|
||||
|
||||
try {
|
||||
await clear()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(3)
|
||||
}
|
||||
|
||||
console.log(`Processing audio files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`)
|
||||
|
||||
for (let i = 0; i <input.length; i++) {
|
||||
try {
|
||||
len = await audioLength(input[i])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(4)
|
||||
}
|
||||
try {
|
||||
await slices(input[i], len, i)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(4)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
allSlices = await weave(pattern, realtime, random)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(5)
|
||||
}
|
||||
|
||||
try {
|
||||
await render(allSlices, output)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(6)
|
||||
}
|
||||
|
||||
try {
|
||||
await clear()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(7)
|
||||
}
|
||||
|
||||
console.timeEnd('audioloom')
|
||||
async function main(arg) {
|
||||
let input = arg.input.split(':');
|
||||
let output = arg.output;
|
||||
let pattern = [];
|
||||
let realtime = false;
|
||||
let random = false;
|
||||
let allSlices;
|
||||
let len;
|
||||
console.time('audioloom');
|
||||
if (input.length < 2) {
|
||||
console.error('Must provide more than 1 input');
|
||||
return process.exit(1);
|
||||
}
|
||||
if (!output) {
|
||||
console.error('Must provide audio output path');
|
||||
return process.exit(2);
|
||||
}
|
||||
if (arg.random) {
|
||||
random = true;
|
||||
}
|
||||
if (arg.tmp) {
|
||||
TMPDIR = arg.tmp;
|
||||
}
|
||||
if (arg.fps) {
|
||||
SLICE = (1000 / parseFloat(arg.fps)) + '';
|
||||
}
|
||||
// ms overrides fps
|
||||
if (arg.ms) {
|
||||
SLICE = (1000 / parseFloat(arg.ms)) + '';
|
||||
}
|
||||
if (arg.pattern) {
|
||||
pattern = arg.pattern.split(':');
|
||||
pattern = pattern.map(function (el) {
|
||||
return parseInt(el);
|
||||
});
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
pattern.push(1);
|
||||
}
|
||||
}
|
||||
if (arg.realtime)
|
||||
realtime = true;
|
||||
TMPPATH = path.join(TMPDIR, 'audioloom');
|
||||
try {
|
||||
await clear();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(3);
|
||||
}
|
||||
console.log(`Processing audio files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`);
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
try {
|
||||
len = await audioLength(input[i]);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(4);
|
||||
}
|
||||
try {
|
||||
await slices(input[i], len, i);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(4);
|
||||
}
|
||||
}
|
||||
try {
|
||||
allSlices = await weave(pattern, realtime, random);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(5);
|
||||
}
|
||||
try {
|
||||
await render(allSlices, output);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(6);
|
||||
}
|
||||
try {
|
||||
await clear();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return process.exit(7);
|
||||
}
|
||||
console.timeEnd('audioloom');
|
||||
}
|
||||
|
||||
program
|
||||
.version(packageJson.version)
|
||||
.option('-i, --input [files]', 'Specify input audio files with paths seperated by colon')
|
||||
.option('-o, --output [file]', 'Specify output path of audio file')
|
||||
.option('-p, --pattern [pattern]', 'Specify a pattern for the flicker 1:1 is standard')
|
||||
.option('-r, --realtime', 'Specify if audio files should preserve realtime speed')
|
||||
.option('-t, --tmp [dir]', 'Specify tmp directory for exporting slices')
|
||||
|
||||
.option('-f, --fps [fps]', 'Specify length of slices using an FPS value')
|
||||
.option('-m, --ms [ms]', 'Specify length of slices using length in milliseconds')
|
||||
|
||||
.option('-R, --random', 'Randomize frames. Ignores pattern if included')
|
||||
.parse(process.argv)
|
||||
|
||||
main(program)
|
||||
.version(packageJson.version)
|
||||
.option('-i, --input [files]', 'Specify input audio files with paths seperated by colon')
|
||||
.option('-o, --output [file]', 'Specify output path of audio file')
|
||||
.option('-p, --pattern [pattern]', 'Specify a pattern for the flicker 1:1 is standard')
|
||||
.option('-r, --realtime', 'Specify if audio files should preserve realtime speed')
|
||||
.option('-t, --tmp [dir]', 'Specify tmp directory for exporting slices')
|
||||
.option('-f, --fps [fps]', 'Specify length of slices using an FPS value')
|
||||
.option('-m, --ms [ms]', 'Specify length of slices using length in milliseconds')
|
||||
.option('-R, --random', 'Randomize frames. Ignores pattern if included')
|
||||
.parse(process.argv);
|
||||
main(program);
|
||||
|
|
|
@ -26,6 +26,12 @@
|
|||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.12.0.tgz",
|
||||
"integrity": "sha512-Lg00egj78gM+4aE0Erw05cuDbvX9sLJbaaPwwRtdCdAMnIudqrQZ0oZX98Ek0yiSK/A2nubHgJfvII/rTT2Dwg==",
|
||||
"dev": true
|
||||
},
|
||||
"ajv": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
|
@ -2986,6 +2992,12 @@
|
|||
"prelude-ls": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.3.4000",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz",
|
||||
"integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==",
|
||||
"dev": true
|
||||
},
|
||||
"typical": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz",
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
"description": "Node script for interweaving audio files together",
|
||||
"main": "audioloom",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "node ./build.js",
|
||||
"compile": "./node_modules/.bin/tsc src/audioloom.ts --outFile ./audioloom --noImplicitAny -t ES2017"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -21,8 +23,10 @@
|
|||
"fs-extra": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^11.12.0",
|
||||
"jsdoc-to-markdown": "^4.0.1",
|
||||
"pkg": "^4.3.5",
|
||||
"qunit": "^2.8.0"
|
||||
"qunit": "^2.8.0",
|
||||
"typescript": "^3.3.4000"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,593 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
const execRaw = require('child_process').exec
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const program = require('commander')
|
||||
const fs = require('fs-extra')
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
let TMPDIR : string = os.tmpdir() || '/tmp'
|
||||
let TMPPATH : string
|
||||
|
||||
let EXE : string = `sox`
|
||||
let IDENTIFY : string = `soxi`
|
||||
let SLICE : string = (1000 / 24) + ''
|
||||
|
||||
/**
|
||||
* Shells out to execute a command with async/await.
|
||||
* Async wrapper to exec module.
|
||||
*
|
||||
* @param {string} cmd Command to execute
|
||||
*
|
||||
* @returns {Promise} Promise containing the complete stdio
|
||||
**/
|
||||
async function exec (cmd : string) {
|
||||
return new Promise((resolve : any, reject : any) => {
|
||||
return execRaw(cmd, (err : any, stdio : string, stderr : string) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(stdio)
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Delays process for specified amount of time in milliseconds.
|
||||
*
|
||||
* @param {integer} ms Milliseconds to delay for
|
||||
*
|
||||
* @returns {Promise} Promise that resolves after set time
|
||||
**/
|
||||
async function delay (ms : number) {
|
||||
return new Promise((resolve, reject) =>{
|
||||
return setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Pads a numerical value with preceding zeros to make strings same length.
|
||||
*
|
||||
* @param {integer} i Number to pad
|
||||
* @param {integer} max (optional) Maximum length of string to pad to
|
||||
*
|
||||
* @returns {string} Padded number as a string
|
||||
**/
|
||||
function zeroPad (i : number, max : number = 5) {
|
||||
const len : number = (i + '').length
|
||||
let str : string = i + ''
|
||||
for (let x : number = 0; x < max - len; x++) {
|
||||
str = '0' + str
|
||||
}
|
||||
return str
|
||||
}
|
||||
/**
|
||||
* Shuffles an array into a random state.
|
||||
*
|
||||
* @param {array} a Array to randomize
|
||||
**/
|
||||
function shuffle (array : any[]) {
|
||||
let j : any
|
||||
let temp : any
|
||||
for (let i : number = array.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1))
|
||||
temp = array[i]
|
||||
array[i] = array[j]
|
||||
array[j] = temp
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clears the temporary directory of all files.
|
||||
* Establishes a directory if none exists.
|
||||
**/
|
||||
async function clear () {
|
||||
let cmd : string = `rm -r "${TMPPATH}"`
|
||||
let exists : boolean
|
||||
|
||||
try {
|
||||
exists = await fs.exists(TMPPATH)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
console.log(`Clearing temp directory "${TMPPATH}"`)
|
||||
try {
|
||||
await exec(cmd)
|
||||
} catch (err) {
|
||||
//suppress error
|
||||
console.dir(err)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(TMPPATH)
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
/**
|
||||
* Parses timecode string to float of total seconds
|
||||
*
|
||||
* @param {string} str Timecode string to parse
|
||||
*
|
||||
* @returns {float} Seconds at float
|
||||
**/
|
||||
function parseTC (str : string) {
|
||||
const parts : any [] = str.split(':')
|
||||
let sec : number = 0
|
||||
if (parts[0] != 0) sec += parseFloat(parts[0]) * 60 * 60
|
||||
if (parts[1] != 0) sec += parseFloat(parts[1]) * 60
|
||||
if (parts[2] != 0) sec += parseFloat(parts[2])
|
||||
return sec
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns offset position of audio file to slice at.
|
||||
*
|
||||
* @param {integer} i Count of slice to make
|
||||
* @param {float} slice Length of slice, might be str
|
||||
*
|
||||
* @returns {str} New position cast as string
|
||||
**/
|
||||
async function audioLength (filePath : string) {
|
||||
const exe : string = IDENTIFY
|
||||
const cmd : string = `${exe} -d "${filePath}"`
|
||||
let str : any
|
||||
|
||||
try {
|
||||
str = await exec(cmd)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(11)
|
||||
}
|
||||
return parseTC(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns offset position of audio file to slice at.
|
||||
*
|
||||
* @param {integer} i Count of slice to make
|
||||
* @param {float} slice Length of slice, might be str
|
||||
*
|
||||
* @returns {str} New position cast as string
|
||||
**/
|
||||
|
||||
function offset (i : number, slice : any) {
|
||||
return (i * (parseFloat(slice) / 1000)) + ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all slices from audio file. Appends number to the string
|
||||
* to keep slices in alternating order to be quickly stitched together
|
||||
* or re-sorted.
|
||||
*
|
||||
* @param {string} file String representing path to audio file
|
||||
* @param {float} len Length of the slice to make
|
||||
* @param {integer} order Integer to be appended to pathname of file
|
||||
*
|
||||
* @returns {string} String with the export order, not sure why I did this
|
||||
**/
|
||||
async function slices (file : string, len : number, order : number) {
|
||||
let ext : string = 'wav'
|
||||
let exe : string = EXE
|
||||
let slice : string = SLICE
|
||||
let tmpoutput : string
|
||||
let cmd : string
|
||||
let i : number = 0
|
||||
let total = Math.floor((len * 1000) / parseFloat(slice))
|
||||
console.log(`Exporting ${file} as ${total} slices ${SLICE}ms long...`)
|
||||
|
||||
for (i = 0; i < total; i++) {
|
||||
tmpoutput = path.join(TMPPATH, `export-${zeroPad(i)}_${order}.${ext}`)
|
||||
cmd = `${exe} "${file}" "${tmpoutput}" trim ${offset(i, slice)} ${parseFloat(slice) / 1000}`
|
||||
try {
|
||||
console.log(cmd)
|
||||
await exec(cmd)
|
||||
} catch (err) {
|
||||
console.error('Error exporting file', err)
|
||||
return process.exit(3)
|
||||
}
|
||||
}
|
||||
return path.join(TMPPATH, `export-%05d_${order}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-arranges the slices into the order specified in the pattern.
|
||||
* Calls `patternSort()` to perform the rename and unlink actions
|
||||
*
|
||||
* @param {array} pattern Pattern of the slices per input
|
||||
* @param {boolean} realtime Flag to turn on or off realtime behavior (drop slice / number of files)
|
||||
* @param {boolean} random Flag to turn on or off random behavior
|
||||
*
|
||||
* @returns {array} Array of frame paths
|
||||
**/
|
||||
async function weave (pattern : number[], realtime : boolean, random : boolean) {
|
||||
let slices : string[]
|
||||
let seq : string[]
|
||||
let ext : string = '.wav'
|
||||
let alt : boolean = false
|
||||
|
||||
console.log('Weaving slices...')
|
||||
|
||||
try {
|
||||
slices = await fs.readdir(TMPPATH)
|
||||
} catch (err) {
|
||||
console.error('Error reading tmp directory', err)
|
||||
}
|
||||
|
||||
//console.dir(slices)
|
||||
slices = slices.filter (file => {
|
||||
if (file.indexOf(ext) !== -1) return true
|
||||
})
|
||||
|
||||
for (let el of pattern) {
|
||||
if (el !== 1) alt = true
|
||||
}
|
||||
|
||||
if (random){
|
||||
try {
|
||||
seq = await randomSort(slices, pattern, realtime)
|
||||
} catch (err) {
|
||||
console.error('Error sorting slices')
|
||||
}
|
||||
} else if (!alt) {
|
||||
try {
|
||||
seq = await standardSort(slices, pattern, realtime)
|
||||
} catch (err) {
|
||||
console.error('Error sorting slices')
|
||||
}
|
||||
} else if (alt) {
|
||||
console.warn('This feature is not ready, please check https://github.com/sixteenmillimeter/audioloom.git')
|
||||
process.exit(10)
|
||||
try {
|
||||
seq = await altSort(slices, pattern, realtime)
|
||||
} catch (err) {
|
||||
console.error('Error sorting slices')
|
||||
}
|
||||
}
|
||||
//console.dir(seq)
|
||||
return seq
|
||||
}
|
||||
/**
|
||||
* TODO
|
||||
* Alternate slice sorting method.
|
||||
*
|
||||
* @param {array} list List of slices to group
|
||||
* @param {array} pattern Array representing pattern
|
||||
* @param {boolean} realtime Flag to group with "realtime" behavior
|
||||
*
|
||||
* @returns {array} Sorted array of frames
|
||||
**/
|
||||
async function altSort (list : string[], pattern : number[], realtime : boolean) {
|
||||
let groups : any[] = []
|
||||
let newList : string[] = []
|
||||
let frameCount : number = 0
|
||||
let oldPath : string
|
||||
let newName : string
|
||||
let newPath : string
|
||||
let ext : string = path.extname(list[0])
|
||||
|
||||
for (let g of pattern) {
|
||||
groups.push([])
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
groups[i % pattern.length].push(list[i])
|
||||
}
|
||||
for (let x : number = 0; x < list.length; x++) {
|
||||
for (let g of pattern) {
|
||||
for (let i = 0; i < g; i++) {
|
||||
|
||||
/*oldPath = path.join(TMPPATH, list[i]);
|
||||
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
||||
newPath = path.join(TMPPATH, newName);
|
||||
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`);
|
||||
|
||||
try {
|
||||
//await fs.move(oldPath, newPath, { overwrite: true })
|
||||
newList.push(newName);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}*/
|
||||
|
||||
frameCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
/**
|
||||
* Standard frame sorting method.
|
||||
*
|
||||
* @param {array} list List of slices to group
|
||||
* @param {array} pattern Array representing pattern
|
||||
* @param {boolean} realtime Flag to group with "realtime" behavior
|
||||
*
|
||||
* @returns {array} Sorted array of frames
|
||||
**/
|
||||
async function standardSort (list : string[], pattern : number[], realtime : boolean) {
|
||||
let sliceCount : number = 0
|
||||
let stepCount : number
|
||||
let step : number
|
||||
let skipCount : number
|
||||
let skip : boolean
|
||||
let ext : string = path.extname(list[0])
|
||||
let oldPath : string
|
||||
let newName : string
|
||||
let newPath : string
|
||||
let newList : string[] = []
|
||||
|
||||
if (realtime) {
|
||||
skip = false
|
||||
skipCount = pattern.length + 1
|
||||
}
|
||||
|
||||
for (let i : number = 0; i < list.length; i++) {
|
||||
if (realtime) {
|
||||
skipCount--;
|
||||
if (skipCount === 0) {
|
||||
skip = !skip;
|
||||
skipCount = pattern.length
|
||||
}
|
||||
}
|
||||
|
||||
oldPath = path.join(TMPPATH, list[i])
|
||||
|
||||
if (skip) {
|
||||
console.log(`Skipping ${list[i]}`)
|
||||
try {
|
||||
await fs.unlink(oldPath)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
newName = `./render_${zeroPad(sliceCount)}${ext}`
|
||||
newPath = path.join(TMPPATH, newName)
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`)
|
||||
|
||||
try {
|
||||
await fs.move(oldPath, newPath)
|
||||
newList.push(newName)
|
||||
sliceCount++
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(10)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return newList
|
||||
}
|
||||
/**
|
||||
* Ramdomly sort frames for re-stitching.
|
||||
*
|
||||
* @param {array} list List of frames to group
|
||||
* @param {array} pattern Array representing pattern
|
||||
* @param {boolean} realtime Flag to group with "realtime" behavior
|
||||
*
|
||||
* @returns {array} Sorted array of frames
|
||||
**/
|
||||
async function randomSort (list : string[], pattern : number[], realtime : boolean) {
|
||||
let sliceCount : number = 0
|
||||
let ext : string = path.extname(list[0])
|
||||
let oldPath : string
|
||||
let newName : string
|
||||
let newPath : string
|
||||
let newList : string[] = []
|
||||
let removeLen : number = 0
|
||||
let remove : string[] = []
|
||||
|
||||
shuffle(list)
|
||||
|
||||
if (realtime) {
|
||||
removeLen = Math.floor(list.length / pattern.length)
|
||||
remove = list.slice(removeLen, list.length)
|
||||
list = list.slice(0, removeLen)
|
||||
|
||||
console.log(`Skipping extra slices...`)
|
||||
for (let i = 0; i < remove.length; i++) {
|
||||
oldPath = path.join(TMPPATH, remove[i])
|
||||
console.log(`Skipping ${list[i]}`)
|
||||
try {
|
||||
await fs.unlink(oldPath)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i : number = 0; i < list.length; i++) {
|
||||
oldPath = path.join(TMPPATH, list[i])
|
||||
|
||||
newName = `./render_${zeroPad(sliceCount)}${ext}`
|
||||
newPath = path.join(TMPPATH, newName)
|
||||
console.log(`Renaming ${list[i]} -> ${newName}`)
|
||||
|
||||
try {
|
||||
await fs.move(oldPath, newPath)
|
||||
newList.push(newName)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
sliceCount++
|
||||
}
|
||||
|
||||
return newList
|
||||
}
|
||||
/**
|
||||
* Render the frames into a video using ffmpeg.
|
||||
*
|
||||
* @param {string} output Path to export the video to
|
||||
**/
|
||||
async function render (allSlices : string[], output : string) {
|
||||
let ext : string = path.extname(allSlices[0])
|
||||
let partSize = 500
|
||||
let partCount : number = Math.ceil(allSlices.length / partSize);
|
||||
let partName : string
|
||||
let partFile : string
|
||||
let parts : string[] = []
|
||||
|
||||
|
||||
allSlices = allSlices.map(file => {
|
||||
return path.join(TMPPATH, file)
|
||||
})
|
||||
|
||||
if (partCount < 2) {
|
||||
return await arrToFile(allSlices, output);
|
||||
}
|
||||
|
||||
for (let part : number = 0; part < partCount; part++) {
|
||||
partName = `./render_part_${zeroPad(part)}${ext}`
|
||||
partFile = path.join(TMPPATH, partName)
|
||||
await arrToFile(allSlices.slice(part * partSize, (part + 1) * partSize), partFile);
|
||||
parts.push(partFile);
|
||||
//process.exit()
|
||||
}
|
||||
|
||||
return await arrToFile(parts, output);
|
||||
|
||||
}
|
||||
|
||||
async function arrToFile (arr : string[], output : string) {
|
||||
let exe : string = EXE
|
||||
let cmd : string = `${exe} ${arr.join(' ')} ${output}`
|
||||
|
||||
console.log(`Exporting audio ${output}`)
|
||||
console.log(cmd)
|
||||
|
||||
try {
|
||||
await exec(cmd)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the arguments and runs the process of exporting, sorting and then
|
||||
* "weaving" the slices back into an audio file
|
||||
*
|
||||
* @param {object} arg Object containing all arguments
|
||||
**/
|
||||
async function main (arg : any) {
|
||||
let input : string[] = arg.input.split(':')
|
||||
let output : string = arg.output
|
||||
let pattern : number[] = []
|
||||
let realtime : boolean = false
|
||||
let random : boolean = false
|
||||
let allSlices : string []
|
||||
let len : number
|
||||
console.time('audioloom')
|
||||
|
||||
if (input.length < 2) {
|
||||
console.error('Must provide more than 1 input')
|
||||
return process.exit(1)
|
||||
}
|
||||
|
||||
if (!output) {
|
||||
console.error('Must provide audio output path')
|
||||
return process.exit(2)
|
||||
}
|
||||
|
||||
if (arg.random) {
|
||||
random = true
|
||||
}
|
||||
|
||||
if (arg.tmp) {
|
||||
TMPDIR = arg.tmp
|
||||
}
|
||||
|
||||
if (arg.fps) {
|
||||
SLICE = (1000 / parseFloat(arg.fps)) + ''
|
||||
}
|
||||
|
||||
// ms overrides fps
|
||||
if (arg.ms) {
|
||||
SLICE = (1000 / parseFloat(arg.ms)) + ''
|
||||
}
|
||||
|
||||
if (arg.pattern) {
|
||||
pattern = arg.pattern.split(':')
|
||||
pattern = pattern.map(function (el : any) {
|
||||
return parseInt(el)
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i <input.length; i++) {
|
||||
pattern.push(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (arg.realtime) realtime = true
|
||||
|
||||
TMPPATH = path.join(TMPDIR, 'audioloom')
|
||||
|
||||
try {
|
||||
await clear()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(3)
|
||||
}
|
||||
|
||||
console.log(`Processing audio files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`)
|
||||
|
||||
for (let i : number = 0; i <input.length; i++) {
|
||||
try {
|
||||
len = await audioLength(input[i])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(4)
|
||||
}
|
||||
try {
|
||||
await slices(input[i], len, i)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(4)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
allSlices = await weave(pattern, realtime, random)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(5)
|
||||
}
|
||||
|
||||
try {
|
||||
await render(allSlices, output)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(6)
|
||||
}
|
||||
|
||||
try {
|
||||
await clear()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return process.exit(7)
|
||||
}
|
||||
|
||||
console.timeEnd('audioloom')
|
||||
}
|
||||
|
||||
program
|
||||
.version(packageJson.version)
|
||||
.option('-i, --input [files]', 'Specify input audio files with paths seperated by colon')
|
||||
.option('-o, --output [file]', 'Specify output path of audio file')
|
||||
.option('-p, --pattern [pattern]', 'Specify a pattern for the flicker 1:1 is standard')
|
||||
.option('-r, --realtime', 'Specify if audio files should preserve realtime speed')
|
||||
.option('-t, --tmp [dir]', 'Specify tmp directory for exporting slices')
|
||||
|
||||
.option('-f, --fps [fps]', 'Specify length of slices using an FPS value')
|
||||
.option('-m, --ms [ms]', 'Specify length of slices using length in milliseconds')
|
||||
|
||||
.option('-R, --random', 'Randomize frames. Ignores pattern if included')
|
||||
.parse(process.argv)
|
||||
|
||||
main(program)
|
Loading…
Reference in New Issue