Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor #530 [BLENDER] Waveform #531

Merged
merged 1 commit into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,6 @@ export NODE_OPTIONS="--max-old-space-size=8192"
pnpm install
node initDB.js jsons/exportDataEmpty.json
```
### Waveform generation
We use [audiowaveform](https://github.com/bbc/audiowaveform/tree/master?fbclid=IwAR19jDCDp5DCzdLK7Z8EgE7W5NSjm8B-wdFABsrT62D2b80bVtCuydkMgnM) to generate waveform. Refer to [Installation](https://github.com/bbc/audiowaveform/blob/master/README.md#installation) section to install the package first.

259 changes: 22 additions & 237 deletions utils/generateWaveform.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,15 @@
const fs = require("fs");
const process = require("process");
const path = require("path");
const axios = require("axios");

const child_process = require("child_process");

const sharp = require("sharp");
const jm = require("join-images");
const readline = require("readline");
const { getAudioDurationInSeconds } = require("get-audio-duration");

// the annotation part can be used when the file is a module

// import { createWriteStream, existsSync, unlink } from "fs";
// import { platform } from "process";
// import { join, dirname, basename } from "path";
// import path from 'path';
// import axios from "axios";
// import { fileURLToPath } from 'url';
// import { execSync } from 'child_process';
// const url = require("url");

// const __filename = url.fileURLToPath(import.meta.url);
// const __dirname = path.dirname(__filename);
// const __dirname = path.dirname(__filename);

// procedure: check existance->download->construct command->generate waveform

// const img_res = '4000x1000';
let img_milisec_per_pin = 4;
let img_res_width = 40000;
let img_res_height = 400;
let img_res = String(img_res_width) + "x" + String(img_res_height);
let music_duration = 420000;
// console.log(`img_res = ${img_res}`);
const hex_colo = "3D82B1";
const sname = "../files/music/waveform.png";
const sname_tmp = "../files/music/waveform_tmp.png";
const sname = "../files/data/wavform.json";
const musicName = "../files/music/2023.mp3"; // you can alter the music name here
let ifp = path.join(String(__dirname), sname); // name (path included) to the waveform image
let ifp_temp = path.join(String(__dirname), sname_tmp); // name (path included) to the waveform image
let sfp = path.join(String(__dirname), musicName); // name (path included) to the music
const leftName = "../files/music/leftWaveform.png";
const rightName = "../files/music/rightWaveform.png";
const lcp = path.join(String(__dirname), leftName);
const rcp = path.join(String(__dirname), rightName);
let releaseUrl = ""; // url for downloading ffmpeg
let ffbin = ""; // the path name(file name included) of the ffmpeg binary
let cmd = []; // the command that will later be used to generate waveform

const readLength = async () => {
await getAudioDurationInSeconds(sfp).then((duration) => {
music_duration = duration * 1000;
console.log(`duration = ${music_duration} miliseconds`);
});
};
let pixelsPerSecond = 1000;


const rl = readline.createInterface({
Expand All @@ -65,13 +20,17 @@ const rl = readline.createInterface({
const readIntegerInput = (prompt) => {
return new Promise((resolve, reject) => {
rl.question(prompt, (input) => {

const parsedInput = parseInt(input, 10);
if (!isNaN(parsedInput)) {
resolve(parsedInput);
} else {
console.log("Invalid input, try again...");
resolve(readIntegerInput(prompt));
// reject(new Error('Invalid input. Please enter a valid integer.'));
if (input == "") {
resolve(1000);
} else {
console.log("Invalid input, try again...");
resolve(readIntegerInput(prompt));
}
}
});
});
Expand All @@ -80,12 +39,10 @@ const readIntegerInput = (prompt) => {
const readInput = async () => {

try {
img_milisec_per_pin = await readIntegerInput("Enter time duration(miliseconds) in one pixel: ");
img_res_width = Math.round(music_duration / img_milisec_per_pin);
// img_res_width = await readIntegerInput('Enter the width of the waveform: ');
// img_res_height = await readIntegerInput('Enter the height of the waveform: ');
img_res = img_res_width + "x" + img_res_height;
console.log(`Waveform resolution: ${img_res}`);
pixelsPerSecond = await readIntegerInput("Enter pixels-per-second(1000): ");
if (!(typeof pixelsPerSecond === 'number' && isFinite(pixelsPerSecond))) {
pixelsPerSecond = 1000;
}
} catch (error) {
console.log("something wrong in the input...");
console.error(error.message);
Expand All @@ -94,16 +51,13 @@ const readInput = async () => {
}
};

const generateWav = () => {

const callFFMPEG = () => {
cmd = ["sudo", "ffmpeg"];
cmd = [...cmd, "-i", String(sfp),
"-hide_banner", "-loglevel", "error", "-filter_complex",
`showwavespic=s=${img_res}:split_channels=1:colors=${hex_colo}`,
"-frames:v", "1", "-y", String(ifp_temp)];
cmd = ["sudo", "audiowaveform"];
cmd = [...cmd, "-i", String(sfp), "-o", String(ifp), "--pixels-per-second", String(pixelsPerSecond)];

let cmdString = cmd.join(" ");
console.log(`cmd = ${cmd.join(" ")}`);
console.log(`cmd = ${cmdString}`);
let startGenerationTime = Date.now();
console.log("start generating");
try {
Expand All @@ -116,178 +70,9 @@ const callFFMPEG = () => {
console.log(`Time taken for generating waveform: ${elapsedTime} ms`);
};

// download from url and save as dest, then generate waveform
const dl_url = async (url, dest, ffmpegExistance) => {
await readInput(img_res_width, img_res_height);
if (!ffmpegExistance) {
let start_time = Date.now();
console.log("downloading ffmpeg...");
await axios({
method: "get",
url: url,
responseType: "stream",
// httpsAgent: {
// rejectUnauthorized: false, // Disable SSL verification
// },
}).then((response) => {
const writer = fs.createWriteStream(dest);
response.data.pipe(writer);
// console.log("here1");
return new Promise((resolve, reject) => {
let error = null;
writer.on("error", err => {
error = err;
writer.close();
reject(err);
});
// or writer.on('finish')?
writer.on("close", () => {
if (!error) {
resolve(true);
}
//no need to call the reject here, as it will have been called in the
//'error' stream;
});

});

}).then(() => {
const downloadTime = (Date.now() - start_time) / 1000; // Convert milliseconds to seconds
console.log(`Download time ${downloadTime.toFixed(2)}s`);
callFFMPEG();
}).catch(function(error) {
console.error("Error downloading file:", error.message);
});
} else {
callFFMPEG();
}
};

// unzip file (not used in this file but it exists in the original blender addon)
const unzip = (zipPath, extractDirPath) => {
const zip = new AdmZip(zipPath);

try {
zip.extractAllTo(extractDirPath, /*overwrite*/ true);
console.log(`Successfully extracted: ${zipPath} to ${extractDirPath}`);
} catch (error) {
console.error("Error extracting zip file:", error.message);
}
};

// check if ffmpeg exists
const ffmpegExist = async () => {
// decide releaseUrl based on operating system
if (process.platform.startsWith("win")) {
releaseUrl = "https://github.com/Pullusb/static_bin/raw/main/ffmpeg/windows/ffmpeg.exe";
} else if (process.platform.startsWith("linux") || process.platform === "freebsd") {
// console.log("linux");
releaseUrl = "https://github.com/Pullusb/static_bin/raw/main/ffmpeg/linux/ffmpeg";
} else { // Mac
releaseUrl = "https://github.com/Pullusb/static_bin/raw/main/ffmpeg/mac/ffmpeg";
}

// Check if ffmpeg is already in the current path
ffbin = path.join(String(__dirname), String(path.basename(releaseUrl)));
// console.log(ffbin);
// console.log(fs.existsSync(ffbin));
return fs.existsSync(ffbin);
};

const cutImages = async () => {
await sharp(String(ifp_temp))
.extract({ left: 0, top: 0, width: img_res_width, height: img_res_height / 4 })
.toFile(String(lcp))
.then(() => {
// console.log("here2.5")
})
.catch((err) => {
// console.log("here2.5");
if (err) {
console.log("Failed extracting upper half of the waveform...");
console.log(err);
}
});
// console.log("here3");
await sharp(String(ifp_temp))
.extract({ left: 0, top: 3 * img_res_height / 4, width: img_res_width, height: img_res_height / 4 })
.toFile(String(rcp))
.then(() => {
// console.log("here3.5")
})
.catch((err) => {
// console.log("here3.5");
if (err) {
console.log("Failed extracting lower half of the waveform...");
console.log(err);
}
});
// console.log("here4");
};

const AsyncJoinImages = async () => {
console.log("joining waveforms...");
await jm.joinImages([String(lcp), String(rcp)], { direction: "vertical" })
.then((img) => {
// Save image as file
img.toFile(String(ifp))
.then(() => { console.log("image saved"); })
.catch((err) => {
console.log("failed to save image as file");
console.log(err);
});

}).catch((err) => {
console.log("Failed joining waveforms...");
console.log(err);
});
};

const AsyncImageManipulation = async () => {
await cutImages();
await AsyncJoinImages();
};

const errReport = (err) => {
if (err) throw err;
};

const rmRedundantWaveform = () => {
let rmCmd = ["rm", String(lcp), String(rcp), String(ifp_temp)];
rmCmdString = rmCmd.join(" ");
// console.log(`rmCmdString = ${rmCmdString}`);
console.log("removing redundant waveforms...");
try {
fs.unlink(String(lcp), errReport);
fs.unlink(String(rcp), errReport);
fs.unlink(String(ifp_temp), errReport);
} catch (error) {
console.error("Error deleting existing file:", error.message);
}
};

const mainGenerate = async (releaseUrl, ffbin) => {
let ffmpegExistance = await ffmpegExist(); // ffbin, releaseUrl is set in ffmpegExist
await readLength();
await dl_url(releaseUrl, String(ffbin), ffmpegExistance);
await AsyncImageManipulation();
rmRedundantWaveform();
};


mainGenerate(releaseUrl, ffbin);

// download if ffmpeg not in path

// delete ffmpeg if exists
// if (ffmpegExistance) {
// try {
// fs.unlink(ffbin, (err) => {
// if (err) throw err;
// console.log(`successfully deleted ${ffbin}`);
// });
const mainGenerate = async () => {
await readInput();
generateWav();
}

// } catch (error) {
// console.error('Error deleting existing file:', error.message);
// }
// }
mainGenerate();
Loading