vite-combine-audio
Version:
Combines audio files into a single file to be used as an audio sprite
228 lines (222 loc) • 10.8 kB
JavaScript
;
var fse = require('fs-extra');
var path = require('path');
var child_process = require('child_process');
var ffmpegPath = require('ffmpeg-static');
var node_fs = require('node:fs');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var fse__namespace = /*#__PURE__*/_interopNamespaceDefault(fse);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
//@ts-ignore
const context = this;
const later = () => {
//@ts-ignore
timeout = undefined;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function combineAudio(options = {}) {
let config;
// Store paths of all .mp3 files
const audioFiles = [];
// Default filename for the merged audio file
let filename = options.filename || 'merged-audio';
// Default temporary and output directories
let tempDir = options.tempDir || 'temp/audio';
let outputDir = options.outputDir || 'audio';
let tempBuildDir = options.tempDir || 'temp/audio';
let outputBuildDir = options.outputDir || 'dist/audio';
// Cumulative current time for audio file starts
let currentTime = 0;
const fileRegex = options.fileRegex || /\.mp3$/;
const outputTypes = options.outputTypes || ['.mp3', '.webm']; // Default output types
let lastAudioLength = 0; // Store the last length of audio files
let runBuild = debounce(() => {
if (lastAudioLength !== audioFiles.length && config.command === 'serve') {
lastAudioLength = audioFiles.length; // Update the last length of audio files
// Run the build process after a delay
plugin.buildEnd();
}
}, 500);
let plugin = {
name: 'audio-build',
configResolved(_config) {
config = _config;
// console.log('config:', config); // Log the resolved config for debugging
tempDir = (config.base + tempDir).replace(/\\/g, '/'); // Normalize path for Windows compatibility
outputDir = (config.base + outputDir).replace(/\\/g, '/'); // Normalize path for Windows compatibility
tempBuildDir = path__namespace.resolve(config.root, tempBuildDir); // Resolve the path for development server
outputBuildDir = path__namespace.resolve(config.root, outputBuildDir); // Resolve the path for development server
},
async transform(src, id) {
if (fileRegex.test(id)) {
// Collect all valid files
if (!audioFiles.includes(id)) {
audioFiles.push(id);
}
let file = id;
let dataItem = {}; // Initialize dataItem with an empty object
let outputs = [];
if (config.command === 'build') {
if (outputTypes.includes('.webm')) {
outputs.push(path__namespace.join(outputDir, filename + '.webm'));
}
if (outputTypes.includes('.mp3')) {
outputs.push(path__namespace.join(outputDir, filename + '.mp3'));
}
}
else {
if (outputTypes.includes('.webm')) {
outputs.push(path__namespace.join(tempDir, filename + '.webm'));
}
if (outputTypes.includes('.mp3')) {
outputs.push(path__namespace.join(tempDir, filename + '.mp3'));
}
}
// Generate metadata for each file
try {
const sanitizedFilePath = file.replace(/\\/g, '/');
// Log the command for debugging
// console.log(`Running command: "${ffmpegPath}" -i "${sanitizedFilePath}"`);
// Get the duration of the current file using ffmpeg-static
const durationOutput = child_process.execSync(`"${ffmpegPath}" -i "${sanitizedFilePath}" 2>&1`, { encoding: 'utf-8' });
const durationMatch = durationOutput.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
if (durationMatch) {
const [, hours, minutes, seconds] = durationMatch;
const duration = parseInt(hours) * 3600 * 1000 + // Hours to milliseconds
parseInt(minutes) * 60 * 1000 + // Minutes to milliseconds
Math.round(parseFloat(seconds) * 1000); // Seconds to milliseconds
dataItem = {
output: outputs,
filename: path__namespace.basename(file),
startTime: currentTime,
duration
};
currentTime += duration; // Update the start time for the next file
}
}
catch (error) {
// Extract metadata from the error output
const durationMatch = error.stdout.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
if (durationMatch) {
const [, hours, minutes, seconds] = durationMatch;
const duration = parseInt(hours) * 3600 * 1000 + // Hours to milliseconds
parseInt(minutes) * 60 * 1000 + // Minutes to milliseconds
Math.round(parseFloat(seconds) * 1000); // Seconds to milliseconds
dataItem = {
output: outputs,
filename: path__namespace.basename(file),
startTime: currentTime,
duration
};
currentTime += duration; // Update the start time for the next file
}
else {
console.error(`Error getting duration for ${file}:`, error);
}
}
runBuild(); // Call the debounced function
return {
code: `export default ${JSON.stringify(dataItem)};`,
map: null,
};
}
},
async buildEnd() {
console.log('Audio files:', audioFiles); // Log the collected audio files
if (audioFiles.length === 0)
return;
// Ensure the output directory exists
fse__namespace.ensureDirSync(outputBuildDir);
fse__namespace.ensureDirSync(tempBuildDir);
// Create a temporary file listing all .mp3 files for ffmpeg
const concatFilePath = path__namespace.join(tempBuildDir, 'concat-list.txt');
const concatFileContent = audioFiles
.map((file) => `file '${file.replace(/\\/g, '/')}'`) // Escape Windows paths
.join('\n');
node_fs.writeFileSync(concatFilePath, concatFileContent);
// Merge the .mp3 files using ffmpeg-static
try {
let mp3Path = path__namespace.join(tempBuildDir, filename + '.mp3');
child_process.execSync(`"${ffmpegPath}" -loglevel error -y -f concat -safe 0 -i "${concatFilePath}" -c copy "${mp3Path}"`, { stdio: 'inherit' });
}
catch (error) {
console.error('Error merging audio files:', error);
throw error;
}
// Generate the .webm file
if (outputTypes.includes('.webm')) {
const webmPath = path__namespace.join(tempBuildDir, filename + '.webm');
let mp3Path = path__namespace.join(tempBuildDir, filename + '.mp3');
try {
child_process.execSync(`"${ffmpegPath}" -loglevel error -y -i "${mp3Path}" -c:a libvorbis "${webmPath}"`, { stdio: 'inherit' });
}
catch (error) {
console.error('Error converting merged audio to .webm:', error);
throw error;
}
}
// Clean up the temporary concat file
fse__namespace.remove(concatFilePath);
},
writeBundle() {
// Ensure the merged file is preserved in the dist folder
if (outputTypes.includes('.mp3')) {
let mp3Path = path__namespace.join(tempBuildDir, filename + '.mp3');
let mp3OuputPath = path__namespace.join(outputBuildDir, filename + '.mp3');
fse__namespace.move(mp3Path, mp3OuputPath);
}
if (outputTypes.includes('.webm')) {
const webmPath = path__namespace.join(tempBuildDir, filename + '.webm');
const outputwebmPath = path__namespace.join(outputBuildDir, filename + '.webm');
fse__namespace.move(webmPath, outputwebmPath);
}
},
configureServer(server) {
// Serve the merged audio file during development
server.middlewares.use((req, res, next) => {
let tempMp3Path = path__namespace.join(tempDir, filename + '.mp3').replace(/\\/g, '/');
let tempWebmPath = path__namespace.join(tempDir, filename + '.webm').replace(/\\/g, '/');
if (req.url === tempMp3Path || req.url === tempWebmPath) {
// if (audioFiles.indexOf(req.url) !== -1) {
const filePath = path__namespace.join(config.root, req.url).replace(/\\/g, '/');
if (node_fs.existsSync(filePath)) {
res.setHeader('Content-Type', req.url.endsWith('.mp3') ? 'audio/mpeg' : 'audio/webm');
node_fs.createReadStream(filePath).pipe(res);
return;
}
else {
res.statusCode = 404;
res.end('File not found');
return;
}
}
next();
});
},
};
return plugin;
}
module.exports = combineAudio;
//# sourceMappingURL=index.js.map