feat: introduce audio merging and trimming tools with support for multiple formats

This commit is contained in:
AshAnand34
2025-07-07 15:50:06 -07:00
parent a1b929e45c
commit 7962bba04f
13 changed files with 851 additions and 1 deletions

View File

@@ -0,0 +1,128 @@
import { Box, FormControlLabel, Radio, RadioGroup } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import { GetGroupsType } from '@components/options/ToolOptions';
import { InitialValuesType } from './types';
import ToolAudioInput from '@components/input/ToolAudioInput';
import ToolFileResult from '@components/result/ToolFileResult';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { trimAudio } from './service';
const initialValues: InitialValuesType = {
startTime: '00:00:00',
endTime: '00:01:00',
outputFormat: 'mp3'
};
const formatOptions = [
{ label: 'MP3', value: 'mp3' },
{ label: 'AAC', value: 'aac' },
{ label: 'WAV', value: 'wav' }
];
export default function Trim({ title, longDescription }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const compute = async (
optionsValues: InitialValuesType,
input: File | null
) => {
if (!input) return;
setLoading(true);
try {
const trimmedFile = await trimAudio(input, optionsValues);
setResult(trimmedFile);
} catch (err) {
console.error(`Failed to trim audio: ${err}`);
setResult(null);
} finally {
setLoading(false);
}
};
const getGroups: GetGroupsType<InitialValuesType> | null = ({
values,
updateField
}) => [
{
title: 'Time Settings',
component: (
<Box>
<TextFieldWithDesc
value={values.startTime}
onOwnChange={(val) => updateField('startTime', val)}
description="Start time in format HH:MM:SS (e.g., 00:00:30)"
label="Start Time"
/>
<Box mt={2}>
<TextFieldWithDesc
value={values.endTime}
onOwnChange={(val) => updateField('endTime', val)}
description="End time in format HH:MM:SS (e.g., 00:01:30)"
label="End Time"
/>
</Box>
</Box>
)
},
{
title: 'Output Format',
component: (
<Box mt={2}>
<RadioGroup
row
value={values.outputFormat}
onChange={(e) =>
updateField(
'outputFormat',
e.target.value as 'mp3' | 'aac' | 'wav'
)
}
>
{formatOptions.map((opt) => (
<FormControlLabel
key={opt.value}
value={opt.value}
control={<Radio />}
label={opt.label}
/>
))}
</RadioGroup>
</Box>
)
}
];
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolAudioInput
value={input}
onChange={setInput}
title={'Input Audio'}
/>
}
resultComponent={
loading ? (
<ToolFileResult title="Trimming Audio" value={null} loading={true} />
) : (
<ToolFileResult
title="Trimmed Audio"
value={result}
extension={result ? result.name.split('.').pop() : undefined}
/>
)
}
initialValues={initialValues}
getGroups={getGroups}
setInput={setInput}
compute={compute}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}

View File

@@ -0,0 +1,27 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('audio', {
name: 'Trim Audio',
path: 'trim',
icon: 'mdi:scissors-cutting',
description:
'Cut and trim audio files to extract specific segments by specifying start and end times.',
shortDescription:
'Trim audio files to extract specific time segments (MP3, AAC, WAV).',
keywords: [
'trim',
'audio',
'cut',
'segment',
'extract',
'mp3',
'aac',
'wav',
'audio editing',
'time'
],
longDescription:
'This tool allows you to trim audio files by specifying start and end times. You can extract specific segments from longer audio files, remove unwanted parts, or create shorter clips. Supports various audio formats including MP3, AAC, and WAV. Perfect for podcast editing, music production, or any audio editing needs.',
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,108 @@
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';
import { InitialValuesType } from './types';
const ffmpeg = new FFmpeg();
export async function trimAudio(
input: File,
options: InitialValuesType
): Promise<File> {
if (!ffmpeg.loaded) {
await ffmpeg.load({
wasmURL:
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
});
}
const inputName = 'input.mp3';
await ffmpeg.writeFile(inputName, await fetchFile(input));
const { startTime, endTime, outputFormat } = options;
const outputName = `output.${outputFormat}`;
// Build FFmpeg arguments for trimming
let args: string[] = [
'-i',
inputName,
'-ss',
startTime, // Start time
'-to',
endTime, // End time
'-c',
'copy' // Copy without re-encoding for speed
];
// Add format-specific arguments
if (outputFormat === 'mp3') {
args = [
'-i',
inputName,
'-ss',
startTime,
'-to',
endTime,
'-ar',
'44100',
'-ac',
'2',
'-b:a',
'192k',
'-f',
'mp3',
outputName
];
} else if (outputFormat === 'aac') {
args = [
'-i',
inputName,
'-ss',
startTime,
'-to',
endTime,
'-c:a',
'aac',
'-b:a',
'192k',
'-f',
'adts',
outputName
];
} else if (outputFormat === 'wav') {
args = [
'-i',
inputName,
'-ss',
startTime,
'-to',
endTime,
'-acodec',
'pcm_s16le',
'-ar',
'44100',
'-ac',
'2',
'-f',
'wav',
outputName
];
}
await ffmpeg.exec(args);
const trimmedAudio = await ffmpeg.readFile(outputName);
let mimeType = 'audio/mp3';
if (outputFormat === 'aac') mimeType = 'audio/aac';
if (outputFormat === 'wav') mimeType = 'audio/wav';
return new File(
[
new Blob([trimmedAudio], {
type: mimeType
})
],
`${input.name.replace(/\.[^/.]+$/, '')}_trimmed.${outputFormat}`,
{ type: mimeType }
);
}

View File

@@ -0,0 +1,58 @@
import { expect, describe, it, vi } from 'vitest';
// Mock FFmpeg since it doesn't support Node.js
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([1, 2, 3, 4, 5])),
deleteFile: vi.fn().mockResolvedValue(undefined)
}))
}));
vi.mock('@ffmpeg/util', () => ({
fetchFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4, 5]))
}));
import { trimAudio } from './service';
describe('trimAudio', () => {
it('should trim audio file with valid time parameters', async () => {
// Create a mock audio file
const mockAudioData = new Uint8Array([0, 1, 2, 3, 4, 5]);
const mockFile = new File([mockAudioData], 'test.mp3', {
type: 'audio/mp3'
});
const options = {
startTime: '00:00:10',
endTime: '00:00:20',
outputFormat: 'mp3' as const
};
const result = await trimAudio(mockFile, options);
expect(result).toBeInstanceOf(File);
expect(result.name).toContain('_trimmed.mp3');
expect(result.type).toBe('audio/mp3');
});
it('should handle different output formats', async () => {
const mockAudioData = new Uint8Array([0, 1, 2, 3, 4, 5]);
const mockFile = new File([mockAudioData], 'test.wav', {
type: 'audio/wav'
});
const options = {
startTime: '00:00:00',
endTime: '00:00:30',
outputFormat: 'wav' as const
};
const result = await trimAudio(mockFile, options);
expect(result).toBeInstanceOf(File);
expect(result.name).toContain('_trimmed.wav');
expect(result.type).toBe('audio/wav');
});
});

View File

@@ -0,0 +1,5 @@
export type InitialValuesType = {
startTime: string;
endTime: string;
outputFormat: 'mp3' | 'aac' | 'wav';
};