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

Ft/audio export multi format #271

Draft
wants to merge 11 commits into
base: development
Choose a base branch
from
Draft
12 changes: 4 additions & 8 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@
"[javascript]": {
"editor.formatOnSave": false
},
"editor.rulers": [
100
],
"editor.rulers": [100],
"editor.fontLigatures": true,
"prettier.tabWidth": 4,
"eslint.alwaysShowStatus": true,
"prettier.disableLanguages": [
"js"
],
"prettier.disableLanguages": ["js"],
"prettier.useTabs": true,
"editor.formatOnSave": true,
"editor.multiCursorModifier": "alt",
Expand All @@ -27,8 +23,8 @@
"eslint.codeAction.disableRuleComment": {},
"eslint.codeAction.showDocumentation": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"eslint.workingDirectories": [],
"editor.tabSize": 2
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@
"@electron/remote": "^2.0.8",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@ffmpeg/ffmpeg": "^0.12.7",
"@ffmpeg/util": "^0.12.1",
"@headlessui/react": "^1.7.8",
"@heroicons/react": "^2.0.14",
"@material-ui/core": "^4.12.4",
Expand Down
254 changes: 144 additions & 110 deletions renderer/src/components/AudioRecorder/core/audioUtils.js
Original file line number Diff line number Diff line change
@@ -1,134 +1,168 @@
/* eslint-disable no-await-in-loop */
import { fetchFile } from '@ffmpeg/util';
import * as localforage from 'localforage';
import * as logger from '../../../logger';

const toWav = require('audiobuffer-to-wav');
import packageInfo from '../../../../../package.json';

function sec_to_min_sec_milli_convertor(time) {
logger.debug('audioUtils.js', 'In time conversion function');
let milliseconds = time.toString().split('.')[1];
if (milliseconds === undefined) {
milliseconds = '0';
try {
logger.debug('audioUtils.js', 'In time conversion function');
let milliseconds = time.toString().split('.')[1];
if (milliseconds === undefined) {
milliseconds = '0';
}
const minutes = Math.floor(time / 60);
const seconds = (time - minutes * 60).toString().split('.')[0].padStart(2, 0);
const formatedStringTime = `${minutes.toString().padStart(2, 0)}:${seconds}:${milliseconds.padStart(2, 0)}`;
logger.debug('audioUtils.js', 'In time conversion function done');
return [minutes, seconds, milliseconds, formatedStringTime];
} catch (err) {
throw new Error(`audio generation failed : ${err}`);
}
const minutes = Math.floor(time / 60);
const seconds = (time - minutes * 60).toString().split('.')[0].padStart(2, 0);
const formatedStringTime = `${minutes.toString().padStart(2, 0)}:${seconds}:${milliseconds.padStart(2, 0)}`;
return [minutes, seconds, milliseconds, formatedStringTime];
}

async function generateTimeStampData(buffers, book, chapter) {
logger.debug('audioUtils.js', 'In TimeStamp Generation');
return new Promise((resolve) => {
let fileString = 'verse_number\tstart_timestamp\tduration\n';
const seperator = '\t';
const fileType = 'tsv';
const file = `${book}_${chapter.toString().padStart(3, 0)}.${fileType}`;
let start = 0;
buffers.forEach((buffer, index) => {
const currentVerse = `Verse_${(index + 1).toString().padStart(2, 0)}`;
const startTimeString = sec_to_min_sec_milli_convertor(start)[3];
const durationString = sec_to_min_sec_milli_convertor(buffer.duration)[3];
fileString += `${currentVerse + seperator + startTimeString + seperator + durationString}\n`;
start += buffer.duration;
try {
logger.debug('audioUtils.js', 'In TimeStamp Generation');
return new Promise((resolve) => {
let fileString = 'verse_number\tstart_timestamp\tduration\n';
const seperator = '\t';
const fileType = 'tsv';
const file = `${book}_${chapter.toString().padStart(3, 0)}.${fileType}`;
let start = 0; // because of 1 sec silence in merged verses
buffers.forEach((buffer, index) => {
const currentVerse = `Verse_${(index + 1).toString().padStart(2, 0)}`;
const startTimeString = sec_to_min_sec_milli_convertor(start)[3];
let durationString;
const silenceDuration = 2;
let offset = 0;
if (index === 0 || (index === buffers.length - 1)) {
// adding this because of 2 sec silence in the start || 2 sec silence in the end add it to last verse
durationString = sec_to_min_sec_milli_convertor(buffer.duration + silenceDuration)[3];
offset = silenceDuration;
} else {
offset = 0;
durationString = sec_to_min_sec_milli_convertor(buffer.duration)[3];
}
fileString += `${currentVerse + seperator + startTimeString + seperator + durationString}\n`;
start += buffer.duration + offset;
});
resolve([file, fileString]);
}).catch((err) => {
throw new Error(`audio generation failed : ${err}`);
});
resolve([file, fileString]);
});
}

async function fetchAndCombineAudio(audioArr, path) {
logger.debug('audioUtils.js', 'In Fetch and merge audio function');
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const context = new window.AudioContext();

// store the decoded buff
const sources = [];

// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const url of audioArr) {
try {
const response = await fetch(path.join('file://', url));
const buffer = await response.arrayBuffer();
const decodedData = await context.decodeAudioData(buffer);
sources.push(decodedData);
} catch (err) {
logger.error('audioUtils.js', `Error reading audio - ${url} : ${err}`);
}
}

logger.debug('audioUtils.js', 'In fetchAndCombineAudio : Fetch audio success ');

const totalLength = sources.reduce((total, source) => total + source.length, 0);
const output = context.createBuffer(1, totalLength, context.sampleRate);

let offset = 0;

// eslint-disable-next-line no-restricted-syntax
for (const source of sources) {
output.copyToChannel(source.getChannelData(0), 0, offset);
offset += source.length;
}
const wavData = toWav(output);
logger.debug('audioUtils.js', 'In fetchAndCombineAudio : generate wav success');
const blob = new Blob([new DataView(wavData)], { type: 'audio/wav' });
logger.debug('audioUtils.js', 'In fetchAndCombineAudio : Generate Final merged Audio success ');
resolve({ blob, buffers: sources });
});
} catch (err) {
throw new Error('audio generation failed : err');
}
}

