improve audio support

This commit is contained in:
Brian Beck 2026-03-04 12:15:24 -08:00
parent d1acb6a5ce
commit cb28b66dad
5587 changed files with 4538 additions and 2846 deletions

113
scripts/convert-wav.ts Normal file
View file

@ -0,0 +1,113 @@
import fs from "node:fs/promises";
import { execFileSync } from "node:child_process";
import { parseArgs } from "node:util";
const FFMPEG_PATH = process.env.FFMPEG_PATH || "ffmpeg";
/**
* Find all .wav files in `docs/base` and convert them to Opus OGG.
* Converted .ogg files are placed alongside the originals (analogous to
* how convert-dts.ts places .glb files alongside .dts files). The manifest
* ignores .ogg files; audioToUrl() swaps the extension at resolution time.
*/
async function run({
onlyNew,
bitrate,
concurrency,
}: {
onlyNew: boolean;
bitrate: string;
concurrency: number;
}) {
const inputFiles: string[] = [];
for await (const wavFile of fs.glob("docs/base/**/*.{wav,WAV}")) {
const oggFile = wavFile.replace(/\.wav$/i, ".ogg");
if (onlyNew) {
try {
await fs.stat(oggFile);
continue; // .ogg already exists, skip
} catch {}
}
inputFiles.push(wavFile);
}
if (inputFiles.length === 0) {
console.log("No .wav files to convert.");
return;
}
console.log(
`Converting ${inputFiles.length} .wav file(s) to Opus OGG (${bitrate})…`,
);
let completed = 0;
let failed = 0;
async function convert(wavFile: string) {
const oggFile = wavFile.replace(/\.wav$/i, ".ogg");
try {
execFileSync(
FFMPEG_PATH,
[
"-y",
"-i",
wavFile,
"-c:a",
"libopus",
"-b:a",
bitrate,
"-vn",
oggFile,
],
{ stdio: "pipe" },
);
completed++;
} catch (err: any) {
failed++;
const stderr = err.stderr?.toString().trim();
console.error(` FAILED: ${wavFile}`);
if (stderr) {
// Show just the last line of ffmpeg output (the actual error).
const lines = stderr.split("\n");
console.error(` ${lines[lines.length - 1]}`);
}
}
}
// Process in batches for parallelism.
for (let i = 0; i < inputFiles.length; i += concurrency) {
const batch = inputFiles.slice(i, i + concurrency);
await Promise.all(batch.map(convert));
const total = Math.min(i + concurrency, inputFiles.length);
process.stdout.write(`\r ${total}/${inputFiles.length}`);
}
process.stdout.write("\n");
console.log(`Done: ${completed} converted, ${failed} failed.`);
}
const { values } = parseArgs({
options: {
new: {
type: "boolean",
default: false,
short: "n",
},
bitrate: {
type: "string",
default: "64k",
short: "b",
},
concurrency: {
type: "string",
default: "8",
short: "j",
},
},
});
run({
onlyNew: values.new!,
bitrate: values.bitrate!,
concurrency: parseInt(values.concurrency!, 10) || 8,
});

View file

@ -15,6 +15,7 @@ const baseDir = process.env.BASE_DIR || "docs/base";
const ignoreList = ignore().add(`
.DS_Store
*.glb
*.ogg
`);
type SourceTuple =

31
scripts/sum-filesize.ts Normal file
View file

@ -0,0 +1,31 @@
import fs from "node:fs/promises";
import { parseArgs } from "node:util";
function formatSize(bytes: number): string {
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(2)} KB`;
return `${bytes} B`;
}
const { positionals } = parseArgs({ allowPositionals: true });
if (positionals.length === 0) {
console.error("Usage: tsx scripts/sum-filesize.ts <glob> [glob...]");
process.exit(1);
}
let total = 0;
let count = 0;
for (const pattern of positionals) {
for await (const file of fs.glob(pattern)) {
const stat = await fs.stat(file);
if (stat.isFile()) {
total += stat.size;
count++;
}
}
}
console.log(`${count} files, ${formatSize(total)}`);