UNPKG

node-lame

Version:
1,806 lines (1,799 loc) 52.6 kB
"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