UNPKG

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
'use strict'; 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