node-lame
Version:
LAME MP3 encoder for Node.js
1,806 lines (1,799 loc) • 52.6 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Lame: () => Lame,
LameOptions: () => LameOptions,
LameStream: () => LameStream,
resolveBundledLameBinary: () => resolveBundledLameBinary,
resolveBundledLibraryDirectory: () => resolveBundledLibraryDirectory,
resolveLameBinary: () => resolveLameBinary
});
module.exports = __toCommonJS(index_exports);
// src/core/lame.ts
var import_node_crypto = require("crypto");
var import_node_events = require("events");
var import_node_fs2 = require("fs");
var import_promises = require("fs/promises");
var import_node_path3 = require("path");
var import_node_os = require("os");
// src/internal/binary/resolve-binary.ts
var import_node_fs = require("fs");
var import_node_path = require("path");
var import_node_url = require("url");
var import_meta = {};
function deriveModuleUrl(meta, filename) {
if (meta && typeof meta.url === "string") {
return meta.url;
}
if (typeof filename === "string") {
return (0, import_node_url.pathToFileURL)(filename).href;
}
return void 0;
}
var moduleUrl = (() => {
let meta = void 0;
try {
meta = import_meta;
} catch {
}
const resolvedFilename = typeof __filename === "string" ? __filename : void 0;
return deriveModuleUrl(meta, resolvedFilename);
})();
function findPackageRootFrom(startDir) {
let current = startDir;
while (true) {
const packageJsonPath = (0, import_node_path.join)(current, "package.json");
if ((0, import_node_fs.existsSync)(packageJsonPath)) {
return current;
}
const parent = (0, import_node_path.dirname)(current);
if (parent === current) {
return null;
}
current = parent;
}
}
function resolvePackageRoot(moduleHref, dirname3) {
const normalizedDir = dirname3 ?? (moduleHref != null ? (0, import_node_url.fileURLToPath)(new URL(".", moduleHref)) : void 0);
if (normalizedDir) {
const detectedRoot = findPackageRootFrom(normalizedDir);
if (detectedRoot) {
return detectedRoot;
}
}
if (dirname3) {
return (0, import_node_path.join)(dirname3, "..", "..", "..");
}
if (moduleHref != null) {
return (0, import_node_url.fileURLToPath)(new URL("../../..", moduleHref));
}
return process.cwd();
}
var PACKAGE_ROOT = resolvePackageRoot(
moduleUrl,
typeof __dirname === "string" ? __dirname : void 0
);
var CUSTOM_BINARY_ENV = "LAME_BINARY";
var LIBRARY_DIRECTORY_NAME = "lib";
function getPlatformExecutableSuffix(platform) {
return platform === "win32" ? ".exe" : "";
}
var PLATFORM_EXECUTABLE_SUFFIX = getPlatformExecutableSuffix(
process.platform
);
function resolveBundledLameBinary() {
const explicitBinary = process.env[CUSTOM_BINARY_ENV];
if (explicitBinary && (0, import_node_fs.existsSync)(explicitBinary)) {
return explicitBinary;
}
const candidate = (0, import_node_path.join)(
PACKAGE_ROOT,
"vendor",
"lame",
`${process.platform}-${process.arch}`,
`lame${PLATFORM_EXECUTABLE_SUFFIX}`
);
if ((0, import_node_fs.existsSync)(candidate)) {
return candidate;
}
return null;
}
function resolveBundledLibraryDirectory() {
const candidate = (0, import_node_path.join)(
PACKAGE_ROOT,
"vendor",
"lame",
`${process.platform}-${process.arch}`,
LIBRARY_DIRECTORY_NAME
);
if ((0, import_node_fs.existsSync)(candidate)) {
return candidate;
}
return null;
}
function resolveLameBinary() {
const resolved = resolveBundledLameBinary();
return resolved ?? "lame";
}
// src/core/lame-options.ts
var LameOptions = class {
args = [];
useDefaultDisptime = true;
/**
* Validate all options and build argument array for binary
* @param {Object} options
*/
constructor(options) {
if (options["output"] == void 0) {
throw new Error("lame: Invalid option: 'output' is required");
}
const entries = Object.entries(options);
for (const [key, value] of entries) {
let arg;
switch (key) {
case "output":
continue;
case "raw":
arg = this.raw(value);
break;
case "swap-bytes":
arg = this.swapBytes(value);
break;
case "swap-channel":
arg = this.swapChannel(value);
break;
case "gain":
arg = this.gain(value);
break;
case "sfreq":
arg = this.sfreq(value);
break;
case "bitwidth":
arg = this.bitwidth(value);
break;
case "signed":
arg = this.signed(value);
break;
case "unsigned":
arg = this.unsigned(value);
break;
case "little-endian":
arg = this.littleEndian(value);
break;
case "big-endian":
arg = this.bigEndian(value);
break;
case "mp1Input":
arg = this.mp1Input(value);
break;
case "mp2Input":
arg = this.mp2Input(value);
break;
case "mp3Input":
arg = this.mp3Input(value);
break;
case "mode":
arg = this.mode(value);
break;
case "to-mono":
arg = this.toMono(value);
break;
case "channel-different-block-sizes":
arg = this.channelDifferentBlockSize(value);
break;
case "freeformat":
arg = this.freeformat(value);
break;
case "disable-info-tag":
arg = this.disableInfoTag(value);
break;
case "nogap":
arg = this.nogap(value);
break;
case "nogapout":
arg = this.nogapout(value);
break;
case "nogaptags":
arg = this.nogaptags(value);
break;
case "out-dir":
arg = this.outDir(value);
break;
case "comp":
arg = this.comp(value);
break;
case "scale":
arg = this.scale(value);
break;
case "scale-l":
arg = this.scaleL(value);
break;
case "scale-r":
arg = this.scaleR(value);
break;
case "replaygain-fast":
arg = this.replaygainFast(value);
break;
case "replaygain-accurate":
arg = this.replaygainAccurate(value);
break;
case "no-replaygain":
arg = this.noreplaygain(value);
break;
case "clip-detect":
arg = this.clipDetect(value);
break;
case "preset":
arg = this.preset(value);
break;
case "noasm":
arg = this.noasm(value);
break;
case "quality":
arg = this.quality(value);
break;
case "quality-high":
arg = this.qualityHigh(value);
break;
case "fast-encoding":
arg = this.fastEncoding(value);
break;
case "bitrate":
arg = this.bitrate(value);
break;
case "max-bitrate":
arg = this.maxBitrate(value);
break;
case "force-bitrate":
arg = this.forceBitrate(value);
break;
case "cbr":
arg = this.cbr(value);
break;
case "abr":
arg = this.abr(value);
break;
case "vbr":
arg = this.vbr(value);
break;
case "vbr-quality":
arg = this.vbrQuality(value);
break;
case "vbr-old":
arg = this.vbrOld(value);
break;
case "vbr-new":
arg = this.vbrNew(value);
break;
case "ignore-noise-in-sfb21":
arg = this.ignoreNoiseInSfb21(value);
break;
case "emp":
arg = this.emp(value);
break;
case "mark-as-copyrighted":
arg = this.markAsCopyrighted(value);
break;
case "mark-as-copy":
arg = this.markAsCopy(value);
break;
case "crc-error-protection":
arg = this.crcErrorProtection(value);
break;
case "nores":
arg = this.nores(value);
break;
case "strictly-enforce-ISO":
arg = this.strictlyEnforceIso(value);
break;
case "lowpass":
arg = this.lowpass(value);
break;
case "lowpass-width":
arg = this.lowpassWidth(value);
break;
case "highpass":
arg = this.highpass(value);
break;
case "highpass-width":
arg = this.highpassWidth(value);
break;
case "resample":
arg = this.resample(value);
break;
case "decode-mp3delay":
arg = this.decodeMp3Delay(value);
break;
case "priority":
arg = this.priority(value);
break;
case "disptime":
arg = this.disptime(value);
break;
case "silent":
arg = this.silent(value);
break;
case "quiet":
arg = this.quiet(value);
break;
case "verbose":
arg = this.verbose(value);
break;
case "help":
arg = this.help(value);
break;
case "usage":
arg = this.usage(value);
break;
case "longhelp":
arg = this.longHelp(value);
break;
case "version":
arg = this.version(value);
break;
case "license":
arg = this.license(value);
break;
case "no-histogram":
arg = this.noHistogram(value);
break;
case "meta":
arg = this.meta(value);
break;
default:
throw new Error("Unknown parameter " + key);
}
if (Array.isArray(arg)) {
this.args.push(...arg.map((item) => String(item)));
}
}
}
/**
* Get all arguments for binary
*/
getArguments() {
return this.args;
}
shouldUseDefaultDisptime() {
return this.useDefaultDisptime;
}
raw(value) {
if (value == true) {
return [`-r`];
} else {
return void 0;
}
}
swapBytes(value) {
if (value == true) {
return [`-x`];
} else {
return void 0;
}
}
swapChannel(value) {
if (value == true) {
return [`--swap-channel`];
}
return void 0;
}
gain(value) {
if (value === void 0) {
return void 0;
}
if (typeof value === "number" && value >= -20 && value <= 12) {
return [`--gain`, String(value)];
}
throw new Error(
"lame: Invalid option: 'gain' must be a number between -20 and 12."
);
}
sfreq(value) {
if (value == 8 || value == 11.025 || value == 12 || value == 16 || value == 22.05 || value == 24 || value == 32 || value == 44.1 || value == 48) {
return [`-s`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'sfreq' is not in range of 8, 11.025, 12, 16, 22.05, 24, 32, 44.1 or 48."
);
}
}
bitwidth(value) {
if (value == 8 || value == 16 || value == 24 || value == 32) {
return [`--bitwidth`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'sfreq' is not in range of 8, 16, 24 or 32."
);
}
}
signed(value) {
if (value == true) {
return [`--signed`];
} else {
return void 0;
}
}
unsigned(value) {
if (value == true) {
return [`--unsigned`];
} else {
return void 0;
}
}
littleEndian(value) {
if (value == true) {
return [`--little-endian`];
} else {
return void 0;
}
}
bigEndian(value) {
if (value == true) {
return [`--big-endian`];
} else {
return void 0;
}
}
mp1Input(value) {
if (value == true) {
return [`--mp1input`];
} else {
return void 0;
}
}
mp2Input(value) {
if (value == true) {
return [`--mp2input`];
} else {
return void 0;
}
}
mp3Input(value) {
if (value == true) {
return [`--mp3input`];
} else {
return void 0;
}
}
mode(value) {
if (value == "s" || value == "j" || value == "f" || value == "d" || value == "m" || value == "l" || value == "r" || value == "a") {
return [`-m`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'mode' is not in range of 's', 'j', 'f', 'd', 'm', 'l', 'r' or 'a'."
);
}
}
toMono(value) {
if (value == true) {
return [`-a`];
} else {
return void 0;
}
}
channelDifferentBlockSize(value) {
if (value == true) {
return [`-d`];
} else {
return void 0;
}
}
freeformat(value) {
if (value == null || value === false) {
return void 0;
}
if (typeof value === "string") {
if (value === "FreeAmp" || value === "in_mpg123" || value === "l3dec" || value === "LAME" || value === "MAD") {
return [`--freeformat`];
}
throw new Error(
"lame: Invalid option: 'freeformat' string value must be one of 'FreeAmp', 'in_mpg123', 'l3dec', 'LAME', 'MAD'."
);
}
if (value == true) {
return [`--freeformat`];
}
throw new Error("lame: Invalid option: 'freeformat' must be boolean.");
}
disableInfoTag(value) {
if (value == true) {
return [`-t`];
} else {
return void 0;
}
}
nogap(value) {
if (value == null) {
return void 0;
}
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.trim() !== "")) {
return [`--nogap`, ...value];
}
throw new Error(
"lame: Invalid option: 'nogap' must be a non-empty array of file paths."
);
}
nogapout(value) {
if (value == null) {
return void 0;
}
if (typeof value === "string" && value.trim() !== "") {
return [`--nogapout`, value];
}
throw new Error(
"lame: Invalid option: 'nogapout' must be a non-empty string path."
);
}
nogaptags(value) {
if (value === void 0 || value === false) {
return void 0;
}
if (value == true) {
return [`--nogaptags`];
}
throw new Error("lame: Invalid option: 'nogaptags' must be boolean.");
}
outDir(value) {
if (value == null) {
return void 0;
}
if (typeof value === "string" && value.trim() !== "") {
return [`--out-dir`, value];
}
throw new Error(
"lame: Invalid option: 'out-dir' must be a non-empty string path."
);
}
comp(value) {
return [`--comp`, String(value)];
}
scale(value) {
return [`--scale`, String(value)];
}
scaleL(value) {
return [`--scale-l`, String(value)];
}
scaleR(value) {
return [`--scale-r`, String(value)];
}
replaygainFast(value) {
if (value == true) {
return [`--replaygain-fast`];
} else {
return void 0;
}
}
replaygainAccurate(value) {
if (value == true) {
return [`--replaygain-accurate`];
} else {
return void 0;
}
}
noreplaygain(value) {
if (value == true) {
return [`--noreplaygain`];
} else {
return void 0;
}
}
clipDetect(value) {
if (value == true) {
return [`--clipdetect`];
} else {
return void 0;
}
}
preset(value) {
if (value == null) {
return void 0;
}
if (typeof value === "number" && value >= 8 && value <= 640) {
return [`--preset`, String(value)];
}
if (typeof value !== "string") {
return this.invalidPreset();
}
const trimmed = value.trim();
if (trimmed === "") {
throw new Error("lame: Invalid option: 'preset' cannot be empty.");
}
const tokens = trimmed.split(/\s+/);
const [first, second] = tokens;
const singleValuePresets = [
"medium",
"standard",
"extreme",
"insane",
"phone",
"phon+",
"lw",
"mw-eu",
"mw-us",
"voice",
"fm",
"radio",
"hifi",
"cd",
"studio"
];
if (tokens.length === 1) {
if (singleValuePresets.includes(first) || /^[0-9]+$/.test(first)) {
return [`--preset`, first];
}
return this.invalidPreset();
}
if (tokens.length === 2 && first === "fast" && (second === "medium" || second === "standard" || second === "extreme" || /^[0-9]+$/.test(second))) {
return [`--preset`, "fast", second];
}
if (tokens.length === 2 && first === "cbr" && /^[0-9]+$/.test(second)) {
return [`--preset`, "cbr", second];
}
return this.invalidPreset();
}
invalidPreset() {
throw new Error(
"lame: Invalid option: 'preset' must be a supported preset keyword, numeric bitrate, or preset tuple like 'fast <value>' or 'cbr <bitrate>'."
);
}
noasm(value) {
if (value == "mmx" || value == "3dnow" || value == "sse") {
return [`--noasm`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'noasm' is not in range of 'mmx', '3dnow' or 'sse'."
);
}
}
quality(value) {
if (typeof value === "number" && value >= 0 && value <= 9) {
return [`-q`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'quality' is not in range of 0 to 9."
);
}
}
qualityHigh(value) {
if (value == true) {
return [`-h`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'quality-high' must be boolean."
);
}
fastEncoding(value) {
if (value == true) {
return [`-f`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'fast-encoding' must be boolean."
);
}
bitrate(value) {
if (value == 8 || value == 16 || value == 24 || value == 32 || value == 40 || value == 48 || value == 56 || value == 64 || value == 80 || value == 96 || value == 112 || value == 128 || value == 144 || value == 160 || value == 192 || value == 224 || value == 256 || value == 320) {
return [`-b`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'bitrate' is not in range of 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256 or 320."
);
}
}
maxBitrate(value) {
if (value == 8 || value == 16 || value == 24 || value == 32 || value == 40 || value == 48 || value == 56 || value == 64 || value == 80 || value == 96 || value == 112 || value == 128 || value == 144 || value == 160 || value == 192 || value == 224 || value == 256 || value == 320) {
return [`-B`, String(value)];
} else if (value === void 0) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'max-bitrate' is not in range of 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256 or 320."
);
}
forceBitrate(value) {
if (value == true) {
return [`-F`];
} else {
return void 0;
}
}
cbr(value) {
if (value == true) {
return [`--cbr`];
} else {
return void 0;
}
}
abr(value) {
if (typeof value === "number" && value >= 8 && value <= 310) {
return [`--abr`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'abr' is not in range of 8 to 310."
);
}
}
vbr(value) {
if (value == true) {
return [`-v`];
} else {
return void 0;
}
}
vbrQuality(value) {
if (typeof value === "number" && value >= 0 && value <= 9) {
return [`-V`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'vbrQuality' is not in range of 0 to 9."
);
}
}
vbrOld(value) {
if (value == true) {
return [`--vbr-old`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'vbr-old' must be boolean."
);
}
vbrNew(value) {
if (value == true) {
return [`--vbr-new`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'vbr-new' must be boolean."
);
}
ignoreNoiseInSfb21(value) {
if (value == true) {
return [`-Y`];
} else {
return void 0;
}
}
emp(value) {
if (value == "n" || value == 5 || value == "c") {
return [`-e`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'emp' is not in range of 'n', 5 or 'c'."
);
}
}
markAsCopyrighted(value) {
if (value == true) {
return [`-c`];
} else {
return void 0;
}
}
markAsCopy(value) {
if (value == true) {
return [`-o`];
} else {
return void 0;
}
}
crcErrorProtection(value) {
if (value == true) {
return [`-p`];
} else {
return void 0;
}
}
nores(value) {
if (value == true) {
return [`--nores`];
} else {
return void 0;
}
}
strictlyEnforceIso(value) {
if (value == true) {
return [`--strictly-enforce-ISO`];
} else {
return void 0;
}
}
lowpass(value) {
return [`--lowpass`, String(value)];
}
lowpassWidth(value) {
return [`--lowpass-width`, String(value)];
}
highpass(value) {
return [`--highpass`, String(value)];
}
highpassWidth(value) {
return [`--highpass-width`, String(value)];
}
resample(value) {
if (value == 8 || value == 11.025 || value == 12 || value == 16 || value == 22.05 || value == 24 || value == 32 || value == 44.1 || value == 48) {
return [`--resample`, String(value)];
} else {
throw new Error(
"lame: Invalid option: 'resample' is not in range of 8, 11.025, 12, 16, 22.05, 24, 32, 44.1 or 48."
);
}
}
decodeMp3Delay(value) {
if (value == null) {
return void 0;
}
if (typeof value === "number" && Number.isFinite(value)) {
return [`--decode-mp3delay`, String(value)];
}
throw new Error(
"lame: Invalid option: 'decode-mp3delay' must be a finite number."
);
}
priority(value) {
if (value == null) {
return void 0;
}
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4) {
return [`--priority`, String(value)];
}
throw new Error(
"lame: Invalid option: 'priority' must be an integer between 0 and 4."
);
}
disptime(value) {
if (value === false) {
this.useDefaultDisptime = false;
return void 0;
}
if (value == null) {
return void 0;
}
if (typeof value === "number" && value > 0) {
this.useDefaultDisptime = false;
return [`--disptime`, String(value)];
}
throw new Error(
"lame: Invalid option: 'disptime' must be a positive number of seconds or false to disable progress output."
);
}
silent(value) {
if (value === void 0 || value === false) {
return void 0;
}
if (value == true) {
return [`--silent`];
}
throw new Error(
"lame: Invalid option: 'silent' must be boolean."
);
}
quiet(value) {
if (value === void 0 || value === false) {
return void 0;
}
if (value == true) {
return [`--quiet`];
}
throw new Error(
"lame: Invalid option: 'quiet' must be boolean."
);
}
verbose(value) {
if (value === void 0 || value === false) {
return void 0;
}
if (value == true) {
return [`--verbose`];
}
throw new Error(
"lame: Invalid option: 'verbose' must be boolean."
);
}
help(value) {
return this.helpLike("--help", value);
}
usage(value) {
return this.helpLike("--usage", value);
}
longHelp(value) {
if (value === void 0 || value === false) {
return void 0;
}
if (value == true) {
return [`--longhelp`];
}
throw new Error(
"lame: Invalid option: 'longhelp' must be boolean."
);
}
version(value) {
if (value == true) {
return [`--version`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'version' must be boolean."
);
}
license(value) {
if (value == true) {
return [`--license`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'license' must be boolean."
);
}
noHistogram(value) {
if (value == true) {
return [`--nohist`];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
"lame: Invalid option: 'no-histogram' must be boolean."
);
}
helpLike(flag, value) {
if (value == true) {
return [flag];
}
if (typeof value === "string" && (value === "id3" || value === "dev")) {
return [flag, value];
}
if (value === void 0 || value === false) {
return void 0;
}
throw new Error(
`lame: Invalid option: '${flag.slice(2)}' must be boolean or one of 'id3', 'dev'.`
);
}
meta(metaObj) {
if (metaObj == null || typeof metaObj !== "object") {
throw new Error("lame: Invalid option: 'meta' must be an object.");
}
const metaRecord = metaObj;
const fieldMap = {
title: "--tt",
artist: "--ta",
album: "--tl",
year: "--ty",
comment: "--tc",
track: "--tn",
genre: "--tg",
artwork: "--ti",
"genre-list": "--genre-list",
"pad-id3v2-size": "--pad-id3v2-size"
};
for (const key of Object.keys(metaRecord)) {
const value = metaRecord[key];
if (key in fieldMap) {
this.args.push(fieldMap[key]);
this.args.push(`${value}`);
} else if (key == "add-id3v2" || key == "id3v1-only" || key == "id3v2-only" || key == "id3v2-latin1" || key == "id3v2-utf16" || key == "space-id3v1" || key == "pad-id3v2" || key == "ignore-tag-errors") {
this.args.push(`--${key}`);
} else if (key == "custom") {
this.appendCustomFrames(value);
} else {
throw new Error(
`lame: Invalid option: 'meta' unknown property '${key}'`
);
}
}
return void 0;
}
appendCustomFrames(value) {
if (value == null) {
return;
}
const pushFrame = (id, frameValue) => {
if (typeof id !== "string" || id.trim() === "") {
throw new Error(
"lame: Invalid option: 'meta.custom' frame id must be a non-empty string."
);
}
this.args.push("--tv");
this.args.push(`${id}=${String(frameValue)}`);
};
if (Array.isArray(value)) {
for (const entry of value) {
if (typeof entry === "string") {
const [id, ...rest] = entry.split("=");
if (!id || rest.length === 0) {
throw new Error(
"lame: Invalid option: 'meta.custom' array entries must be 'id=value'."
);
}
pushFrame(id, rest.join("="));
} else if (Array.isArray(entry) && entry.length === 2) {
pushFrame(
String(entry[0]),
entry[1]
);
} else if (typeof entry === "object" && entry != null && "id" in entry && "value" in entry) {
const typedEntry = entry;
pushFrame(typedEntry.id, typedEntry.value);
} else {
throw new Error(
"lame: Invalid option: 'meta.custom' array entries must be strings, tuples, or objects with id/value."
);
}
}
return;
}
if (typeof value === "object") {
const record = value;
for (const [id, frameValue] of Object.entries(record)) {
pushFrame(id, frameValue);
}
return;
}
throw new Error(
"lame: Invalid option: 'meta.custom' must be an array or object."
);
}
};
// src/core/lame-process.ts
var import_node_child_process = require("child_process");
var import_node_path2 = require("path");
var LAME_TAG_MESSAGE = "Writing LAME Tag...done";
function createInitialStatus() {
return {
started: false,
finished: false,
progress: 0,
eta: void 0
};
}
function buildLameSpawnArgs(builder, kind, input, output) {
const args = builder.getArguments();
const normalizedArgs = [...args];
if (builder.shouldUseDefaultDisptime() && !normalizedArgs.includes("--disptime")) {
normalizedArgs.push("--disptime", "1");
}
if (kind === "decode") {
normalizedArgs.push("--decode");
}
return [input, output, ...normalizedArgs.map((value) => String(value))];
}
function markProcessFinished(status, emitter) {
if (status.finished) {
return;
}
status.finished = true;
status.progress = 100;
status.eta = "00:00";
emitter.emit("finish");
emitter.emit("progress", [status.progress, status.eta]);
}
function parseEncodeProgressLine(content) {
const progressMatch = content.match(/\(\s*((?:[0-9]{1,2})|100)%\)\|/);
if (!progressMatch) {
return null;
}
const etaMatch = content.match(/[0-9]{1,2}:[0-9][0-9] /);
const progress = Number(progressMatch[1]);
const eta = etaMatch ? etaMatch[0].trim() : void 0;
return { progress, eta };
}
function parseDecodeProgressLine(content) {
const progressMatch = content.match(/[0-9]{1,10}\/[0-9]{1,10}/);
if (!progressMatch) {
return null;
}
const [current, total] = progressMatch[0].split("/").map(Number);
if (!Number.isFinite(current) || !Number.isFinite(total) || total === 0) {
return NaN;
}
return Math.floor(current / total * 100);
}
function normalizeCliMessage(content) {
if (content.startsWith("lame: ") || content.startsWith("Warning: ") || content.includes("Error ")) {
return content.startsWith("lame: ") ? content : `lame: ${content}`;
}
return null;
}
function processProgressChunk(payload, options) {
const { kind, status, emitter, completeOnTag } = options;
const content = payload.toString();
const lines = content.split(/\r?\n/);
for (const rawLine of lines) {
const line = rawLine.trim();
if (line === "") {
continue;
}
if (completeOnTag && line.includes(LAME_TAG_MESSAGE)) {
markProcessFinished(status, emitter);
continue;
}
if (kind === "encode") {
const parsed = parseEncodeProgressLine(line);
if (parsed) {
if (parsed.progress !== void 0 && parsed.progress > status.progress) {
status.progress = parsed.progress;
}
if (parsed.eta) {
status.eta = parsed.eta;
}
emitter.emit("progress", [status.progress, status.eta]);
continue;
}
} else {
const parsed = parseDecodeProgressLine(line);
if (parsed !== null) {
if (!Number.isNaN(parsed)) {
status.progress = parsed;
}
emitter.emit("progress", [status.progress, status.eta]);
continue;
}
}
const normalized = normalizeCliMessage(line);
if (normalized) {
return { error: new Error(normalized) };
}
}
return {};
}
function getExitError(code, executable) {
if (code === 0) {
return null;
}
if (code === 255) {
return new Error(
"Unexpected termination of the process, possibly directly after the start. Please check if the input and/or output does not exist."
);
}
if (code === 127) {
return new Error(
`lame: Failed to execute '${executable}'. Exit code 127 usually indicates missing shared libraries or an unreadable binary. Run scripts/diagnose-lame.mjs for details.`
);
}
if (code !== null) {
return new Error(`lame: Process exited with code ${code}`);
}
return new Error("lame: Process exited unexpectedly");
}
function applyBundledLibraryPath(env, libraryDir) {
if (!libraryDir) {
return env;
}
let variable = null;
if (process.platform === "linux") {
variable = "LD_LIBRARY_PATH";
} else if (process.platform === "darwin") {
variable = "DYLD_LIBRARY_PATH";
} else if (process.platform === "win32") {
variable = "PATH";
}
if (!variable) {
return env;
}
const currentValue = env[variable];
return {
...env,
[variable]: currentValue ? `${libraryDir}${import_node_path2.delimiter}${currentValue}` : libraryDir
};
}
function spawnLameProcess(options) {
const {
binaryPath,
spawnArgs,
kind,
status,
emitter,
onError,
progressSources,
completeOnTag,
onStdoutData,
onStdoutEnd,
onStdoutError,
onStderrData,
onStderrError,
onStdinError,
onSuccess
} = options;
status.started = true;
status.finished = false;
status.progress = 0;
status.eta = void 0;
const executable = binaryPath ?? resolveLameBinary();
const libraryDir = resolveBundledLibraryDirectory();
const childEnv = applyBundledLibraryPath({ ...process.env }, libraryDir);
const child = (0, import_node_child_process.spawn)(executable, spawnArgs, {
env: childEnv
});
const progressTargets = new Set(progressSources);
let hasSeenCliError = false;
let stderrBuffer = "";
const deliveredErrorMessages = /* @__PURE__ */ new Set();
const deliverError = (error) => {
const message = error.message ?? String(error);
if (deliveredErrorMessages.has(message)) {
return;
}
deliveredErrorMessages.add(message);
onError(error);
};
const emitCliError = (error) => {
hasSeenCliError = true;
deliverError(error);
};
const emitExitError = (error) => {
if (hasSeenCliError) {
return;
}
deliverError(error);
};
const handleStdout = (chunk) => {
if (progressTargets.has("stdout")) {
const { error } = processProgressChunk(chunk, {
kind,
status,
emitter,
completeOnTag
});
if (error) {
emitCliError(error);
return;
}
}
onStdoutData?.(chunk);
};
const handleStderr = (chunk) => {
stderrBuffer += chunk.toString();
if (progressTargets.has("stderr")) {
const { error } = processProgressChunk(chunk, {
kind,
status,
emitter,
completeOnTag
});
if (error) {
emitCliError(error);
return;
}
}
onStderrData?.(chunk);
};
child.stdout.on("data", handleStdout);
child.stderr.on("data", handleStderr);
if (onStdoutEnd) {
child.stdout.on("end", onStdoutEnd);
}
child.stdout.on("error", (error) => {
onStdoutError?.(error);
emitCliError(error);
});
child.stderr.on("error", (error) => {
onStderrError?.(error);
emitCliError(error);
});
child.stdin.on("error", (error) => {
onStdinError?.(error);
emitCliError(error);
});
child.on("error", emitCliError);
child.on("close", (code) => {
const exitError = getExitError(code, executable);
if (exitError) {
const bufferedLines = stderrBuffer.split(/\r?\n/).map((value) => value.trim()).filter((value) => value.length > 0);
for (const line of bufferedLines) {
const normalized = normalizeCliMessage(line);
if (normalized) {
emitCliError(new Error(normalized));
return;
}
}
emitExitError(exitError);
return;
}
markProcessFinished(status, emitter);
onSuccess?.();
});
return child;
}
// src/core/lame.ts
function isFloatArray(view) {
return view instanceof Float32Array || view instanceof Float64Array;
}
var Lame = class {
status = createInitialStatus();
emitter = new import_node_events.EventEmitter();
options;
builder;
filePath;
fileBuffer;
fileBufferTempFilePath;
progressedFilePath;
progressedBuffer;
progressedBufferTempFilePath;
lamePath;
tempPath;
constructor(options) {
if (options.output === "stream") {
throw new Error(
'lame: The streaming output mode requires LameStream with mode set to "encode" or "decode"'
);
}
this.options = options;
this.builder = new LameOptions(this.options);
this.lamePath = resolveLameBinary();
this.tempPath = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), "node-lame");
}
setFile(path) {
if (!(0, import_node_fs2.existsSync)(path)) {
throw new Error("Audio file (path) does not exist");
}
this.filePath = path;
this.fileBuffer = void 0;
return this;
}
setBuffer(file) {
const normalized = this.normalizeInputBuffer(file);
this.fileBuffer = normalized;
this.filePath = void 0;
return this;
}
setLamePath(path) {
if (typeof path !== "string" || path.trim() === "") {
throw new Error("Lame path must be a non-empty string");
}
this.lamePath = path;
return this;
}
setTempPath(path) {
if (typeof path !== "string" || path.trim() === "") {
throw new Error("Temp path must be a non-empty string");
}
this.tempPath = path;
return this;
}
getFile() {
if (!this.progressedFilePath) {
throw new Error("Audio is not yet decoded/encoded");
}
return this.progressedFilePath;
}
getBuffer() {
if (!this.progressedBuffer) {
throw new Error("Audio is not yet decoded/encoded");
}
return this.progressedBuffer;
}
getEmitter() {
return this.emitter;
}
getStatus() {
return this.status;
}
async encode() {
return this.executeConversion("encode");
}
async decode() {
return this.executeConversion("decode");
}
/**
* Executes the CLI for the provided conversion type, handling buffers, files,
* and cleanup in case of errors.
*/
async executeConversion(type) {
if (!this.filePath && !this.fileBuffer) {
throw new Error("Audio file to encode is not set");
}
let inputPath = this.filePath;
if (!inputPath && this.fileBuffer) {
inputPath = await this.persistInputBufferToTempFile(type);
}
try {
return await this.spawnLameAndTrackProgress(inputPath, type);
} catch (error) {
await this.removeTempArtifacts();
throw error;
}
}
/**
* Spawns the LAME process and listens to progress updates, resolving once complete.
*/
async spawnLameAndTrackProgress(inputFilePath, type) {
this.status = createInitialStatus();
const { outputPath, bufferOutput } = await this.prepareOutputTarget(type);
const spawnArgs = buildLameSpawnArgs(
this.builder,
type,
inputFilePath,
outputPath
);
return new Promise((resolve, reject) => {
const cleanup = async () => {
if (this.fileBufferTempFilePath) {
await (0, import_promises.unlink)(this.fileBufferTempFilePath).catch(() => {
});
this.fileBufferTempFilePath = void 0;
}
};
this.emitter.once("finish", () => {
cleanup().then(async () => {
if (bufferOutput && this.progressedBufferTempFilePath) {
const buffer = await (0, import_promises.readFile)(
this.progressedBufferTempFilePath
);
await (0, import_promises.unlink)(
this.progressedBufferTempFilePath
).catch(() => {
});
if (!Buffer.isBuffer(buffer)) {
throw new Error(
"Unexpected output format received from temporary file"
);
}
this.progressedBuffer = buffer;
this.progressedBufferTempFilePath = void 0;
}
resolve(this);
}).catch(reject);
});
this.emitter.once("error", (error) => {
cleanup().then(() => reject(error)).catch(reject);
});
spawnLameProcess({
binaryPath: this.lamePath,
spawnArgs,
kind: type,
status: this.status,
emitter: this.emitter,
completeOnTag: true,
progressSources: ["stdout", "stderr"],
onError: (error) => {
this.emitter.emit("error", error);
}
});
});
}
normalizeInputBuffer(input) {
if (Buffer.isBuffer(input)) {
return input;
}
if (input instanceof ArrayBuffer) {
return Buffer.from(new Uint8Array(input));
}
if (ArrayBuffer.isView(input)) {
return this.convertArrayViewToBuffer(input);
}
throw new Error("Audio file (buffer) does not exist");
}
convertArrayViewToBuffer(view) {
if (isFloatArray(view)) {
return this.convertFloatArrayToBuffer(view);
}
const uintView = new Uint8Array(
view.buffer,
view.byteOffset,
view.byteLength
);
return Buffer.from(uintView);
}
convertFloatArrayToBuffer(view) {
const bitwidth = this.options.bitwidth ?? 16;
const useBigEndian = this.shouldUseBigEndian();
const isSigned = this.isSignedForBitwidth(bitwidth);
switch (bitwidth) {
case 8: {
const buffer = Buffer.alloc(view.length);
for (let index = 0; index < view.length; index += 1) {
const sample = view[index];
if (isSigned) {
const value = this.scaleToSignedInteger(sample, 8);
buffer.writeInt8(value, index);
} else {
const value = this.scaleToUnsignedInteger(sample, 8);
buffer.writeUInt8(value, index);
}
}
return buffer;
}
case 16: {
if (!isSigned) {
throw new Error(
"lame: Float PCM input only supports signed samples for bitwidth 16"
);
}
const buffer = Buffer.alloc(view.length * 2);
for (let index = 0; index < view.length; index += 1) {
const value = this.scaleToSignedInteger(view[index], 16);
const offset = index * 2;
if (useBigEndian) {
buffer.writeInt16BE(value, offset);
} else {
buffer.writeInt16LE(value, offset);
}
}
return buffer;
}
case 24: {
if (!isSigned) {
throw new Error(
"lame: Float PCM input only supports signed samples for bitwidth 24"
);
}
const buffer = Buffer.alloc(view.length * 3);
for (let index = 0; index < view.length; index += 1) {
const offset = index * 3;
const scaled = this.scaleToSignedInteger(view[index], 24);
let value = scaled;
if (value < 0) {
value += 1 << 24;
}
if (useBigEndian) {
buffer[offset] = value >> 16 & 255;
buffer[offset + 1] = value >> 8 & 255;
buffer[offset + 2] = value & 255;
} else {
buffer[offset] = value & 255;
buffer[offset + 1] = value >> 8 & 255;
buffer[offset + 2] = value >> 16 & 255;
}
}
return buffer;
}
case 32: {
if (!isSigned) {
throw new Error(
"lame: Float PCM input only supports signed samples for bitwidth 32"
);
}
const buffer = Buffer.alloc(view.length * 4);
for (let index = 0; index < view.length; index += 1) {
const value = this.scaleToSignedInteger(view[index], 32);
const offset = index * 4;
if (useBigEndian) {
buffer.writeInt32BE(value, offset);
} else {
buffer.writeInt32LE(value, offset);
}
}
return buffer;
}
}
}
shouldUseBigEndian() {
if (this.options["big-endian"] === true) {
return true;
}
if (this.options["little-endian"] === true) {
return false;
}
return false;
}
isSignedForBitwidth(bitwidth) {
if (bitwidth === 8) {
if (this.options.unsigned === true) {
return false;
}
return this.options.signed === true;
}
if (this.options.unsigned === true) {
return false;
}
return true;
}
clampSample(value) {
if (!Number.isFinite(value)) {
return 0;
}
if (value <= -1) {
return -1;
}
if (value >= 1) {
return 1;
}
return value;
}
scaleToSignedInteger(value, bitwidth) {
const clamped = this.clampSample(value);
const positiveMax = Math.pow(2, bitwidth - 1) - 1;
const negativeScale = Math.pow(2, bitwidth - 1);
if (clamped <= -1) {
return -negativeScale;
}
if (clamped >= 1) {
return positiveMax;
}
if (clamped < 0) {
const scaled2 = Math.round(clamped * negativeScale);
return Math.max(-negativeScale, scaled2);
}
const scaled = Math.round(clamped * positiveMax);
return Math.min(positiveMax, scaled);
}
scaleToUnsignedInteger(value, bitwidth) {
const clamped = this.clampSample(value);
const normalized = (clamped + 1) / 2;
const max = Math.pow(2, bitwidth) - 1;
const scaled = Math.round(normalized * max);
return Math.min(Math.max(scaled, 0), max);
}
async persistInputBufferToTempFile(type) {
const tempPath = await this.generateTempFilePath("raw", type);
await (0, import_promises.writeFile)(tempPath, this.toUint8Array(this.fileBuffer));
this.fileBufferTempFilePath = tempPath;
return tempPath;
}
toUint8Array(view) {
if (view instanceof Buffer) {
return new Uint8Array(
view.buffer,
view.byteOffset,
view.byteLength
);
}
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}
async prepareOutputTarget(type) {
if (this.options.output === "buffer") {
const tempOutPath = await this.generateTempFilePath(
"encoded",
type
);
this.progressedBufferTempFilePath = tempOutPath;
return { outputPath: tempOutPath, bufferOutput: true };
}
this.progressedFilePath = this.options.output;
await this.ensureOutputDirectoryExists(this.progressedFilePath);
return {
outputPath: this.progressedFilePath,
bufferOutput: false
};
}
async ensureOutputDirectoryExists(filePath) {
const dir = (0, import_node_path3.dirname)(filePath);
if (!dir || dir === ".") {
return;
}
await (0, import_promises.mkdir)(dir, { recursive: true });
}
async generateTempFilePath(type, progressType) {
const dir = (0, import_node_path3.join)(this.tempPath, type);
await (0, import_promises.mkdir)(dir, { recursive: true });
const token = (0, import_node_crypto.randomBytes)(16).toString("hex");
const extension = type === "raw" && progressType === "decode" ? ".mp3" : "";
return (0, import_node_path3.join)(dir, `${token}${extension}`);
}
async removeTempArtifacts() {
if (this.fileBufferTempFilePath) {
await (0, import_promises.unlink)(this.fileBufferTempFilePath).catch(() => {
});
this.fileBufferTempFilePath = void 0;
}
if (this.progressedBufferTempFilePath) {
await (0, import_promises.unlink)(this.progressedBufferTempFilePath).catch(() => {
});
this.progressedBufferTempFilePath = void 0;
}
}
};
// src/core/lame-stream.ts
var import_node_events2 = require("events");
var import_node_stream = require("stream");
var LameStream = class extends import_node_stream.Duplex {
emitter;
status;
builder;
binaryPath;
kind;
child;
isStdoutPaused = false;
hasErrored = false;
finished = false;
constructor(options) {
super({ allowHalfOpen: false });
const { binaryPath, mode, ...cliOptions } = options;
if (!isValidStreamMode(mode)) {
throw new Error(
'lame: LameStream requires a mode of either "encode" or "decode"'
);
}
const normalizedOptions = {
...cliOptions,
output: "stream"
};
this.binaryPath = binaryPath;
this.status = createInitialStatus();
this.emitter = new import_node_events2.EventEmitter();
this.builder = new LameOptions(normalizedOptions);
this.kind = mode;
this.initialize(this.kind);
}
getEmitter() {
return this.emitter;
}
getStatus() {
return this.status;
}
_read() {
if (!this.child) {
return;
}
if (this.isStdoutPaused && !this.child.stdout.readableEnded) {
this.isStdoutPaused = false;
this.child.stdout.resume();
}
}
_write(chunk, encoding, callback) {
const child = this.child;
if (!child) {
callback(new Error("lame: Stream mode is not initialized yet"));
return;
}
if (this.finished || child.stdin.destroyed) {
callback(new Error("lame: Stream has already finished"));
return;
}
try {
const flushed = child.stdin.write(chunk, encoding);
if (!flushed) {
const cleanup = () => {
child.stdin.off("drain", onDrain);
child.stdin.off("error", onError);
child.stdin.off("close", onClose);
};
const onDrain = () => {
cleanup();
callback();
};
const onError = (error) => {
cleanup();
callback(error);
};
const onClose = () => {
cleanup();
callback(new Error("lame: Input stream closed before drain"));
};
child.stdin.once("drain", onDrain);
child.stdin.once("error", onError);
child.stdin.once("close", onClose);
return;
}
} catch (error) {
callback(error);
return;
}
callback();
}
_final(callback) {
const child = this.child;
if (!child) {
callback(new Err