From 0be741e5f6c2ebe3a9d33996bdd43b7cbf778443 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 2 Sep 2025 17:53:31 +0800 Subject: [PATCH] feat: conversion sample rate --- messages/en.json | 3 +- .../components/functional/FancyInput.svelte | 3 +- src/lib/converters/ffmpeg.svelte.ts | 144 +++++++++++++++--- src/lib/sections/settings/Conversion.svelte | 30 ++++ src/lib/sections/settings/index.svelte.ts | 4 + 5 files changed, 165 insertions(+), 19 deletions(-) diff --git a/messages/en.json b/messages/en.json index 4956e45..a104ecb 100644 --- a/messages/en.json +++ b/messages/en.json @@ -98,7 +98,8 @@ "quality_description": "This changes the default output quality of the converted files (in its category). Higher values may result in longer conversion times and file size.", "quality_video": "This changes the default output quality of the converted video files. Higher values may result in longer conversion times and file size.", "quality_audio": "Audio (kbps)", - "quality_images": "Image (%)" + "quality_images": "Image (%)", + "rate": "Sample rate (Hz)" }, "vertd": { "title": "Video conversion", diff --git a/src/lib/components/functional/FancyInput.svelte b/src/lib/components/functional/FancyInput.svelte index 4bb8711..c1b6740 100644 --- a/src/lib/components/functional/FancyInput.svelte +++ b/src/lib/components/functional/FancyInput.svelte @@ -34,7 +34,8 @@ {disabled} class="w-full p-3 rounded-lg bg-panel border-2 border-button {prefix ? 'pl-[2rem]' : 'pl-3'} - {extension ? 'pr-[4rem]' : 'pr-3'}" + {extension ? 'pr-[4rem]' : 'pr-3'} + {disabled && 'opacity-50 cursor-not-allowed'}" /> {#if prefix}
diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index a36290e..d73c167 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -174,7 +174,7 @@ export class FFmpegConverter extends Converter { const bitrateListener = (event: { message: string }) => { if (bitrate !== null) return; const n = parseInt(event.message.trim(), 10); - if (!n) return null; + if (!n) return; bitrate = Math.round(n / 1000); log( ["converters", this.name], @@ -195,6 +195,48 @@ export class FFmpegConverter extends Converter { } } + private async detectAudioSampleRate( + ffmpeg: FFmpeg, + ): Promise { + const args = [ + "-v", + "quiet", + "-select_streams", + "a:0", + "-show_entries", + "stream=sample_rate", + "-of", + "default=noprint_wrappers=1:nokey=1", + "input", + ]; + + try { + let sampleRate: number | null = null; + + const sampleRateListener = (event: { message: string }) => { + if (sampleRate !== null) return; + const n = parseInt(event.message.trim(), 10); + if (!n) return; + sampleRate = n; + log( + ["converters", this.name], + `Detected stream audio sample rate: ${sampleRate} Hz`, + ); + }; + + ffmpeg.on("log", sampleRateListener); + + try { + await ffmpeg.ffprobe.call(ffmpeg, args); + return sampleRate; + } finally { + ffmpeg.off("log", sampleRateListener); + } + } catch { + return null; + } + } + private async buildConversionCommand( ffmpeg: FFmpeg, input: VertFile, @@ -203,23 +245,74 @@ export class FFmpegConverter extends Converter { const inputFormat = input.from.slice(1); const outputFormat = to.slice(1); - const lossless = ["flac", "alac", "wav"]; + const lossless = ["flac", "alac", "wav", "dsd", "dsf", "dff"]; const userSetting = Settings.instance.settings.ffmpegQuality; - log(["converters", this.name], `using user setting for audio bitrate: ${userSetting}`); + const userSampleRate = Settings.instance.settings.ffmpegSampleRate; + const customSampleRate = + Settings.instance.settings.ffmpegCustomSampleRate; let audioBitrateArgs: string[] = []; + let sampleRateArgs: string[] = []; + const isLosslessToLossy = + lossless.includes(inputFormat) && !lossless.includes(outputFormat); if (userSetting !== "auto") { // user's setting audioBitrateArgs = ["-b:a", `${userSetting}k`]; + log( + ["converters", this.name], + `using user setting for audio bitrate: ${userSetting}`, + ); } else { // detect bitrate of original file and use - if (lossless.includes(inputFormat) && !lossless.includes(outputFormat)) { - audioBitrateArgs = ["-b:a", "320k"]; - log(["converters", this.name], `using default audio bitrate: 320k`); + if (isLosslessToLossy) { + // use safe default + audioBitrateArgs = ["-b:a", "128k"]; + log( + ["converters", this.name], + `converting from lossless to lossy, using default audio bitrate: 128k`, + ); } else { const inputBitrate = await this.detectAudioBitrate(ffmpeg); - audioBitrateArgs = inputBitrate ? ["-b:a", `${inputBitrate}k`] : []; - log(["converters", this.name], `using detected audio bitrate: ${inputBitrate}k`); + audioBitrateArgs = inputBitrate + ? ["-b:a", `${inputBitrate}k`] + : []; + log( + ["converters", this.name], + `using detected audio bitrate: ${inputBitrate}k`, + ); + } + } + + // sample rate setting + if (userSampleRate !== "auto") { + if (userSampleRate === "custom") { + sampleRateArgs = ["-ar", customSampleRate.toString()]; + } else { + sampleRateArgs = ["-ar", userSampleRate]; + } + log( + ["converters", this.name], + `using user setting for sample rate: ${userSampleRate}`, + ); + } else { + // detect sample rate of original file and use + if (isLosslessToLossy) { + // use safe default + sampleRateArgs = ["-ar", "44100"]; + log( + ["converters", this.name], + `converting from lossless to lossy, using default sample rate: 44100Hz`, + ); + } else { + const inputSampleRate = + await this.detectAudioSampleRate(ffmpeg); + sampleRateArgs = inputSampleRate + ? ["-ar", inputSampleRate.toString()] + : []; + log( + ["converters", this.name], + `using detected audio sample rate: ${inputSampleRate}Hz`, + ); } } @@ -235,6 +328,7 @@ export class FFmpegConverter extends Converter { "-map", "0:a:0", ...audioBitrateArgs, + ...sampleRateArgs, "output" + to, ]; } @@ -270,6 +364,7 @@ export class FFmpegConverter extends Converter { "1", ...codecArgs, ...audioBitrateArgs, + ...sampleRateArgs, "output" + to, ]; } else { @@ -288,6 +383,7 @@ export class FFmpegConverter extends Converter { "1", ...codecArgs, ...audioBitrateArgs, + ...sampleRateArgs, "output" + to, ]; } @@ -305,6 +401,7 @@ export class FFmpegConverter extends Converter { "-c:a", audioCodec, ...audioBitrateArgs, + ...sampleRateArgs, "output" + to, ]; } @@ -476,13 +573,26 @@ const getCodecs = (ext: string): { video: string; audio: string } => { }; export const CONVERSION_BITRATES = [ - "auto", - 320, - 256, - 192, - 128, - 96, - 64, - 32, + "auto", + 320, + 256, + 192, + 128, + 96, + 64, + 32, ] as const; -export type ConversionBitrate = (typeof CONVERSION_BITRATES)[number]; \ No newline at end of file +export type ConversionBitrate = (typeof CONVERSION_BITRATES)[number]; + +export const SAMPLE_RATES = [ + "auto", + "custom", + "48000", + "44100", + "32000", + "22050", + "16000", + "11025", + "8000", +] as const; +export type SampleRate = (typeof SAMPLE_RATES)[number]; diff --git a/src/lib/sections/settings/Conversion.svelte b/src/lib/sections/settings/Conversion.svelte index 4c387e7..f9a4620 100644 --- a/src/lib/sections/settings/Conversion.svelte +++ b/src/lib/sections/settings/Conversion.svelte @@ -6,6 +6,8 @@ import { CONVERSION_BITRATES, type ConversionBitrate, + SAMPLE_RATES, + type SampleRate, } from "$lib/converters/ffmpeg.svelte"; import { m } from "$lib/paraglide/messages"; import Dropdown from "$lib/components/functional/Dropdown.svelte"; @@ -81,6 +83,34 @@ />
+
+
+

+ {m["settings.conversion.rate"]()} +

+ r.toString())} + selected={settings.ffmpegSampleRate.toString()} + onselect={(option: string) => { + settings.ffmpegSampleRate = + option as SampleRate; + }} + settingsStyle + /> +
+
+

  

+ +
+
video) + ffmpegSampleRate: string; // audio (or audio <-> video) + ffmpegCustomSampleRate: number; // audio (or audio <-> video) - only used when ffmpegSampleRate is "custom" } export class Settings { @@ -26,6 +28,8 @@ export class Settings { vertdSpeed: "slow", magickQuality: 100, ffmpegQuality: "auto", + ffmpegSampleRate: "auto", + ffmpegCustomSampleRate: 44100, }); public save() {