audio.ts•4.39 kB
import ffmpeg from 'fluent-ffmpeg';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { log } from './logger.js';
// Attempt to find ffmpeg path automatically or allow setting via environment variable
const FFMPEG_PATH = process.env.FFMPEG_PATH || findFfmpegPath();
if (FFMPEG_PATH) {
ffmpeg.setFfmpegPath(FFMPEG_PATH);
log.info(`Using ffmpeg found at: ${FFMPEG_PATH}`);
} else {
log.warn('ffmpeg path not found or set. Audio conversion will likely fail. Set FFMPEG_PATH environment variable if needed.');
}
function findFfmpegPath(): string | null {
// Basic check in common locations or PATH (this is simplified)
// A more robust solution might use 'which' or 'where' commands
// or check standard installation directories per OS.
// For now, we rely on it being in the system PATH or set via env var.
// Returning null signifies it wasn't explicitly found here.
return null;
}
export class AudioUtils {
/**
* Convert an audio file to Opus format in an Ogg container using ffmpeg.
* Throws an error if ffmpeg is not found or conversion fails.
*
* @param inputPath Path to the input audio file.
* @param outputPath Optional path for the output file. Defaults to input path with .ogg extension.
* @param bitrate Target bitrate (e.g., "32k").
* @param sampleRate Target sample rate (e.g., 24000).
* @returns Path to the converted file.
*/
static convertToOpusOgg(
inputPath: string,
outputPath?: string,
bitrate = '32k',
sampleRate = 24000,
): Promise<string> {
return new Promise((resolve, reject) => {
if (!FFMPEG_PATH && !process.env.FFMPEG_PATH) {
// Check again in case it became available after startup
if (!findFfmpegPath()) {
return reject(new Error('ffmpeg path not configured. Set FFMPEG_PATH or ensure ffmpeg is in system PATH.'));
}
}
if (!fs.existsSync(inputPath)) {
return reject(new Error(`Input file not found: ${inputPath}`));
}
const finalOutputPath = outputPath || `${path.parse(inputPath).name}.ogg`;
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
log.debug(`Starting ffmpeg conversion: ${inputPath} -> ${finalOutputPath}`);
ffmpeg(inputPath)
.audioCodec('libopus')
.audioBitrate(bitrate)
.audioFrequency(sampleRate)
.outputOptions([
'-application voip', // Optimize for voice
'-vbr on', // Variable bitrate
'-compression_level 10', // Max compression
'-frame_duration 60', // Good frame duration for voice
])
.output(finalOutputPath)
.on('end', () => {
log.debug(`ffmpeg conversion finished: ${finalOutputPath}`);
resolve(finalOutputPath);
})
.on('error', (err: Error) => { // Add type Error
log.error(`ffmpeg conversion error for ${inputPath}:`, err);
reject(new Error(`ffmpeg conversion failed: ${err.message}`));
})
.run();
});
}
/**
* Converts an audio file to Opus/Ogg and saves it to a temporary file.
* Useful when you need a temporary .ogg file for sending.
*
* @param inputPath Path to the input audio file.
* @param bitrate Target bitrate.
* @param sampleRate Target sample rate.
* @returns Path to the temporary converted file.
*/
static async convertToOpusOggTemp(
inputPath: string,
bitrate = '32k',
sampleRate = 24000,
): Promise<string> {
const tempFileName = `whatsapp_audio_converted_${Date.now()}.ogg`;
const tempOutputPath = path.join(os.tmpdir(), tempFileName);
log.debug(`Converting ${inputPath} to temporary file: ${tempOutputPath}`);
try {
const convertedPath = await this.convertToOpusOgg(inputPath, tempOutputPath, bitrate, sampleRate);
return convertedPath;
} catch (error) {
// Clean up temp file if conversion failed partway
if (fs.existsSync(tempOutputPath)) {
try {
fs.unlinkSync(tempOutputPath);
} catch (cleanupError) {
log.warn(`Failed to clean up temporary file ${tempOutputPath}:`, cleanupError);
}
}
throw error; // Re-throw the original conversion error
}
}
}