fix(audio): expose sample rate setting in Convert Audio (#561)

The Convert Audio tool promised configurable bitrate, sample rate, and channel count, but only format and bitrate were exposed. Adds an optional sampleRate setting (8000 to 96000 Hz, omitted = preserve source) wired through the Zod schema, the FFmpeg -ar flag, the standalone settings panel, and the pipeline builder controls.

Impossible combinations fail loudly instead of degrading silently: MP3 + 96000 Hz is rejected (libmp3lame caps at 48 kHz), and MP3 bitrates above the encoder ceiling at low rates (64 kbps at 8 kHz, 160 kbps at 16/22.05 kHz) are rejected rather than clamped. The UI offers only legal combinations and sanitizes stored pipeline settings on load.

Docs updated in English plus all 20 localized pages with refreshed i18n_source_hash stamps; two new UI strings added to all 21 locales.

Fixes #558
This commit is contained in:
SnapOtter
2026-07-18 10:14:50 +08:00
committed by GitHub
parent 6ecc598fc4
commit d4eaa655b2
47 changed files with 541 additions and 133 deletions
+63 -19
View File
@@ -3,10 +3,53 @@ import { z } from "zod";
import { runMediaTool } from "../../lib/media-tool.js";
import { createToolRoute } from "../tool-factory.js";
const settingsSchema = z.object({
format: z.enum(["mp3", "wav", "ogg", "flac", "m4a"]).default("mp3"),
bitrateKbps: z.number().int().min(32).max(320).default(192),
});
// libmp3lame clamps the bitrate silently above these: MPEG-2.5 (8 kHz) tops out
// at 64 kbps, MPEG-2 (16/22.05 kHz) at 160 kbps. Reject instead of degrading.
const MP3_BITRATE_CAPS: Record<number, number> = { 8000: 64, 16000: 160, 22050: 160 };
const settingsSchema = z
.object({
format: z.enum(["mp3", "wav", "ogg", "flac", "m4a"]).default("mp3"),
bitrateKbps: z.number().int().min(32).max(320).default(192),
// Omitted = preserve the source sample rate (no -ar flag).
sampleRate: z
.union(
[
z.literal(8000),
z.literal(16000),
z.literal(22050),
z.literal(32000),
z.literal(44100),
z.literal(48000),
z.literal(96000),
],
{
errorMap: () => ({
message: "must be one of 8000, 16000, 22050, 32000, 44100, 48000, 96000",
}),
},
)
.optional(),
})
.superRefine((val, ctx) => {
if (val.format !== "mp3" || !val.sampleRate) return;
if (val.sampleRate === 96000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["sampleRate"],
message: "MP3 output supports sample rates up to 48000 Hz",
});
return;
}
const cap = MP3_BITRATE_CAPS[val.sampleRate];
if (cap && val.bitrateKbps > cap) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["bitrateKbps"],
message: `MP3 at ${val.sampleRate} Hz supports at most ${cap} kbps`,
});
}
});
const CONTENT_TYPES: Record<string, string> = {
mp3: "audio/mpeg",
@@ -29,31 +72,31 @@ export function registerConvertAudio(app: FastifyInstance) {
const outName = `${base}.${settings.format}`;
const { outPath } = await runMediaTool(ctx, outName, (inPath, out) => {
const rate = settings.sampleRate ? ["-ar", String(settings.sampleRate)] : [];
switch (settings.format) {
case "mp3":
return [
"-i",
inPath,
"-vn",
"-c:a",
"libmp3lame",
"-b:a",
`${settings.bitrateKbps}k`,
out,
];
case "wav":
return ["-i", inPath, "-vn", "-c:a", "pcm_s16le", out];
return ["-i", inPath, "-vn", "-c:a", "pcm_s16le", ...rate, out];
case "ogg": {
// libvorbis ABR (-b:a) fails with "encoder setup failed" when the bitrate is
// too high for the source sample rate (e.g. 8 kHz). Use quality VBR (-q:a),
// which adapts to the rate. Map bitrate -> quality (~bitrate/32: 192k -> q6).
const quality = (settings.bitrateKbps / 32).toFixed(1);
return ["-i", inPath, "-vn", "-c:a", "libvorbis", "-q:a", quality, out];
return ["-i", inPath, "-vn", "-c:a", "libvorbis", "-q:a", quality, ...rate, out];
}
case "flac":
return ["-i", inPath, "-vn", "-c:a", "flac", out];
return ["-i", inPath, "-vn", "-c:a", "flac", ...rate, out];
case "m4a":
return ["-i", inPath, "-vn", "-c:a", "aac", "-b:a", `${settings.bitrateKbps}k`, out];
return [
"-i",
inPath,
"-vn",
"-c:a",
"aac",
"-b:a",
`${settings.bitrateKbps}k`,
...rate,
out,
];
default:
return [
"-i",
@@ -63,6 +106,7 @@ export function registerConvertAudio(app: FastifyInstance) {
"libmp3lame",
"-b:a",
`${settings.bitrateKbps}k`,
...rate,
out,
];
}