function sortingLogic(a, b) {
// expected format : '1_10_1_default.mp3',
return a.split('_')[1] - b.split('_')[1];
}

export async function mergeAudio(audioArr, dirPath, path, book, chapter) {
export async function mergeAudio(audioArr, dirPath, path, book, chapter, extension, ffmpeg, project) {
try {
logger.debug('audioUtils.js', 'In Merge Audio fucntion');
// const audio = new ConcatAudio(window);
return new Promise((resolve) => {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
audioArr.sort(sortingLogic);
for (let i = 0; i < audioArr.length; i++) {
audioArr[i] = path.join(dirPath, audioArr[i]);
}
logger.debug('audioUtils.js', 'start merging audios');

fetchAndCombineAudio(audioArr, path)
.then(async (mergedData) => {
logger.debug('audioUtils.js', `Audio merge success . Started Generate Timestamp : ${book} : ${chapter}`);
generateTimeStampData(mergedData.buffers, book, chapter)
.then((timeStampData) => {
logger.debug('audioUtils.js', `return Merged Audio for chapter : ${book} : ${chapter}`);
resolve([mergedData.blob, timeStampData]);
});
logger.debug('audioUtils.js', 'mergeAudio : start merging audios');

const inputCmdArr = [];
const commandArr = [];
const audioBuffersArr = [];
let filterStr = '';
let currentUser;
await localforage.getItem('userProfile').then((value) => {
currentUser = value?.username;
});
// only support specific data fields // title, artist, album, year, comment
const audioMetaDataArr = [
'-metadata', `title=${book}_${chapter}`,
'-metadata', `artist=${currentUser || packageInfo.name}`,
'-metadata', `album=${project.name}`,
'-metadata', `comment=${packageInfo.name}_${packageInfo.version}`,
'-metadata', `date=${new Date().getFullYear().toString()}`,
];

const audioContext = new (window.AudioContext || window.webkitAudioContext)();

for (let i = 0; i < audioArr.length; i++) {
const audioFile = await fetchFile(path.join('file://', audioArr[i]));
const audioFileCopy = new Uint8Array(audioFile);
await ffmpeg.writeFile(`input_${i}.wav`, audioFile);
inputCmdArr.push('-i', `input_${i}.wav`);
filterStr += `[${i}:0]`;

const audioBuffer = await audioContext.decodeAudioData(audioFileCopy.buffer);
audioBuffersArr.push(audioBuffer);
}
filterStr += `concat=n=${audioArr.length + 1 }:v=0:a=1[out]`;

commandArr.push(
...inputCmdArr,
'-filter_complex',
`aevalsrc=0:d=2[silence1];[silence1]${filterStr}`,
'-map',
'[out]',
'-ar',
'48000',
`output.${extension}`,
);

console.log({ filterStr, commandArr });

Check warning on line 118 in renderer/src/components/AudioRecorder/core/audioUtils.js

View workflow job for this annotation

GitHub Actions / Lint Run

Unexpected console statement

Check warning on line 118 in renderer/src/components/AudioRecorder/core/audioUtils.js

View workflow job for this annotation

GitHub Actions / Lint Run

Unexpected console statement

logger.debug('audioUtils.js', 'mergeAudio : audio internal write done and buffers created');

// exeute merge process
await ffmpeg.exec(commandArr);

// add end 2 sec to the create audio
await ffmpeg.exec([
'-i',
`output.${extension}`,
'-filter_complex',
'aevalsrc=0:d=2[silenceEnd];[0:0][silenceEnd]concat=n=2:v=0:a=1[outEnd]',
'-map',
'[outEnd]',
...audioMetaDataArr,
'-ar',
'48000',
`outputMerged.${extension}`,
]);

// write the audio back and convert

// generate unit8 buffer out
const fileData = await ffmpeg.readFile(`outputMerged.${extension}`);

logger.debug('audioUtils.js', 'mergeAudio : audio merged buffer created');

// Create a Blob from the result
const blob = new Blob([fileData.buffer], { type: extension === 'mp3' ? 'audio/mpeg' : 'audio/wav' });
logger.debug('audioUtils.js', 'mergeAudio : audio merged blob created');

if (blob) {
generateTimeStampData(audioBuffersArr, book, chapter)
.then((timeStampData) => {
logger.debug('audioUtils.js', `mergeAudio : return timestamp for Merged Audio for chapter : ${book} : ${chapter}`);
resolve([blob, timeStampData]);
}).catch((err) => {
throw new Error(`unable to generate audio : ${err}`);
});
} else {
throw new Error('unable to generate audio');
}
} catch (err) {
reject(new Error(`audio generation failed : ${err}`));
}
});
} catch (err) {
throw new Error(`audio generation failed : ${err}`);
}
}

// old snippet for reference

// export async function mergeAudio(audioArr, dirPath, path, book, chapter) {
// logger.debug('audioUtils.js', 'In Merge Audio fucntion');
// const audio = new ConcatAudio(window);
// return new Promise((resolve) => {
// let merged;
// let output;
// audioArr.sort();
// for (let i = 0; i < audioArr.length; i++) {
// audioArr[i] = path.join(dirPath, audioArr[i]);
// }
// logger.debug('audioUtils.js', 'start merging audios');
// audio.fetchAudio(...audioArr)
// .then(async (buffers) => {
// // generate timestamp data string
// await generateTimeStampData(buffers, book, chapter)
// .then((timeStampData) => {
// // merging all buffers
// merged = audio.concatAudio(buffers);
// return [merged, timeStampData];
// })
// .then(async ([merged, timeStampData]) => {
// output = audio.export(merged, 'audio/mp3');
// logger.debug('audioUtils.js', `return Merged Audio for chapter : ${book} : ${chapter}`);
// resolve([output.blob, timeStampData]);
// });
// });
// });
// }
4 changes: 2 additions & 2 deletions renderer/src/components/EditorPage/AudioEditor/AudioEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ const AudioEditor = ({ editor }) => {
} else {
// If found only one audio for the verse then making that audio as default one.
// replace url with `${chapter}_${verseNum[1]}_1_default.mp3`
bookContent[key].contents[v].take1 = `${chapter}_${verseNum[1]}_1_default.mp3`;
bookContent[key].contents[v].take1 = `${chapter}_${verseNum[1]}_1_default.wav`;
bookContent[key].contents[v].default = 'take1';
fs.renameSync(path.join(filePath, chapterNum, verse), path.join(filePath, chapterNum, `${chapter}_${verseNum[1]}_1_default.mp3`));
fs.renameSync(path.join(filePath, chapterNum, verse), path.join(filePath, chapterNum, `${chapter}_${verseNum[1]}_1_default.wav`));
}
}
},
Expand Down
Loading
Loading