From 8fc9081487d4bef20b41cb1262a4b1c91a38c723 Mon Sep 17 00:00:00 2001 From: Srivarshan-T Date: Sun, 20 Jul 2025 00:19:13 +0530 Subject: [PATCH 01/11] feat: add AAC to MP3 audio converter tool --- public/locales/en/audio.json | 6 ++ .../audio/AAC-MP3/AAC-MP3.service.test.ts | 44 ++++++++++++++ src/pages/tools/audio/AAC-MP3/index.tsx | 59 +++++++++++++++++++ src/pages/tools/audio/AAC-MP3/meta.ts | 15 +++++ src/pages/tools/audio/AAC-MP3/service.ts | 35 +++++++++++ src/pages/tools/audio/index.ts | 4 +- 6 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts create mode 100644 src/pages/tools/audio/AAC-MP3/index.tsx create mode 100644 src/pages/tools/audio/AAC-MP3/meta.ts create mode 100644 src/pages/tools/audio/AAC-MP3/service.ts diff --git a/public/locales/en/audio.json b/public/locales/en/audio.json index 5184efe..9a95626 100644 --- a/public/locales/en/audio.json +++ b/public/locales/en/audio.json @@ -57,5 +57,11 @@ "title": "What is {{title}}?" }, "trimmingAudio": "Trimming Audio" + }, + "AACMP3": { + "title": "AAC to MP3", + "description": "convert AAC audio files to MP3 format.Which is a common audio format used for music and other audio files.", + "shortDescription": "convert AAC audio files to MP3 format.", + "longDescription": "convert AAC audio files to MP3 format. MP3 is a widely used audio format that provides good sound quality and is compatible with most devices and platforms. This tool allows you to convert your AAC files to MP3 easily, making it convenient for playback on various devices." } } diff --git a/src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts b/src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts new file mode 100644 index 0000000..07ae026 --- /dev/null +++ b/src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, vi } from 'vitest'; + +// Mock FFmpeg and fetchFile +vi.mock('@ffmpeg/ffmpeg', () => ({ + FFmpeg: vi.fn().mockImplementation(() => ({ + load: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4, 5])), + unlink: vi.fn().mockResolvedValue(undefined) + })) +})); + +vi.mock('@ffmpeg/util', () => ({ + fetchFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4, 5])) +})); + +// Import service +import { AACtoMp3 } from './service'; + +describe('convertAACtoMP3', () => { + it('should return a new MP3 File when given a valid AAC file', async () => { + const mockAACData = new Uint8Array([0, 1, 2, 3, 4, 5]); + const mockFile = new File([mockAACData], 'sample.aac', { + type: 'audio/aac' + }); + + const result = await AACtoMp3(mockFile); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('sample.mp3'); + expect(result.type).toBe('audio/mpeg'); + }); + + it('should throw error if file type is not AAC', async () => { + const mockFile = new File(['dummy'], 'song.wav', { + type: 'audio/wav' + }); + + await expect(() => AACtoMp3(mockFile)).rejects.toThrowError( + 'Only .aac files are allowed.' // FIXED to match actual error + ); + }); +}); diff --git a/src/pages/tools/audio/AAC-MP3/index.tsx b/src/pages/tools/audio/AAC-MP3/index.tsx new file mode 100644 index 0000000..bab58cc --- /dev/null +++ b/src/pages/tools/audio/AAC-MP3/index.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolAudioInput from '@components/input/ToolAudioInput'; +import ToolFileResult from '@components/result/ToolFileResult'; + +import { AACtoMp3 } from './service'; + +export default function AACMP3({ title, longDescription }: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const compute = async ( + _optionsValues: {}, + input: File | null + ): Promise => { + if (!input) return; + + try { + if (!input.name.toLowerCase().endsWith('.aac')) { + setInput(null); + alert('please upload .aac files are allowed.'); + setResult(null); + + return; + } + setLoading(true); + const resultFile = await AACtoMp3(input); + setResult(resultFile); + } catch (error) { + console.error('Conversion failed:', error); + setResult(null); + } + setLoading(false); + }; + + return ( + + } + resultComponent={ + + } + initialValues={{}} + getGroups={null} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/audio/AAC-MP3/meta.ts b/src/pages/tools/audio/AAC-MP3/meta.ts new file mode 100644 index 0000000..5252bda --- /dev/null +++ b/src/pages/tools/audio/AAC-MP3/meta.ts @@ -0,0 +1,15 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('audio', { + i18n: { + name: 'audio:AACMP3.title', + description: 'audio:AACMP3.description', + shortDescription: 'audio:AACMP3.shortDescription', + longDescription: 'audio:AACMP3.longDescription' + }, + path: 'AAC-MP3', + icon: 'bi:filetype-mp3', + keywords: ['AAC', 'MP3', 'convert', 'audio', 'file conversion'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/audio/AAC-MP3/service.ts b/src/pages/tools/audio/AAC-MP3/service.ts new file mode 100644 index 0000000..f3d37d6 --- /dev/null +++ b/src/pages/tools/audio/AAC-MP3/service.ts @@ -0,0 +1,35 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; + +const ffmpeg = new FFmpeg(); +let isLoaded = false; + +export async function AACtoMp3(input: File): Promise { + if (!isLoaded) { + await ffmpeg.load(); + isLoaded = true; + } + + const inName = 'input.aac'; + const outName = 'output.mp3'; + + await ffmpeg.writeFile(inName, await fetchFile(input)); + + await ffmpeg.exec([ + '-i', + inName, + '-c:a', + 'libmp3lame', + '-b:a', + '192k', + outName + ]); + + const data = await ffmpeg.readFile(outName); + + const mp3 = new File([data], input.name.replace(/\.aac$/i, '.mp3'), { + type: 'audio/mpeg' + }); + + return mp3; +} diff --git a/src/pages/tools/audio/index.ts b/src/pages/tools/audio/index.ts index 3596a1c..12341b4 100644 --- a/src/pages/tools/audio/index.ts +++ b/src/pages/tools/audio/index.ts @@ -1,3 +1,4 @@ +import { tool as audioAACMP3 } from './AAC-MP3/meta'; import { tool as audioMergeAudio } from './merge-audio/meta'; import { tool as audioTrim } from './trim/meta'; import { tool as audioChangeSpeed } from './change-speed/meta'; @@ -7,5 +8,6 @@ export const audioTools = [ audioExtractAudio, audioChangeSpeed, audioTrim, - audioMergeAudio + audioMergeAudio, + audioAACMP3 ]; From dffabc8134ab52238fb65d44a7757278f3161f4f Mon Sep 17 00:00:00 2001 From: Srivarshan-T Date: Fri, 25 Jul 2025 13:54:48 +0530 Subject: [PATCH 02/11] feat: implement audio converter tool with support for MP3, AAC, and WAV formats --- public/locales/en/converters.json | 8 ++ public/locales/en/translation.json | 12 ++ .../audio/AAC-MP3/AAC-MP3.service.test.ts | 44 ------- src/pages/tools/audio/AAC-MP3/index.tsx | 59 --------- src/pages/tools/audio/AAC-MP3/meta.ts | 15 --- src/pages/tools/audio/AAC-MP3/service.ts | 35 ------ src/pages/tools/audio/index.ts | 4 +- .../audio-converter.service.test.ts | 64 ++++++++++ .../converters/audio-converter/index.tsx | 112 ++++++++++++++++++ .../tools/converters/audio-converter/meta.ts | 15 +++ .../converters/audio-converter/service.ts | 100 ++++++++++++++++ src/pages/tools/converters/index.ts | 3 + src/tools/defineTool.tsx | 3 +- src/tools/index.ts | 13 +- 14 files changed, 328 insertions(+), 159 deletions(-) create mode 100644 public/locales/en/converters.json delete mode 100644 src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts delete mode 100644 src/pages/tools/audio/AAC-MP3/index.tsx delete mode 100644 src/pages/tools/audio/AAC-MP3/meta.ts delete mode 100644 src/pages/tools/audio/AAC-MP3/service.ts create mode 100644 src/pages/tools/converters/audio-converter/audio-converter.service.test.ts create mode 100644 src/pages/tools/converters/audio-converter/index.tsx create mode 100644 src/pages/tools/converters/audio-converter/meta.ts create mode 100644 src/pages/tools/converters/audio-converter/service.ts create mode 100644 src/pages/tools/converters/index.ts diff --git a/public/locales/en/converters.json b/public/locales/en/converters.json new file mode 100644 index 0000000..eb48c4d --- /dev/null +++ b/public/locales/en/converters.json @@ -0,0 +1,8 @@ +{ + "audioconverter": { + "title": "Audio Converter", + "description": "Convert audio files between different formats.", + "shortDescription": "Convert audio files to various formats.", + "longDescription": "This tool allows you to convert audio files from one format to another, supporting a wide range of audio formats for seamless conversion." + } +} diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 789e161..b6c69fb 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -17,11 +17,23 @@ "fileCopied": "File copied", "selectFileDescription": "Click here to select a {{type}} from your device, press Ctrl+V to use a {{type}} from your clipboard, or drag and drop a file from desktop" }, + "converters": { + "audioconverter": { + "title": "Audio Converter", + "description": "Convert audio files between different formats.", + "shortDescription": "Convert audio files to various formats.", + "longDescription": "This tool allows you to convert audio files from one format to another, supporting a wide range of audio formats for seamless conversion." + } + }, "categories": { "audio": { "description": "Tools for working with audio – extract audio from video, adjusting audio speed, merging multiple audio files and much more.", "title": "Audio Tools" }, + "converters": { + "description": "Tools for converting data between different formats – convert images, audio, video, text, and more.", + "title": "Converter Tools" + }, "csv": { "description": "Tools for working with CSV files - convert CSV to different formats, manipulate CSV data, validate CSV structure, and process CSV files efficiently.", "title": "CSV Tools" diff --git a/src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts b/src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts deleted file mode 100644 index 07ae026..0000000 --- a/src/pages/tools/audio/AAC-MP3/AAC-MP3.service.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; - -// Mock FFmpeg and fetchFile -vi.mock('@ffmpeg/ffmpeg', () => ({ - FFmpeg: vi.fn().mockImplementation(() => ({ - load: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - exec: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4, 5])), - unlink: vi.fn().mockResolvedValue(undefined) - })) -})); - -vi.mock('@ffmpeg/util', () => ({ - fetchFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4, 5])) -})); - -// Import service -import { AACtoMp3 } from './service'; - -describe('convertAACtoMP3', () => { - it('should return a new MP3 File when given a valid AAC file', async () => { - const mockAACData = new Uint8Array([0, 1, 2, 3, 4, 5]); - const mockFile = new File([mockAACData], 'sample.aac', { - type: 'audio/aac' - }); - - const result = await AACtoMp3(mockFile); - - expect(result).toBeInstanceOf(File); - expect(result.name).toBe('sample.mp3'); - expect(result.type).toBe('audio/mpeg'); - }); - - it('should throw error if file type is not AAC', async () => { - const mockFile = new File(['dummy'], 'song.wav', { - type: 'audio/wav' - }); - - await expect(() => AACtoMp3(mockFile)).rejects.toThrowError( - 'Only .aac files are allowed.' // FIXED to match actual error - ); - }); -}); diff --git a/src/pages/tools/audio/AAC-MP3/index.tsx b/src/pages/tools/audio/AAC-MP3/index.tsx deleted file mode 100644 index bab58cc..0000000 --- a/src/pages/tools/audio/AAC-MP3/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from 'react'; -import ToolContent from '@components/ToolContent'; -import { ToolComponentProps } from '@tools/defineTool'; -import ToolAudioInput from '@components/input/ToolAudioInput'; -import ToolFileResult from '@components/result/ToolFileResult'; - -import { AACtoMp3 } from './service'; - -export default function AACMP3({ title, longDescription }: ToolComponentProps) { - const [input, setInput] = useState(null); - const [result, setResult] = useState(null); - const [loading, setLoading] = useState(false); - - const compute = async ( - _optionsValues: {}, - input: File | null - ): Promise => { - if (!input) return; - - try { - if (!input.name.toLowerCase().endsWith('.aac')) { - setInput(null); - alert('please upload .aac files are allowed.'); - setResult(null); - - return; - } - setLoading(true); - const resultFile = await AACtoMp3(input); - setResult(resultFile); - } catch (error) { - console.error('Conversion failed:', error); - setResult(null); - } - setLoading(false); - }; - - return ( - - } - resultComponent={ - - } - initialValues={{}} - getGroups={null} - setInput={setInput} - compute={compute} - toolInfo={{ title: `What is a ${title}?`, description: longDescription }} - /> - ); -} diff --git a/src/pages/tools/audio/AAC-MP3/meta.ts b/src/pages/tools/audio/AAC-MP3/meta.ts deleted file mode 100644 index 5252bda..0000000 --- a/src/pages/tools/audio/AAC-MP3/meta.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineTool } from '@tools/defineTool'; -import { lazy } from 'react'; - -export const tool = defineTool('audio', { - i18n: { - name: 'audio:AACMP3.title', - description: 'audio:AACMP3.description', - shortDescription: 'audio:AACMP3.shortDescription', - longDescription: 'audio:AACMP3.longDescription' - }, - path: 'AAC-MP3', - icon: 'bi:filetype-mp3', - keywords: ['AAC', 'MP3', 'convert', 'audio', 'file conversion'], - component: lazy(() => import('./index')) -}); diff --git a/src/pages/tools/audio/AAC-MP3/service.ts b/src/pages/tools/audio/AAC-MP3/service.ts deleted file mode 100644 index f3d37d6..0000000 --- a/src/pages/tools/audio/AAC-MP3/service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { FFmpeg } from '@ffmpeg/ffmpeg'; -import { fetchFile } from '@ffmpeg/util'; - -const ffmpeg = new FFmpeg(); -let isLoaded = false; - -export async function AACtoMp3(input: File): Promise { - if (!isLoaded) { - await ffmpeg.load(); - isLoaded = true; - } - - const inName = 'input.aac'; - const outName = 'output.mp3'; - - await ffmpeg.writeFile(inName, await fetchFile(input)); - - await ffmpeg.exec([ - '-i', - inName, - '-c:a', - 'libmp3lame', - '-b:a', - '192k', - outName - ]); - - const data = await ffmpeg.readFile(outName); - - const mp3 = new File([data], input.name.replace(/\.aac$/i, '.mp3'), { - type: 'audio/mpeg' - }); - - return mp3; -} diff --git a/src/pages/tools/audio/index.ts b/src/pages/tools/audio/index.ts index 12341b4..3596a1c 100644 --- a/src/pages/tools/audio/index.ts +++ b/src/pages/tools/audio/index.ts @@ -1,4 +1,3 @@ -import { tool as audioAACMP3 } from './AAC-MP3/meta'; import { tool as audioMergeAudio } from './merge-audio/meta'; import { tool as audioTrim } from './trim/meta'; import { tool as audioChangeSpeed } from './change-speed/meta'; @@ -8,6 +7,5 @@ export const audioTools = [ audioExtractAudio, audioChangeSpeed, audioTrim, - audioMergeAudio, - audioAACMP3 + audioMergeAudio ]; diff --git a/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts b/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts new file mode 100644 index 0000000..ae4ae6e --- /dev/null +++ b/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts @@ -0,0 +1,64 @@ +import { expect, describe, it, vi, beforeEach } from 'vitest'; + +// Mock FFmpeg since it doesn't support Node.js in tests +vi.mock('@ffmpeg/ffmpeg', () => ({ + FFmpeg: vi.fn().mockImplementation(() => ({ + loaded: false, + load: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue(new Uint8Array([10, 20, 30, 40, 50])), + deleteFile: vi.fn().mockResolvedValue(undefined) + })) +})); + +vi.mock('@ffmpeg/util', () => ({ + fetchFile: vi.fn().mockResolvedValue(new Uint8Array([10, 20, 30, 40, 50])) +})); + +import { convertAudio } from './service'; + +describe('convertAudio', () => { + let mockInputFile: File; + + beforeEach(() => { + const mockAudioData = new Uint8Array([1, 2, 3, 4, 5]); + mockInputFile = new File([mockAudioData], 'input.aac', { + type: 'audio/aac' + }); + }); + + it('should convert to MP3 format correctly', async () => { + const outputFormat = 'mp3' as const; + const result = await convertAudio(mockInputFile, outputFormat); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('input.mp3'); // base name + outputFormat extension + expect(result.type).toBe('audio/mpeg'); + }); + + it('should convert to AAC format correctly', async () => { + const outputFormat = 'aac' as const; + const result = await convertAudio(mockInputFile, outputFormat); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('input.aac'); + expect(result.type).toBe('audio/aac'); + }); + + it('should convert to WAV format correctly', async () => { + const outputFormat = 'wav' as const; + const result = await convertAudio(mockInputFile, outputFormat); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('input.wav'); + expect(result.type).toBe('audio/wav'); + }); + + it('should throw error for unsupported formats', async () => { + // @ts-expect-error - intentionally passing unsupported format + await expect(convertAudio(mockInputFile, 'flac')).rejects.toThrow( + 'Unsupported output format' + ); + }); +}); diff --git a/src/pages/tools/converters/audio-converter/index.tsx b/src/pages/tools/converters/audio-converter/index.tsx new file mode 100644 index 0000000..10f55a5 --- /dev/null +++ b/src/pages/tools/converters/audio-converter/index.tsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import { Box } from '@mui/material'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolAudioInput from '@components/input/ToolAudioInput'; +import ToolFileResult from '@components/result/ToolFileResult'; +import SelectWithDesc from '@components/options/SelectWithDesc'; +import { convertAudio } from './service'; +import { useTranslation } from 'react-i18next'; +import { GetGroupsType } from '@components/options/ToolOptions'; + +type InitialValuesType = { + outputFormat: 'mp3' | 'aac' | 'wav'; +}; + +const initialValues: InitialValuesType = { + outputFormat: 'mp3' +}; + +export default function AudioConverter({ + title, + longDescription +}: ToolComponentProps) { + const { t } = useTranslation('audio'); + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + // Explicitly type getGroups to match GetGroupsType + const getGroups: GetGroupsType = ({ + values, + updateField + }) => [ + { + id: 'output-format', + title: t('audioConverter.outputFormat', 'Output Format'), + description: t( + 'audioConverter.outputFormatDescription', + 'Select the desired output audio format' + ), + component: ( + + + updateField( + 'outputFormat', + value as InitialValuesType['outputFormat'] + ) + } + options={[ + { label: 'MP3', value: 'mp3' }, + { label: 'AAC', value: 'aac' }, + { label: 'WAV', value: 'wav' } + ]} + description={t( + 'audioConverter.outputFormatDescription', + 'Select the desired output audio format' + )} + /> + + ) + } + ]; + + const compute = async ( + values: InitialValuesType, + inputFile: File | null + ): Promise => { + if (!inputFile) return; + + try { + setLoading(true); + const resultFile = await convertAudio(inputFile, values.outputFormat); + setResult(resultFile); + } catch (error) { + console.error('Conversion failed:', error); + setResult(null); + } finally { + setLoading(false); + } + }; + + return ( + + } + resultComponent={ + + } + initialValues={initialValues} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ + title: `What is a ${title}?`, + description: longDescription + }} + /> + ); +} diff --git a/src/pages/tools/converters/audio-converter/meta.ts b/src/pages/tools/converters/audio-converter/meta.ts new file mode 100644 index 0000000..757fed4 --- /dev/null +++ b/src/pages/tools/converters/audio-converter/meta.ts @@ -0,0 +1,15 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('converters', { + i18n: { + name: 'translation:converters.audioconverter.title', + description: 'translation:converters.audioconverter.description', + shortDescription: 'translation:converters.audioconverter.shortDescription', + longDescription: 'translation:converters.audioconverter.longDescription' + }, + path: 'audio-converter', + icon: 'mdi:music-note-outline', + keywords: ['audio', 'converter'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/converters/audio-converter/service.ts b/src/pages/tools/converters/audio-converter/service.ts new file mode 100644 index 0000000..e2676c3 --- /dev/null +++ b/src/pages/tools/converters/audio-converter/service.ts @@ -0,0 +1,100 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; + +const ffmpeg = new FFmpeg(); +let isLoaded = false; + +async function loadFFmpeg() { + if (!isLoaded) { + await ffmpeg.load(); + isLoaded = true; + } +} + +/** + * Converts input audio file to selected output format ('mp3', 'aac', or 'wav'). + * Supports any input audio file type accepted by FFmpeg. + * + * @param input - Source audio File + * @param outputFormat - 'mp3' | 'aac' | 'wav' + * @returns Converted audio File + */ +export async function convertAudio( + input: File, + outputFormat: 'mp3' | 'aac' | 'wav' +): Promise { + await loadFFmpeg(); + + // Use the original input extension for input filename + const inputExtMatch = input.name.match(/\.[^.]+$/); + const inputExt = inputExtMatch ? inputExtMatch[0] : '.audio'; + + const inputFileName = `input${inputExt}`; + const outputFileName = `output.${outputFormat}`; + + // Write the input file to FFmpeg FS + await ffmpeg.writeFile(inputFileName, await fetchFile(input)); + + // Build the FFmpeg args depending on the output format + // You can customize the codec and bitrate options per format here + let args: string[]; + + switch (outputFormat) { + case 'mp3': + args = [ + '-i', + inputFileName, + '-c:a', + 'libmp3lame', + '-b:a', + '192k', + outputFileName + ]; + break; + + case 'aac': + args = [ + '-i', + inputFileName, + '-c:a', + 'aac', + '-b:a', + '192k', + outputFileName + ]; + break; + + case 'wav': + args = ['-i', inputFileName, '-c:a', 'pcm_s16le', outputFileName]; + break; + + default: + throw new Error(`Unsupported output format: ${outputFormat}`); + } + + // Execute ffmpeg with arguments + await ffmpeg.exec(args); + + // Read the output file from FFmpeg FS + const data = await ffmpeg.readFile(outputFileName); + + // Determine MIME type by outputFormat + let mimeType = ''; + switch (outputFormat) { + case 'mp3': + mimeType = 'audio/mpeg'; + break; + case 'aac': + mimeType = 'audio/aac'; + break; + case 'wav': + mimeType = 'audio/wav'; + break; + } + + // Create a new File with the original name but new extension + const baseName = input.name.replace(/\.[^.]+$/, ''); + const convertedFileName = `${baseName}.${outputFormat}`; + + return new File([data], convertedFileName, { type: mimeType }); +} diff --git a/src/pages/tools/converters/index.ts b/src/pages/tools/converters/index.ts new file mode 100644 index 0000000..59ce53b --- /dev/null +++ b/src/pages/tools/converters/index.ts @@ -0,0 +1,3 @@ +import { tool as convertersAudioConverter } from './audio-converter/meta'; + +export const convertersTools = [convertersAudioConverter]; diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index ed19326..4f776a1 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -30,7 +30,8 @@ export type ToolCategory = | 'pdf' | 'image-generic' | 'audio' - | 'xml'; + | 'xml' + | 'converters'; export interface DefinedTool { type: ToolCategory; diff --git a/src/tools/index.ts b/src/tools/index.ts index dd1b723..fff2b76 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -13,6 +13,7 @@ import { timeTools } from '../pages/tools/time'; import { IconifyIcon } from '@iconify/react'; import { pdfTools } from '../pages/tools/pdf'; import { xmlTools } from '../pages/tools/xml'; +import { convertersTools } from '../pages/tools/converters'; import { TFunction } from 'i18next'; import { FullI18nKey, I18nNamespaces } from '../i18n'; @@ -30,7 +31,8 @@ const toolCategoriesOrder: ToolCategory[] = [ 'png', 'time', 'xml', - 'gif' + 'gif', + 'converters' ]; export const tools: DefinedTool[] = [ ...imageTools, @@ -43,7 +45,8 @@ export const tools: DefinedTool[] = [ ...numberTools, ...timeTools, ...audioTools, - ...xmlTools + ...xmlTools, + ...convertersTools ]; const categoriesConfig: { type: ToolCategory; @@ -134,6 +137,12 @@ const categoriesConfig: { icon: 'mdi-light:xml', value: 'translation:categories.xml.description', title: 'translation:categories.xml.title' + }, + { + type: 'converters', + icon: 'mdi:swap-horizontal', + value: 'translation:categories.converters.description', + title: 'translation:categories.converters.title' } ]; // use for changelogs From 1a9252ffb582c25806d74c42295216b3700bc46c Mon Sep 17 00:00:00 2001 From: Srivarshan-T <140912234+Srivarshan-T@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:18:49 +0530 Subject: [PATCH 03/11] Update audio.json removed the previously added aactomp3 descriptions --- public/locales/en/audio.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/public/locales/en/audio.json b/public/locales/en/audio.json index 9a95626..17bf435 100644 --- a/public/locales/en/audio.json +++ b/public/locales/en/audio.json @@ -57,11 +57,5 @@ "title": "What is {{title}}?" }, "trimmingAudio": "Trimming Audio" - }, - "AACMP3": { - "title": "AAC to MP3", - "description": "convert AAC audio files to MP3 format.Which is a common audio format used for music and other audio files.", - "shortDescription": "convert AAC audio files to MP3 format.", - "longDescription": "convert AAC audio files to MP3 format. MP3 is a widely used audio format that provides good sound quality and is compatible with most devices and platforms. This tool allows you to convert your AAC files to MP3 easily, making it convenient for playback on various devices." - } + } } From 82b56b49731e37a3f43ce118116fcb0b39ae7be1 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 16:43:04 +0100 Subject: [PATCH 04/11] Chore: - 'file' util added - getFileExtension method --- src/utils/file.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/utils/file.ts diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..a3200ac --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,11 @@ +/** + * Returns the file extension + * + * @param {string} filename - The filename + * @return {string} - the file extension + */ +export function getFileExtension(filename: string): string { + const lastDot = filename.lastIndexOf('.'); + if (lastDot <= 0) return ''; // No extension + return filename.slice(lastDot + 1).toLowerCase(); +} From cfc0ba3b418469bfde073e42d07438db2e54a2a8 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 16:45:10 +0100 Subject: [PATCH 05/11] chore: - 'types.ts' file - global variables including necessary details about audio formats added --- .../tools/converters/audio-converter/types.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/pages/tools/converters/audio-converter/types.ts diff --git a/src/pages/tools/converters/audio-converter/types.ts b/src/pages/tools/converters/audio-converter/types.ts new file mode 100644 index 0000000..d749b7a --- /dev/null +++ b/src/pages/tools/converters/audio-converter/types.ts @@ -0,0 +1,16 @@ +export const AUDIO_FORMATS = { + // Lossy formats + mp3: { codec: 'libmp3lame', bitrate: '192k', mimeType: 'audio/mpeg' }, + aac: { codec: 'aac', bitrate: '192k', mimeType: 'audio/aac' }, + ogg: { codec: 'libvorbis', bitrate: '192k', mimeType: 'audio/ogg' }, + + // Lossless formats + wav: { codec: 'pcm_s16le', bitrate: null, mimeType: 'audio/wav' }, + flac: { codec: 'flac', bitrate: null, mimeType: 'audio/flac' } +} as const; + +export type AudioFormat = keyof typeof AUDIO_FORMATS; + +export type InitialValuesType = { + outputFormat: AudioFormat; +}; From 09456bf6eda3356e3da4b52e1ee59a9cc4bf4c40 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 16:47:51 +0100 Subject: [PATCH 06/11] chore: - use of InitialValuesType - cleaning up added after conversion --- .../converters/audio-converter/service.ts | 92 +++++++------------ 1 file changed, 34 insertions(+), 58 deletions(-) diff --git a/src/pages/tools/converters/audio-converter/service.ts b/src/pages/tools/converters/audio-converter/service.ts index e2676c3..aaac23e 100644 --- a/src/pages/tools/converters/audio-converter/service.ts +++ b/src/pages/tools/converters/audio-converter/service.ts @@ -1,6 +1,12 @@ import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile } from '@ffmpeg/util'; +import { InitialValuesType, AUDIO_FORMATS } from './types'; +import { getFileExtension } from 'utils/file'; +/** + * optimzed call for FFmpeg instance creation, + * avoiding to download required FFmpeg binaries on every reload + */ const ffmpeg = new FFmpeg(); let isLoaded = false; @@ -21,80 +27,50 @@ async function loadFFmpeg() { */ export async function convertAudio( input: File, - outputFormat: 'mp3' | 'aac' | 'wav' + options: InitialValuesType ): Promise { await loadFFmpeg(); // Use the original input extension for input filename - const inputExtMatch = input.name.match(/\.[^.]+$/); - const inputExt = inputExtMatch ? inputExtMatch[0] : '.audio'; + const inputExt = getFileExtension(input.name); - const inputFileName = `input${inputExt}`; - const outputFileName = `output.${outputFormat}`; + if (inputExt === options.outputFormat) return input; + + const inputFileName = inputExt ? `input.${inputExt}` : 'input'; + const outputFileName = `output.${options.outputFormat}`; // Write the input file to FFmpeg FS await ffmpeg.writeFile(inputFileName, await fetchFile(input)); // Build the FFmpeg args depending on the output format // You can customize the codec and bitrate options per format here - let args: string[]; - switch (outputFormat) { - case 'mp3': - args = [ - '-i', - inputFileName, - '-c:a', - 'libmp3lame', - '-b:a', - '192k', - outputFileName - ]; - break; + const format = AUDIO_FORMATS[options.outputFormat]; + const { codec, bitrate, mimeType } = format; - case 'aac': - args = [ - '-i', - inputFileName, - '-c:a', - 'aac', - '-b:a', - '192k', - outputFileName - ]; - break; - - case 'wav': - args = ['-i', inputFileName, '-c:a', 'pcm_s16le', outputFileName]; - break; - - default: - throw new Error(`Unsupported output format: ${outputFormat}`); - } + const args = bitrate + ? ['-i', inputFileName, '-c:a', codec, '-b:a', bitrate, outputFileName] + : ['-i', inputFileName, '-c:a', codec, outputFileName]; // Execute ffmpeg with arguments - await ffmpeg.exec(args); + try { + await ffmpeg.exec(args); - // Read the output file from FFmpeg FS - const data = await ffmpeg.readFile(outputFileName); + // Read the output file from FFmpeg FS + const data = await ffmpeg.readFile(outputFileName); - // Determine MIME type by outputFormat - let mimeType = ''; - switch (outputFormat) { - case 'mp3': - mimeType = 'audio/mpeg'; - break; - case 'aac': - mimeType = 'audio/aac'; - break; - case 'wav': - mimeType = 'audio/wav'; - break; + // Create a new File with the original name but new extension + const baseName = input.name.replace(/\.[^.]+$/, ''); + const convertedFileName = `${baseName}.${options.outputFormat}`; + + return new File([data], convertedFileName, { type: mimeType }); + } finally { + // Clean up FFmpeg virtual filesystem + try { + await ffmpeg.deleteFile(inputFileName); + await ffmpeg.deleteFile(outputFileName); + } catch (e) { + // Ignore cleanup errors + } } - - // Create a new File with the original name but new extension - const baseName = input.name.replace(/\.[^.]+$/, ''); - const convertedFileName = `${baseName}.${outputFormat}`; - - return new File([data], convertedFileName, { type: mimeType }); } From ff27f8debe251cd89d961c151f403fb40707a937 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 16:50:18 +0100 Subject: [PATCH 07/11] chore: select values populated through AUDIO_FORMATS iteration --- .../converters/audio-converter/index.tsx | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/src/pages/tools/converters/audio-converter/index.tsx b/src/pages/tools/converters/audio-converter/index.tsx index 10f55a5..1654bf7 100644 --- a/src/pages/tools/converters/audio-converter/index.tsx +++ b/src/pages/tools/converters/audio-converter/index.tsx @@ -8,10 +8,7 @@ import SelectWithDesc from '@components/options/SelectWithDesc'; import { convertAudio } from './service'; import { useTranslation } from 'react-i18next'; import { GetGroupsType } from '@components/options/ToolOptions'; - -type InitialValuesType = { - outputFormat: 'mp3' | 'aac' | 'wav'; -}; +import { InitialValuesType, AUDIO_FORMATS, AudioFormat } from './types'; const initialValues: InitialValuesType = { outputFormat: 'mp3' @@ -26,6 +23,24 @@ export default function AudioConverter({ const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); + const compute = async ( + values: InitialValuesType, + inputFile: File | null + ): Promise => { + if (!inputFile) return; + + try { + setLoading(true); + const resultFile = await convertAudio(inputFile, values); + setResult(resultFile); + } catch (error) { + console.error('Conversion failed:', error); + setResult(null); + } finally { + setLoading(false); + } + }; + // Explicitly type getGroups to match GetGroupsType const getGroups: GetGroupsType = ({ values, @@ -42,17 +57,11 @@ export default function AudioConverter({ - updateField( - 'outputFormat', - value as InitialValuesType['outputFormat'] - ) - } - options={[ - { label: 'MP3', value: 'mp3' }, - { label: 'AAC', value: 'aac' }, - { label: 'WAV', value: 'wav' } - ]} + onChange={(value) => updateField('outputFormat', value)} + options={Object.entries(AUDIO_FORMATS).map(([value]) => ({ + label: value.toUpperCase(), + value: value as AudioFormat + }))} description={t( 'audioConverter.outputFormatDescription', 'Select the desired output audio format' @@ -63,24 +72,6 @@ export default function AudioConverter({ } ]; - const compute = async ( - values: InitialValuesType, - inputFile: File | null - ): Promise => { - if (!inputFile) return; - - try { - setLoading(true); - const resultFile = await convertAudio(inputFile, values.outputFormat); - setResult(resultFile); - } catch (error) { - console.error('Conversion failed:', error); - setResult(null); - } finally { - setLoading(false); - } - }; - return ( Date: Wed, 10 Dec 2025 17:01:26 +0100 Subject: [PATCH 08/11] tests: convert audio test cases --- .../audio-converter.service.test.ts | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts b/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts index ae4ae6e..9c23299 100644 --- a/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts +++ b/src/pages/tools/converters/audio-converter/audio-converter.service.test.ts @@ -1,4 +1,6 @@ import { expect, describe, it, vi, beforeEach } from 'vitest'; +import { convertAudio } from './service'; +import { InitialValuesType } from './types'; // Mock FFmpeg since it doesn't support Node.js in tests vi.mock('@ffmpeg/ffmpeg', () => ({ @@ -16,8 +18,6 @@ vi.mock('@ffmpeg/util', () => ({ fetchFile: vi.fn().mockResolvedValue(new Uint8Array([10, 20, 30, 40, 50])) })); -import { convertAudio } from './service'; - describe('convertAudio', () => { let mockInputFile: File; @@ -29,17 +29,17 @@ describe('convertAudio', () => { }); it('should convert to MP3 format correctly', async () => { - const outputFormat = 'mp3' as const; - const result = await convertAudio(mockInputFile, outputFormat); + const options: InitialValuesType = { outputFormat: 'mp3' }; + const result = await convertAudio(mockInputFile, options); expect(result).toBeInstanceOf(File); - expect(result.name).toBe('input.mp3'); // base name + outputFormat extension + expect(result.name).toBe('input.mp3'); expect(result.type).toBe('audio/mpeg'); }); it('should convert to AAC format correctly', async () => { - const outputFormat = 'aac' as const; - const result = await convertAudio(mockInputFile, outputFormat); + const options: InitialValuesType = { outputFormat: 'aac' }; + const result = await convertAudio(mockInputFile, options); expect(result).toBeInstanceOf(File); expect(result.name).toBe('input.aac'); @@ -47,18 +47,49 @@ describe('convertAudio', () => { }); it('should convert to WAV format correctly', async () => { - const outputFormat = 'wav' as const; - const result = await convertAudio(mockInputFile, outputFormat); + const options: InitialValuesType = { outputFormat: 'wav' }; + const result = await convertAudio(mockInputFile, options); expect(result).toBeInstanceOf(File); expect(result.name).toBe('input.wav'); expect(result.type).toBe('audio/wav'); }); - it('should throw error for unsupported formats', async () => { - // @ts-expect-error - intentionally passing unsupported format - await expect(convertAudio(mockInputFile, 'flac')).rejects.toThrow( - 'Unsupported output format' - ); + it('should convert to FLAC format correctly', async () => { + const options: InitialValuesType = { outputFormat: 'flac' }; + const result = await convertAudio(mockInputFile, options); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('input.flac'); + expect(result.type).toBe('audio/flac'); + }); + + it('should convert to OGG format correctly', async () => { + const options: InitialValuesType = { outputFormat: 'ogg' }; + const result = await convertAudio(mockInputFile, options); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('input.ogg'); + expect(result.type).toBe('audio/ogg'); + }); + + it('should return original file if input format matches output format', async () => { + const options: InitialValuesType = { outputFormat: 'aac' }; + const result = await convertAudio(mockInputFile, options); + + expect(result).toBe(mockInputFile); // Same instance + expect(result.name).toBe('input.aac'); + }); + + it('should handle files without extensions', async () => { + const fileNoExt = new File([new Uint8Array([1, 2, 3])], 'audiofile', { + type: 'audio/aac' + }); + const options: InitialValuesType = { outputFormat: 'mp3' }; + const result = await convertAudio(fileNoExt, options); + + expect(result).toBeInstanceOf(File); + expect(result.name).toBe('audiofile.mp3'); + expect(result.type).toBe('audio/mpeg'); }); }); From 29852a44dd85719b00f63a3c519aecd9d2095f25 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 17:19:02 +0100 Subject: [PATCH 09/11] chore: ToolAudioInput accepted types changed to supported conversion formats --- src/components/input/ToolAudioInput.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/input/ToolAudioInput.tsx b/src/components/input/ToolAudioInput.tsx index c9090a7..143973c 100644 --- a/src/components/input/ToolAudioInput.tsx +++ b/src/components/input/ToolAudioInput.tsx @@ -2,13 +2,19 @@ import React, { useRef } from 'react'; import { Box, Typography } from '@mui/material'; import BaseFileInput from './BaseFileInput'; import { BaseFileInputProps } from './file-input-utils'; +import { AUDIO_FORMATS } from 'pages/tools/converters/audio-converter/types'; interface AudioFileInputProps extends Omit { accept?: string[]; } +const AUDIO_ACCEPT_TYPES = [ + 'audio/*', + ...Object.keys(AUDIO_FORMATS).map((format) => `.${format}`) +]; + export default function ToolAudioInput({ - accept = ['audio/*', '.mp3', '.wav', '.aac'], + accept = AUDIO_ACCEPT_TYPES, ...props }: AudioFileInputProps) { const audioRef = useRef(null); From 29a174bc2218cc737fa21e2dbf88e3179560ce7c Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 18:16:20 +0100 Subject: [PATCH 10/11] fix: data wrapped inside blob --- src/pages/tools/converters/audio-converter/service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/converters/audio-converter/service.ts b/src/pages/tools/converters/audio-converter/service.ts index aaac23e..dc243e3 100644 --- a/src/pages/tools/converters/audio-converter/service.ts +++ b/src/pages/tools/converters/audio-converter/service.ts @@ -63,7 +63,13 @@ export async function convertAudio( const baseName = input.name.replace(/\.[^.]+$/, ''); const convertedFileName = `${baseName}.${options.outputFormat}`; - return new File([data], convertedFileName, { type: mimeType }); + return new File( + [new Blob([data as any], { type: mimeType })], + convertedFileName, + { + type: mimeType + } + ); } finally { // Clean up FFmpeg virtual filesystem try { From 13fee3aefa3a2cd3a350144c7c1b72638b6ded50 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Wed, 10 Dec 2025 18:17:34 +0100 Subject: [PATCH 11/11] chore: - convertes type specified (generalUsers) - category icon changed --- src/tools/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index 481e073..1dbeb2f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -140,7 +140,7 @@ const categoriesConfig: { }, { type: 'converters', - icon: 'mdi:swap-horizontal', + icon: 'streamline-plump:convert-pdf-1', value: 'translation:categories.converters.description', title: 'translation:categories.converters.title' } @@ -154,7 +154,8 @@ const CATEGORIES_USER_TYPES_MAPPINGS: Partial> = png: 'generalUsers', 'image-generic': 'generalUsers', video: 'generalUsers', - audio: 'generalUsers' + audio: 'generalUsers', + converters: 'generalUsers' }; // Filter tools by user types export const filterToolsByUserTypes = (