mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user