Skip to main content
Glama
audio-processing.test.ts20.4 kB
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import * as fs from 'fs'; import * as path from 'path'; import AudioProcessor from '../../src/services/audio-processor.js'; import { AudioOperations, ProcessingInput, ProcessingOutput } from '../../src/types/index.js'; // Mock dependencies jest.mock('fluent-ffmpeg'); jest.mock('glob'); jest.mock('fs', () => ({ promises: { stat: jest.fn(), access: jest.fn(), mkdir: jest.fn(), unlink: jest.fn() }, constants: { R_OK: 4, W_OK: 2, F_OK: 0 } })); const mockGlob = jest.mocked(await import('glob')).glob; const mockFs = jest.mocked(fs.promises); const mockFfmpeg = jest.mocked(await import('fluent-ffmpeg')).default; describe('Audio Processing Integration', () => { let audioProcessor: AudioProcessor; let mockCommand: any; beforeEach(() => { audioProcessor = new AudioProcessor(); jest.clearAllMocks(); // Setup mock FFmpeg command mockCommand = { audioFilters: jest.fn().mockReturnThis(), audioFrequency: jest.fn().mockReturnThis(), audioChannels: jest.fn().mockReturnThis(), audioCodec: jest.fn().mockReturnThis(), audioBitrate: jest.fn().mockReturnThis(), seekInput: jest.fn().mockReturnThis(), duration: jest.fn().mockReturnThis(), output: jest.fn().mockReturnThis(), on: jest.fn().mockReturnThis(), run: jest.fn() }; (mockFfmpeg as any).mockReturnValue(mockCommand); }); afterEach(() => { jest.clearAllMocks(); }); describe('Single File Processing', () => { beforeEach(() => { // Mock successful file validation mockFs.stat.mockResolvedValue({ isFile: () => true } as any); mockFs.access.mockResolvedValue(undefined); mockFs.mkdir.mockResolvedValue(undefined); // Mock successful FFmpeg execution mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end') { setTimeout(() => callback(), 0); } return mockCommand; }); }); it('should process single audio file with volume operations', async () => { const operations: AudioOperations = { volume: { adjust: -3, normalize: true, targetLUFS: -20 } }; const result = await audioProcessor.processAudioFile( '/input/audio.mp3', '/output/processed.mp3', operations, false ); expect(result.success).toBe(true); expect(result.inputFile).toBe('/input/audio.mp3'); expect(result.outputFile).toBe('/output/processed.mp3'); expect(result.operations).toEqual(operations); expect(result.processingTime).toBeGreaterThan(0); // Verify FFmpeg commands were called expect(mockCommand.audioFilters).toHaveBeenCalledWith('volume=-3dB'); expect(mockCommand.audioFilters).toHaveBeenCalledWith('loudnorm=I=-20:LRA=7:tp=-2'); expect(mockCommand.output).toHaveBeenCalledWith('/output/processed.mp3'); expect(mockCommand.run).toHaveBeenCalled(); }); it('should process single audio file with format operations', async () => { const operations: AudioOperations = { format: { sampleRate: 44100, bitrate: 192, channels: 2, codec: 'mp3' } }; const result = await audioProcessor.processAudioFile( '/input/audio.wav', '/output/converted.mp3', operations, true ); expect(result.success).toBe(true); // Verify format operations were applied expect(mockCommand.audioFrequency).toHaveBeenCalledWith(44100); expect(mockCommand.audioBitrate).toHaveBeenCalledWith('192k'); expect(mockCommand.audioChannels).toHaveBeenCalledWith(2); expect(mockCommand.audioCodec).toHaveBeenCalledWith('libmp3lame'); }); it('should process single audio file with effects operations', async () => { const operations: AudioOperations = { effects: { fadeIn: 1.0, fadeOut: 2.0, trim: { start: 5.0, end: 15.0 }, loop: { enabled: true, count: 3 } } }; const result = await audioProcessor.processAudioFile( '/input/audio.wav', '/output/effects.wav', operations, false ); expect(result.success).toBe(true); // Verify effects were applied expect(mockCommand.seekInput).toHaveBeenCalledWith(5.0); expect(mockCommand.duration).toHaveBeenCalledWith(10.0); expect(mockCommand.audioFilters).toHaveBeenCalledWith([ 'afade=t=in:ss=0:d=1', 'afade=t=out:st=2:d=1', 'aloop=loop=2:size=samples' ]); }); it('should handle complex operations combination', async () => { const operations: AudioOperations = { volume: { adjust: -6, normalize: true, targetLUFS: -18 }, format: { sampleRate: 48000, channels: 1, codec: 'aac', bitrate: 128 }, effects: { fadeIn: 0.5, fadeOut: 1.0 } }; const result = await audioProcessor.processAudioFile( '/input/stereo.wav', '/output/mono.m4a', operations, true ); expect(result.success).toBe(true); // Verify all operations were applied expect(mockCommand.audioFilters).toHaveBeenCalledWith('volume=-6dB'); expect(mockCommand.audioFilters).toHaveBeenCalledWith('loudnorm=I=-18:LRA=7:tp=-2'); expect(mockCommand.audioFrequency).toHaveBeenCalledWith(48000); expect(mockCommand.audioChannels).toHaveBeenCalledWith(1); expect(mockCommand.audioCodec).toHaveBeenCalledWith('aac'); expect(mockCommand.audioBitrate).toHaveBeenCalledWith('128k'); expect(mockCommand.audioFilters).toHaveBeenCalledWith([ 'afade=t=in:ss=0:d=0.5', 'afade=t=out:st=1:d=1' ]); }); it('should handle processing errors gracefully', async () => { // Mock FFmpeg error mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'error') { setTimeout(() => callback(new Error('FFmpeg encoding failed')), 0); } return mockCommand; }); const operations: AudioOperations = { volume: { adjust: -3 } }; const result = await audioProcessor.processAudioFile( '/input/audio.mp3', '/output/failed.mp3', operations, false ); expect(result.success).toBe(false); expect(result.error).toContain('FFmpeg processing failed'); expect(result.inputFile).toBe('/input/audio.mp3'); expect(result.outputFile).toBe('/output/failed.mp3'); }); it('should handle file validation errors', async () => { // Mock file not found mockFs.stat.mockRejectedValue(new Error('ENOENT: no such file')); const operations: AudioOperations = { volume: { adjust: -3 } }; const result = await audioProcessor.processAudioFile( '/input/nonexistent.mp3', '/output/failed.mp3', operations, false ); expect(result.success).toBe(false); expect(result.error).toContain('Cannot access input file'); }); it('should handle output file conflicts', async () => { // Mock existing output file mockFs.stat.mockResolvedValue({ isFile: () => true } as any); // First access call for input validation // Second access call for output file check mockFs.access .mockResolvedValueOnce(undefined) // Input file exists .mockResolvedValueOnce(undefined); // Output file exists const operations: AudioOperations = { volume: { adjust: -3 } }; const result = await audioProcessor.processAudioFile( '/input/audio.mp3', '/output/existing.mp3', operations, false // Don't overwrite ); expect(result.success).toBe(false); expect(result.error).toContain('Output file already exists'); }); }); describe('Batch Processing', () => { beforeEach(() => { // Mock successful file validation for all files mockFs.stat.mockResolvedValue({ isFile: () => true } as any); mockFs.access.mockResolvedValue(undefined); mockFs.mkdir.mockResolvedValue(undefined); // Mock successful FFmpeg execution mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end') { setTimeout(() => callback(), 0); } return mockCommand; }); }); it('should process multiple files in a directory', async () => { // Mock glob to return test files (mockGlob as jest.Mock).mockResolvedValue([ '/input/audio1.mp3', '/input/audio2.wav', '/input/audio3.ogg' ]); const input: ProcessingInput = { directory: '/input', pattern: '*.{mp3,wav,ogg}' }; const output: ProcessingOutput = { directory: '/output', suffix: '_processed' }; const operations: AudioOperations = { volume: { normalize: true }, format: { sampleRate: 44100 } }; const result = await audioProcessor.batchProcessAudio( input, output, operations, false ); expect(result.totalFiles).toBe(3); expect(result.successfulFiles).toBe(3); expect(result.failedFiles).toBe(0); expect(result.results).toHaveLength(3); expect(result.totalProcessingTime).toBeGreaterThan(0); // Verify all files were processed expect(result.results[0].inputFile).toBe('/input/audio1.mp3'); expect(result.results[1].inputFile).toBe('/input/audio2.wav'); expect(result.results[2].inputFile).toBe('/input/audio3.ogg'); // Verify FFmpeg was called for each file expect(mockCommand.run).toHaveBeenCalledTimes(3); }); it('should handle mixed success and failure in batch processing', async () => { (mockGlob as jest.Mock).mockResolvedValue([ '/input/audio1.mp3', '/input/audio2.wav' ]); // Mock one success and one failure let callCount = 0; mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end' && callCount === 0) { callCount++; setTimeout(() => callback(), 0); } else if (event === 'error' && callCount === 1) { setTimeout(() => callback(new Error('Processing failed')), 0); } return mockCommand; }); const input: ProcessingInput = { directory: '/input' }; const output: ProcessingOutput = { directory: '/output' }; const operations: AudioOperations = { format: { codec: 'mp3' } }; const result = await audioProcessor.batchProcessAudio( input, output, operations, false ); expect(result.totalFiles).toBe(2); expect(result.successfulFiles).toBe(1); expect(result.failedFiles).toBe(1); }); it('should handle no files found scenario', async () => { (mockGlob as jest.Mock).mockResolvedValue([]); const input: ProcessingInput = { directory: '/input', pattern: '*.unknown' }; const output: ProcessingOutput = { directory: '/output' }; const operations: AudioOperations = {}; await expect(audioProcessor.batchProcessAudio(input, output, operations, false)) .rejects.toThrow('No audio files found matching the criteria'); }); it('should generate correct output paths for batch processing', async () => { (mockGlob as jest.Mock).mockResolvedValue([ '/input/subfolder/music.mp3', '/input/voice.wav' ]); const input: ProcessingInput = { directory: '/input' }; const output: ProcessingOutput = { directory: '/output', suffix: '_optimized', format: 'ogg' }; const operations: AudioOperations = { format: { codec: 'vorbis' } }; await audioProcessor.batchProcessAudio(input, output, operations, false); // Verify correct output paths were generated const calls = mockCommand.output.mock.calls; expect(calls[0][0]).toMatch(/music_optimized\.ogg$/); expect(calls[1][0]).toMatch(/voice_optimized\.ogg$/); }); }); describe('Queue Management', () => { it('should provide queue status information', () => { const status = audioProcessor.getQueueStatus(); expect(status).toHaveProperty('size'); expect(status).toHaveProperty('pending'); expect(status).toHaveProperty('isPaused'); expect(typeof status.size).toBe('number'); expect(typeof status.pending).toBe('number'); expect(typeof status.isPaused).toBe('boolean'); }); it('should allow pausing and resuming the queue', () => { // Initial state should not be paused expect(audioProcessor.getQueueStatus().isPaused).toBe(false); // Pause the queue audioProcessor.pause(); expect(audioProcessor.getQueueStatus().isPaused).toBe(true); // Resume the queue audioProcessor.resume(); expect(audioProcessor.getQueueStatus().isPaused).toBe(false); }); it('should allow clearing the queue', () => { audioProcessor.clear(); const status = audioProcessor.getQueueStatus(); expect(status.size).toBe(0); expect(status.pending).toBe(0); }); }); describe('Codec Mapping', () => { beforeEach(() => { mockFs.stat.mockResolvedValue({ isFile: () => true } as any); mockFs.access.mockResolvedValue(undefined); mockFs.mkdir.mockResolvedValue(undefined); mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end') { setTimeout(() => callback(), 0); } return mockCommand; }); }); it('should map codec names correctly', async () => { const codecTests = [ { input: 'pcm', expected: 'pcm_s16le' }, { input: 'mp3', expected: 'libmp3lame' }, { input: 'aac', expected: 'aac' }, { input: 'vorbis', expected: 'libvorbis' }, { input: 'flac', expected: 'flac' } ]; for (const test of codecTests) { jest.clearAllMocks(); const operations: AudioOperations = { format: { codec: test.input as any } }; await audioProcessor.processAudioFile( '/input/audio.wav', '/output/converted.wav', operations, false ); expect(mockCommand.audioCodec).toHaveBeenCalledWith(test.expected); } }); }); describe('Error Handling and Edge Cases', () => { it('should handle empty operations gracefully', async () => { mockFs.stat.mockResolvedValue({ isFile: () => true } as any); mockFs.access.mockResolvedValue(undefined); mockFs.mkdir.mockResolvedValue(undefined); mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end') { setTimeout(() => callback(), 0); } return mockCommand; }); const result = await audioProcessor.processAudioFile( '/input/audio.mp3', '/output/copied.mp3', {}, // Empty operations false ); expect(result.success).toBe(true); // Should not have called any modification methods expect(mockCommand.audioFilters).not.toHaveBeenCalled(); expect(mockCommand.audioFrequency).not.toHaveBeenCalled(); expect(mockCommand.audioChannels).not.toHaveBeenCalled(); }); it('should handle invalid input types gracefully', async () => { const input: ProcessingInput = {}; const output: ProcessingOutput = { directory: '/output' }; await expect(audioProcessor.batchProcessAudio(input, output, {}, false)) .rejects.toThrow('No valid input specification provided'); }); it('should handle concurrent processing with proper queue management', async () => { const processor = new AudioProcessor(1); // Concurrency of 1 mockFs.stat.mockResolvedValue({ isFile: () => true } as any); mockFs.access.mockResolvedValue(undefined); mockFs.mkdir.mockResolvedValue(undefined); let processedCount = 0; mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end') { setTimeout(() => { processedCount++; callback(); }, 10); } return mockCommand; }); // Start multiple processing tasks const promises = [ processor.processAudioFile('/input/1.mp3', '/output/1.mp3', {}, false), processor.processAudioFile('/input/2.mp3', '/output/2.mp3', {}, false), processor.processAudioFile('/input/3.mp3', '/output/3.mp3', {}, false) ]; const results = await Promise.all(promises); expect(results).toHaveLength(3); expect(results.every(r => r.success)).toBe(true); expect(processedCount).toBe(3); }); }); describe('Real-world Scenarios', () => { beforeEach(() => { mockFs.stat.mockResolvedValue({ isFile: () => true } as any); mockFs.access.mockResolvedValue(undefined); mockFs.mkdir.mockResolvedValue(undefined); mockCommand.on.mockImplementation((event: string, callback: Function) => { if (event === 'end') { setTimeout(() => callback(), 0); } return mockCommand; }); }); it('should handle ElevenLabs voice optimization workflow', async () => { const operations: AudioOperations = { format: { sampleRate: 22050, bitrate: 160, channels: 1, codec: 'mp3' }, volume: { normalize: true, targetLUFS: -20 }, effects: { fadeIn: 0.05, fadeOut: 0.1 } }; const result = await audioProcessor.processAudioFile( '/input/elevenlabs_voice.wav', '/output/game_ready_voice.mp3', operations, true ); expect(result.success).toBe(true); // Verify ElevenLabs-specific optimizations expect(mockCommand.audioFrequency).toHaveBeenCalledWith(22050); expect(mockCommand.audioChannels).toHaveBeenCalledWith(1); expect(mockCommand.audioCodec).toHaveBeenCalledWith('libmp3lame'); expect(mockCommand.audioBitrate).toHaveBeenCalledWith('160k'); expect(mockCommand.audioFilters).toHaveBeenCalledWith('loudnorm=I=-20:LRA=7:tp=-2'); expect(mockCommand.audioFilters).toHaveBeenCalledWith([ 'afade=t=in:ss=0:d=0.05', 'afade=t=out:st=0.1:d=1' ]); }); it('should handle game audio asset preparation', async () => { (mockGlob as jest.Mock).mockResolvedValue([ '/assets/music/track1.wav', '/assets/sfx/explosion.wav', '/assets/voice/dialogue1.wav' ]); const input: ProcessingInput = { directory: '/assets', pattern: '**/*.wav' }; const output: ProcessingOutput = { directory: '/game/audio', suffix: '_game', format: 'ogg' }; const operations: AudioOperations = { format: { codec: 'vorbis', bitrate: 192 }, volume: { normalize: true, targetLUFS: -18 } }; const result = await audioProcessor.batchProcessAudio( input, output, operations, true ); expect(result.totalFiles).toBe(3); expect(result.successfulFiles).toBe(3); // Verify game-optimized settings were applied expect(mockCommand.audioCodec).toHaveBeenCalledWith('libvorbis'); expect(mockCommand.audioBitrate).toHaveBeenCalledWith('192k'); expect(mockCommand.audioFilters).toHaveBeenCalledWith('loudnorm=I=-18:LRA=7:tp=-2'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DeveloperZo/mcp-audio-tweaker'

If you have feedback or need assistance with the MCP directory API, please join our Discord server