diff --git a/src/pages/tools/audio/change-speed/index.tsx b/src/pages/tools/audio/change-speed/index.tsx new file mode 100644 index 0000000..d995055 --- /dev/null +++ b/src/pages/tools/audio/change-speed/index.tsx @@ -0,0 +1,187 @@ +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 { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; +import RadioWithTextField from '@components/options/RadioWithTextField'; + +const initialValues: InitialValuesType = { + newSpeed: 2, + outputFormat: 'mp3' +}; + +const formatOptions = [ + { label: 'MP3', value: 'mp3' }, + { label: 'AAC', value: 'aac' }, + { label: 'WAV', value: 'wav' } +]; + +export default function ChangeSpeed({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + // FFmpeg only supports a tempo between 0.5 and 2.0, so we chain filters + const computeAudioFilter = (speed: number): string => { + if (speed <= 2 && speed >= 0.5) { + return `atempo=${speed}`; + } + const filters: string[] = []; + let remainingSpeed = speed; + while (remainingSpeed > 2.0) { + filters.push('atempo=2.0'); + remainingSpeed /= 2.0; + } + while (remainingSpeed < 0.5) { + filters.push('atempo=0.5'); + remainingSpeed /= 0.5; + } + filters.push(`atempo=${remainingSpeed.toFixed(2)}`); + return filters.join(','); + }; + + const compute = async ( + optionsValues: InitialValuesType, + input: File | null + ) => { + if (!input) return; + const { newSpeed, outputFormat } = optionsValues; + let ffmpeg: FFmpeg | null = null; + let ffmpegLoaded = false; + setLoading(true); + try { + ffmpeg = new FFmpeg(); + if (!ffmpegLoaded) { + await ffmpeg.load({ + wasmURL: + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' + }); + ffmpegLoaded = true; + } + const fileName = input.name; + const outputName = `output.${outputFormat}`; + await ffmpeg.writeFile(fileName, await fetchFile(input)); + const audioFilter = computeAudioFilter(newSpeed); + let args = ['-i', fileName, '-filter:a', audioFilter]; + if (outputFormat === 'mp3') { + args.push('-b:a', '192k', '-f', 'mp3', outputName); + } else if (outputFormat === 'aac') { + args.push('-c:a', 'aac', '-b:a', '192k', '-f', 'adts', outputName); + } else if (outputFormat === 'wav') { + args.push( + '-acodec', + 'pcm_s16le', + '-ar', + '44100', + '-ac', + '2', + '-f', + 'wav', + outputName + ); + } + await ffmpeg.exec(args); + const data = await ffmpeg.readFile(outputName); + let mimeType = 'audio/mp3'; + if (outputFormat === 'aac') mimeType = 'audio/aac'; + if (outputFormat === 'wav') mimeType = 'audio/wav'; + const blob = new Blob([data], { type: mimeType }); + const newFile = new File( + [blob], + fileName.replace(/\.[^/.]+$/, `-${newSpeed}x.${outputFormat}`), + { type: mimeType } + ); + await ffmpeg.deleteFile(fileName); + await ffmpeg.deleteFile(outputName); + setResult(newFile); + } catch (err) { + console.error(`Failed to process audio: ${err}`); + setResult(null); + } finally { + setLoading(false); + } + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'New Audio Speed', + component: ( + + updateField('newSpeed', Number(val))} + description="Default multiplier: 2 means 2x faster" + type="number" + /> + + ) + }, + { + title: 'Output Format', + component: ( + + + updateField( + 'outputFormat', + e.target.value as 'mp3' | 'aac' | 'wav' + ) + } + > + {formatOptions.map((opt) => ( + } + label={opt.label} + /> + ))} + + + ) + } + ]; + return ( + + } + resultComponent={ + loading ? ( + + ) : ( + + ) + } + initialValues={initialValues} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/audio/change-speed/meta.ts b/src/pages/tools/audio/change-speed/meta.ts new file mode 100644 index 0000000..a23d674 --- /dev/null +++ b/src/pages/tools/audio/change-speed/meta.ts @@ -0,0 +1,13 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('audio', { + name: 'Change speed', + path: 'change-speed', + icon: 'material-symbols-light:speed-outline', + description: + 'This online utility lets you change the speed of an audio. You can speed it up or slow it down.', + shortDescription: 'Quickly change audio speed', + keywords: ['change', 'speed'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/audio/change-speed/service.ts b/src/pages/tools/audio/change-speed/service.ts new file mode 100644 index 0000000..18d6dd6 --- /dev/null +++ b/src/pages/tools/audio/change-speed/service.ts @@ -0,0 +1,8 @@ +import { InitialValuesType } from './types'; + +export function main( + input: File | null, + options: InitialValuesType +): File | null { + return input; +} diff --git a/src/pages/tools/audio/change-speed/types.ts b/src/pages/tools/audio/change-speed/types.ts new file mode 100644 index 0000000..587fc4f --- /dev/null +++ b/src/pages/tools/audio/change-speed/types.ts @@ -0,0 +1,4 @@ +export type InitialValuesType = { + newSpeed: number; + outputFormat: 'mp3' | 'aac' | 'wav'; +}; diff --git a/src/pages/tools/audio/index.ts b/src/pages/tools/audio/index.ts index 40b2828..1c2a77d 100644 --- a/src/pages/tools/audio/index.ts +++ b/src/pages/tools/audio/index.ts @@ -1,2 +1,4 @@ +import { tool as audioChangeSpeed } from './change-speed/meta'; import { tool as audioExtractAudio } from './extract-audio/meta'; -export const audioTools = [audioExtractAudio]; + +export const audioTools = [audioExtractAudio, audioChangeSpeed];