UNPKG

node-lame

Version:
1 lines 109 kB
{"version":3,"sources":["../src/index.ts","../src/core/lame.ts","../src/internal/binary/resolve-binary.ts","../src/core/lame-options.ts","../src/core/lame-process.ts","../src/core/lame-stream.ts"],"sourcesContent":["export { Lame } from \"./core/lame\";\nexport { LameOptions } from \"./core/lame-options\";\nexport { LameStream } from \"./core/lame-stream\";\nexport type { LameOptionsBag, LameProgressEmitter, LameStatus } from \"./types\";\nexport type { LameStreamOptions } from \"./types\";\nexport {\n resolveBundledLameBinary,\n resolveBundledLibraryDirectory,\n resolveLameBinary,\n} from \"./internal/binary/resolve-binary\";\n","import { randomBytes } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\n\nimport type { BitWidth, LameOptionsBag, LameProgressEmitter, LameStatus } from \"../types\";\nimport { resolveLameBinary } from \"../internal/binary/resolve-binary\";\nimport { LameOptions } from \"./lame-options\";\nimport {\n buildLameSpawnArgs,\n createInitialStatus,\n spawnLameProcess,\n} from \"./lame-process\";\nimport type { ProgressKind } from \"./lame-process\";\n\nfunction isFloatArray(\n view: ArrayBufferView,\n): view is Float32Array | Float64Array {\n return view instanceof Float32Array || view instanceof Float64Array;\n}\n\n/**\n * Thin wrapper around the LAME CLI that manages temp files, progress events,\n * and output handling while delegating the heavy lifting to the binary.\n */\nclass Lame {\n private status: LameStatus = createInitialStatus();\n\n private readonly emitter: LameProgressEmitter =\n new EventEmitter() as LameProgressEmitter;\n\n private readonly options: LameOptionsBag;\n private readonly builder: LameOptions;\n\n private filePath?: string;\n private fileBuffer?: Buffer;\n private fileBufferTempFilePath?: string;\n\n private progressedFilePath?: string;\n private progressedBuffer?: Buffer;\n private progressedBufferTempFilePath?: string;\n\n private lamePath: string;\n private tempPath: string;\n\n constructor(options: LameOptionsBag) {\n if (options.output === \"stream\") {\n throw new Error(\n 'lame: The streaming output mode requires LameStream with mode set to \"encode\" or \"decode\"',\n );\n }\n\n this.options = options;\n this.builder = new LameOptions(this.options);\n this.lamePath = resolveLameBinary();\n this.tempPath = join(tmpdir(), \"node-lame\");\n }\n\n public setFile(path: string): this {\n if (!existsSync(path)) {\n throw new Error(\"Audio file (path) does not exist\");\n }\n\n this.filePath = path;\n this.fileBuffer = undefined;\n\n return this;\n }\n\n public setBuffer(\n file: Buffer | ArrayBuffer | ArrayBufferView,\n ): this {\n const normalized = this.normalizeInputBuffer(file);\n\n this.fileBuffer = normalized;\n this.filePath = undefined;\n\n return this;\n }\n\n public setLamePath(path: string): this {\n if (typeof path !== \"string\" || path.trim() === \"\") {\n throw new Error(\"Lame path must be a non-empty string\");\n }\n\n this.lamePath = path;\n\n return this;\n }\n\n public setTempPath(path: string): this {\n if (typeof path !== \"string\" || path.trim() === \"\") {\n throw new Error(\"Temp path must be a non-empty string\");\n }\n\n this.tempPath = path;\n\n return this;\n }\n\n public getFile(): string {\n if (!this.progressedFilePath) {\n throw new Error(\"Audio is not yet decoded/encoded\");\n }\n\n return this.progressedFilePath;\n }\n\n public getBuffer(): Buffer {\n if (!this.progressedBuffer) {\n throw new Error(\"Audio is not yet decoded/encoded\");\n }\n\n return this.progressedBuffer;\n }\n\n public getEmitter(): LameProgressEmitter {\n return this.emitter;\n }\n\n public getStatus(): LameStatus {\n return this.status;\n }\n\n public async encode(): Promise<this> {\n return this.executeConversion(\"encode\");\n }\n\n public async decode(): Promise<this> {\n return this.executeConversion(\"decode\");\n }\n\n /**\n * Executes the CLI for the provided conversion type, handling buffers, files,\n * and cleanup in case of errors.\n */\n private async executeConversion(type: ProgressKind): Promise<this> {\n if (!this.filePath && !this.fileBuffer) {\n throw new Error(\"Audio file to encode is not set\");\n }\n\n let inputPath = this.filePath;\n\n if (!inputPath && this.fileBuffer) {\n inputPath = await this.persistInputBufferToTempFile(type);\n }\n\n try {\n return await this.spawnLameAndTrackProgress(inputPath!, type);\n } catch (error) {\n await this.removeTempArtifacts();\n throw error;\n }\n }\n\n /**\n * Spawns the LAME process and listens to progress updates, resolving once complete.\n */\n private async spawnLameAndTrackProgress(\n inputFilePath: string,\n type: ProgressKind,\n ): Promise<this> {\n this.status = createInitialStatus();\n const { outputPath, bufferOutput } =\n await this.prepareOutputTarget(type);\n const spawnArgs = buildLameSpawnArgs(\n this.builder,\n type,\n inputFilePath,\n outputPath,\n );\n\n return new Promise((resolve, reject) => {\n const cleanup = async () => {\n if (this.fileBufferTempFilePath) {\n await unlink(this.fileBufferTempFilePath).catch(() => {});\n this.fileBufferTempFilePath = undefined;\n }\n };\n\n this.emitter.once(\"finish\", () => {\n cleanup()\n .then(async () => {\n if (bufferOutput && this.progressedBufferTempFilePath) {\n const buffer = await readFile(\n this.progressedBufferTempFilePath,\n );\n await unlink(\n this.progressedBufferTempFilePath,\n ).catch(() => {});\n\n if (!Buffer.isBuffer(buffer)) {\n throw new Error(\n \"Unexpected output format received from temporary file\",\n );\n }\n\n this.progressedBuffer = buffer;\n this.progressedBufferTempFilePath = undefined;\n }\n\n resolve(this);\n })\n .catch(reject);\n });\n\n this.emitter.once(\"error\", (error) => {\n cleanup()\n .then(() => reject(error))\n .catch(reject);\n });\n\n spawnLameProcess({\n binaryPath: this.lamePath,\n spawnArgs,\n kind: type,\n status: this.status,\n emitter: this.emitter,\n completeOnTag: true,\n progressSources: [\"stdout\", \"stderr\"],\n onError: (error) => {\n this.emitter.emit(\"error\", error);\n },\n });\n });\n }\n\n private normalizeInputBuffer(\n input: Buffer | ArrayBuffer | ArrayBufferView,\n ): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n\n if (input instanceof ArrayBuffer) {\n return Buffer.from(new Uint8Array(input));\n }\n\n if (ArrayBuffer.isView(input)) {\n return this.convertArrayViewToBuffer(input);\n }\n\n throw new Error(\"Audio file (buffer) does not exist\");\n }\n\n private convertArrayViewToBuffer(view: ArrayBufferView): Buffer {\n if (isFloatArray(view)) {\n return this.convertFloatArrayToBuffer(view);\n }\n\n const uintView = new Uint8Array(\n view.buffer,\n view.byteOffset,\n view.byteLength,\n );\n\n return Buffer.from(uintView);\n }\n\n private convertFloatArrayToBuffer(\n view: Float32Array | Float64Array,\n ): Buffer {\n const bitwidth: BitWidth = this.options.bitwidth ?? 16;\n const useBigEndian = this.shouldUseBigEndian();\n const isSigned = this.isSignedForBitwidth(bitwidth);\n\n switch (bitwidth) {\n case 8: {\n const buffer = Buffer.alloc(view.length);\n\n for (let index = 0; index < view.length; index += 1) {\n const sample = view[index];\n\n if (isSigned) {\n const value = this.scaleToSignedInteger(sample, 8);\n buffer.writeInt8(value, index);\n } else {\n const value = this.scaleToUnsignedInteger(sample, 8);\n buffer.writeUInt8(value, index);\n }\n }\n\n return buffer;\n }\n\n case 16: {\n if (!isSigned) {\n throw new Error(\n \"lame: Float PCM input only supports signed samples for bitwidth 16\",\n );\n }\n\n const buffer = Buffer.alloc(view.length * 2);\n for (let index = 0; index < view.length; index += 1) {\n const value = this.scaleToSignedInteger(view[index], 16);\n const offset = index * 2;\n\n if (useBigEndian) {\n buffer.writeInt16BE(value, offset);\n } else {\n buffer.writeInt16LE(value, offset);\n }\n }\n\n return buffer;\n }\n\n case 24: {\n if (!isSigned) {\n throw new Error(\n \"lame: Float PCM input only supports signed samples for bitwidth 24\",\n );\n }\n\n const buffer = Buffer.alloc(view.length * 3);\n for (let index = 0; index < view.length; index += 1) {\n const offset = index * 3;\n const scaled = this.scaleToSignedInteger(view[index], 24);\n let value = scaled;\n\n if (value < 0) {\n value += 1 << 24;\n }\n\n if (useBigEndian) {\n buffer[offset] = (value >> 16) & 0xff;\n buffer[offset + 1] = (value >> 8) & 0xff;\n buffer[offset + 2] = value & 0xff;\n } else {\n buffer[offset] = value & 0xff;\n buffer[offset + 1] = (value >> 8) & 0xff;\n buffer[offset + 2] = (value >> 16) & 0xff;\n }\n }\n\n return buffer;\n }\n\n case 32: {\n if (!isSigned) {\n throw new Error(\n \"lame: Float PCM input only supports signed samples for bitwidth 32\",\n );\n }\n\n const buffer = Buffer.alloc(view.length * 4);\n for (let index = 0; index < view.length; index += 1) {\n const value = this.scaleToSignedInteger(view[index], 32);\n const offset = index * 4;\n\n if (useBigEndian) {\n buffer.writeInt32BE(value, offset);\n } else {\n buffer.writeInt32LE(value, offset);\n }\n }\n\n return buffer;\n }\n }\n }\n\n private shouldUseBigEndian(): boolean {\n if (this.options[\"big-endian\"] === true) {\n return true;\n }\n\n if (this.options[\"little-endian\"] === true) {\n return false;\n }\n\n return false;\n }\n\n private isSignedForBitwidth(bitwidth: BitWidth): boolean {\n if (bitwidth === 8) {\n if (this.options.unsigned === true) {\n return false;\n }\n\n return this.options.signed === true;\n }\n\n if (this.options.unsigned === true) {\n return false;\n }\n\n return true;\n }\n\n private clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n if (value <= -1) {\n return -1;\n }\n\n if (value >= 1) {\n return 1;\n }\n\n return value;\n }\n\n private scaleToSignedInteger(value: number, bitwidth: BitWidth): number {\n const clamped = this.clampSample(value);\n\n const positiveMax = Math.pow(2, bitwidth - 1) - 1;\n const negativeScale = Math.pow(2, bitwidth - 1);\n\n if (clamped <= -1) {\n return -negativeScale;\n }\n\n if (clamped >= 1) {\n return positiveMax;\n }\n\n if (clamped < 0) {\n const scaled = Math.round(clamped * negativeScale);\n return Math.max(-negativeScale, scaled);\n }\n\n const scaled = Math.round(clamped * positiveMax);\n return Math.min(positiveMax, scaled);\n }\n\n private scaleToUnsignedInteger(value: number, bitwidth: BitWidth): number {\n const clamped = this.clampSample(value);\n const normalized = (clamped + 1) / 2;\n const max = Math.pow(2, bitwidth) - 1;\n const scaled = Math.round(normalized * max);\n\n return Math.min(Math.max(scaled, 0), max);\n }\n\n private async persistInputBufferToTempFile(\n type: ProgressKind,\n ): Promise<string> {\n const tempPath = await this.generateTempFilePath(\"raw\", type);\n await writeFile(tempPath, this.toUint8Array(this.fileBuffer!));\n this.fileBufferTempFilePath = tempPath;\n return tempPath;\n }\n\n private toUint8Array(view: Buffer | ArrayBufferView): Uint8Array {\n if (view instanceof Buffer) {\n return new Uint8Array(\n view.buffer,\n view.byteOffset,\n view.byteLength,\n );\n }\n\n return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n }\n\n private async prepareOutputTarget(type: ProgressKind): Promise<{\n outputPath: string;\n bufferOutput: boolean;\n }> {\n if (this.options.output === \"buffer\") {\n const tempOutPath = await this.generateTempFilePath(\n \"encoded\",\n type,\n );\n this.progressedBufferTempFilePath = tempOutPath;\n\n return { outputPath: tempOutPath, bufferOutput: true };\n }\n\n this.progressedFilePath = this.options.output as string;\n await this.ensureOutputDirectoryExists(this.progressedFilePath);\n\n return {\n outputPath: this.progressedFilePath,\n bufferOutput: false,\n };\n }\n\n private async ensureOutputDirectoryExists(filePath: string) {\n const dir = dirname(filePath);\n if (!dir || dir === \".\") {\n return;\n }\n\n await mkdir(dir, { recursive: true });\n }\n\n private async generateTempFilePath(\n type: \"raw\" | \"encoded\",\n progressType: ProgressKind,\n ): Promise<string> {\n const dir = join(this.tempPath, type);\n await mkdir(dir, { recursive: true });\n\n const token = randomBytes(16).toString(\"hex\");\n const extension =\n type === \"raw\" && progressType === \"decode\" ? \".mp3\" : \"\";\n\n return join(dir, `${token}${extension}`);\n }\n\n private async removeTempArtifacts() {\n if (this.fileBufferTempFilePath) {\n await unlink(this.fileBufferTempFilePath).catch(() => {});\n this.fileBufferTempFilePath = undefined;\n }\n\n if (this.progressedBufferTempFilePath) {\n await unlink(this.progressedBufferTempFilePath).catch(() => {});\n this.progressedBufferTempFilePath = undefined;\n }\n }\n}\n\nexport { Lame };\nexport {\n normalizeCliMessage,\n parseDecodeProgressLine,\n parseEncodeProgressLine,\n} from \"./lame-process\";\n","import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\ndeclare const __dirname: string | undefined;\ndeclare const __filename: string | undefined;\n\ntype ImportMetaLike = { url?: string } | undefined;\n\nfunction deriveModuleUrl(\n meta: ImportMetaLike,\n filename: string | undefined,\n): string | undefined {\n if (meta && typeof meta.url === \"string\") {\n return meta.url;\n }\n\n if (typeof filename === \"string\") {\n return pathToFileURL(filename).href;\n }\n\n return undefined;\n}\n\nconst moduleUrl = (() => {\n let meta: ImportMetaLike = undefined;\n\n try {\n meta = import.meta as ImportMetaLike;\n } catch {\n // ignore when import.meta is unavailable (CommonJS build)\n }\n\n const resolvedFilename =\n typeof __filename === \"string\" ? __filename : undefined;\n\n return deriveModuleUrl(meta, resolvedFilename);\n})();\n\nfunction findPackageRootFrom(startDir: string): string | null {\n let current = startDir;\n\n while (true) {\n const packageJsonPath = join(current, \"package.json\");\n if (existsSync(packageJsonPath)) {\n return current;\n }\n\n const parent = dirname(current);\n if (parent === current) {\n return null;\n }\n\n current = parent;\n }\n}\n\nfunction resolvePackageRoot(\n moduleHref: string | undefined,\n dirname: string | undefined,\n): string {\n const normalizedDir =\n dirname ??\n (moduleHref != null\n ? fileURLToPath(new URL(\".\", moduleHref))\n : undefined);\n\n if (normalizedDir) {\n const detectedRoot = findPackageRootFrom(normalizedDir);\n if (detectedRoot) {\n return detectedRoot;\n }\n }\n\n if (dirname) {\n return join(dirname, \"..\", \"..\", \"..\");\n }\n\n if (moduleHref != null) {\n return fileURLToPath(new URL(\"../../..\", moduleHref));\n }\n\n return process.cwd();\n}\n\nconst PACKAGE_ROOT = resolvePackageRoot(\n moduleUrl,\n typeof __dirname === \"string\" ? __dirname : undefined,\n);\nconst CUSTOM_BINARY_ENV = \"LAME_BINARY\";\nconst LIBRARY_DIRECTORY_NAME = \"lib\";\n\nfunction getPlatformExecutableSuffix(platform: typeof process.platform): string {\n return platform === \"win32\" ? \".exe\" : \"\";\n}\n\nconst PLATFORM_EXECUTABLE_SUFFIX = getPlatformExecutableSuffix(\n process.platform,\n);\n\n/**\n * Attempt to resolve the absolute path to a bundled LAME binary.\n * Returns null when no bundled binary could be found.\n */\nfunction resolveBundledLameBinary(): string | null {\n const explicitBinary = process.env[CUSTOM_BINARY_ENV];\n if (explicitBinary && existsSync(explicitBinary)) {\n return explicitBinary;\n }\n\n const candidate = join(\n PACKAGE_ROOT,\n \"vendor\",\n \"lame\",\n `${process.platform}-${process.arch}`,\n `lame${PLATFORM_EXECUTABLE_SUFFIX}`,\n );\n\n if (existsSync(candidate)) {\n return candidate;\n }\n\n return null;\n}\n\nfunction resolveBundledLibraryDirectory(): string | null {\n const candidate = join(\n PACKAGE_ROOT,\n \"vendor\",\n \"lame\",\n `${process.platform}-${process.arch}`,\n LIBRARY_DIRECTORY_NAME,\n );\n\n if (existsSync(candidate)) {\n return candidate;\n }\n\n return null;\n}\n\n/**\n * Resolve the binary to use. Preference order:\n * 1. `LAME_BINARY` environment variable.\n * 2. Bundled prebuilt binary located in `vendor/lame/<platform>-<arch>/`.\n * 3. Fallback to the `lame` executable on PATH.\n */\nfunction resolveLameBinary(): string {\n const resolved = resolveBundledLameBinary();\n return resolved ?? \"lame\";\n}\n\nexport {\n deriveModuleUrl,\n getPlatformExecutableSuffix,\n resolveBundledLameBinary,\n resolveBundledLibraryDirectory,\n resolveLameBinary,\n resolvePackageRoot,\n};\n","import type { LameOptionsBag, PriorityLevel } from \"../types\";\n\n/**\n * Translates option objects into the argument list expected by the LAME CLI.\n */\nclass LameOptions {\n private readonly args: string[] = [];\n private useDefaultDisptime = true;\n /**\n * Validate all options and build argument array for binary\n * @param {Object} options\n */\n constructor(options: LameOptionsBag) {\n // Output is required\n if (options[\"output\"] == undefined) {\n throw new Error(\"lame: Invalid option: 'output' is required\");\n }\n\n // Save options as arguments\n const entries = Object.entries(options) as Array<\n [keyof LameOptionsBag, LameOptionsBag[keyof LameOptionsBag]]\n >;\n\n for (const [key, value] of entries) {\n let arg: Array<string | number> | undefined;\n\n switch (key) {\n case \"output\":\n continue;\n case \"raw\":\n arg = this.raw(value);\n break;\n case \"swap-bytes\":\n arg = this.swapBytes(value);\n break;\n case \"swap-channel\":\n arg = this.swapChannel(value);\n break;\n case \"gain\":\n arg = this.gain(value);\n break;\n case \"sfreq\":\n arg = this.sfreq(value);\n break;\n case \"bitwidth\":\n arg = this.bitwidth(value);\n break;\n case \"signed\":\n arg = this.signed(value);\n break;\n case \"unsigned\":\n arg = this.unsigned(value);\n break;\n case \"little-endian\":\n arg = this.littleEndian(value);\n break;\n case \"big-endian\":\n arg = this.bigEndian(value);\n break;\n case \"mp1Input\":\n arg = this.mp1Input(value);\n break;\n case \"mp2Input\":\n arg = this.mp2Input(value);\n break;\n case \"mp3Input\":\n arg = this.mp3Input(value);\n break;\n case \"mode\":\n arg = this.mode(value);\n break;\n case \"to-mono\":\n arg = this.toMono(value);\n break;\n case \"channel-different-block-sizes\":\n arg = this.channelDifferentBlockSize(value);\n break;\n case \"freeformat\":\n arg = this.freeformat(value);\n break;\n case \"disable-info-tag\":\n arg = this.disableInfoTag(value);\n break;\n case \"nogap\":\n arg = this.nogap(value);\n break;\n case \"nogapout\":\n arg = this.nogapout(value);\n break;\n case \"nogaptags\":\n arg = this.nogaptags(value);\n break;\n case \"out-dir\":\n arg = this.outDir(value);\n break;\n case \"comp\":\n arg = this.comp(value);\n break;\n case \"scale\":\n arg = this.scale(value);\n break;\n case \"scale-l\":\n arg = this.scaleL(value);\n break;\n case \"scale-r\":\n arg = this.scaleR(value);\n break;\n case \"replaygain-fast\":\n arg = this.replaygainFast(value);\n break;\n case \"replaygain-accurate\":\n arg = this.replaygainAccurate(value);\n break;\n case \"no-replaygain\":\n arg = this.noreplaygain(value);\n break;\n case \"clip-detect\":\n arg = this.clipDetect(value);\n break;\n case \"preset\":\n arg = this.preset(value);\n break;\n case \"noasm\":\n arg = this.noasm(value);\n break;\n case \"quality\":\n arg = this.quality(value);\n break;\n case \"quality-high\":\n arg = this.qualityHigh(value);\n break;\n case \"fast-encoding\":\n arg = this.fastEncoding(value);\n break;\n case \"bitrate\":\n arg = this.bitrate(value);\n break;\n case \"max-bitrate\":\n arg = this.maxBitrate(value);\n break;\n case \"force-bitrate\":\n arg = this.forceBitrate(value);\n break;\n case \"cbr\":\n arg = this.cbr(value);\n break;\n case \"abr\":\n arg = this.abr(value);\n break;\n case \"vbr\":\n arg = this.vbr(value);\n break;\n case \"vbr-quality\":\n arg = this.vbrQuality(value);\n break;\n case \"vbr-old\":\n arg = this.vbrOld(value);\n break;\n case \"vbr-new\":\n arg = this.vbrNew(value);\n break;\n case \"ignore-noise-in-sfb21\":\n arg = this.ignoreNoiseInSfb21(value);\n break;\n case \"emp\":\n arg = this.emp(value);\n break;\n case \"mark-as-copyrighted\":\n arg = this.markAsCopyrighted(value);\n break;\n case \"mark-as-copy\":\n arg = this.markAsCopy(value);\n break;\n case \"crc-error-protection\":\n arg = this.crcErrorProtection(value);\n break;\n case \"nores\":\n arg = this.nores(value);\n break;\n case \"strictly-enforce-ISO\":\n arg = this.strictlyEnforceIso(value);\n break;\n case \"lowpass\":\n arg = this.lowpass(value);\n break;\n case \"lowpass-width\":\n arg = this.lowpassWidth(value);\n break;\n case \"highpass\":\n arg = this.highpass(value);\n break;\n case \"highpass-width\":\n arg = this.highpassWidth(value);\n break;\n case \"resample\":\n arg = this.resample(value);\n break;\n case \"decode-mp3delay\":\n arg = this.decodeMp3Delay(value);\n break;\n case \"priority\":\n arg = this.priority(value);\n break;\n case \"disptime\":\n arg = this.disptime(value);\n break;\n case \"silent\":\n arg = this.silent(value);\n break;\n case \"quiet\":\n arg = this.quiet(value);\n break;\n case \"verbose\":\n arg = this.verbose(value);\n break;\n case \"help\":\n arg = this.help(value);\n break;\n case \"usage\":\n arg = this.usage(value);\n break;\n case \"longhelp\":\n arg = this.longHelp(value);\n break;\n case \"version\":\n arg = this.version(value);\n break;\n case \"license\":\n arg = this.license(value);\n break;\n case \"no-histogram\":\n arg = this.noHistogram(value);\n break;\n case \"meta\":\n arg = this.meta(value);\n break;\n default:\n throw new Error(\"Unknown parameter \" + key);\n }\n\n if (Array.isArray(arg)) {\n this.args.push(...arg.map((item) => String(item)));\n }\n }\n }\n\n /**\n * Get all arguments for binary\n */\n public getArguments() {\n return this.args;\n }\n\n public shouldUseDefaultDisptime() {\n return this.useDefaultDisptime;\n }\n\n private raw(value: unknown) {\n if (value == true) {\n return [`-r`];\n } else {\n return undefined;\n }\n }\n\n private swapBytes(value: unknown) {\n if (value == true) {\n return [`-x`];\n } else {\n return undefined;\n }\n }\n\n private swapChannel(value: unknown) {\n if (value == true) {\n return [`--swap-channel`];\n }\n\n return undefined;\n }\n\n private gain(value: unknown) {\n if (value === undefined) {\n return undefined;\n }\n\n if (typeof value === \"number\" && value >= -20 && value <= 12) {\n return [`--gain`, String(value)];\n }\n\n throw new Error(\n \"lame: Invalid option: 'gain' must be a number between -20 and 12.\",\n );\n }\n\n private sfreq(value: unknown) {\n if (\n value == 8 ||\n value == 11.025 ||\n value == 12 ||\n value == 16 ||\n value == 22.05 ||\n value == 24 ||\n value == 32 ||\n value == 44.1 ||\n value == 48\n ) {\n return [`-s`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'sfreq' is not in range of 8, 11.025, 12, 16, 22.05, 24, 32, 44.1 or 48.\",\n );\n }\n }\n\n private bitwidth(value: unknown) {\n if (value == 8 || value == 16 || value == 24 || value == 32) {\n return [`--bitwidth`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'sfreq' is not in range of 8, 16, 24 or 32.\",\n );\n }\n }\n\n private signed(value: unknown) {\n if (value == true) {\n return [`--signed`];\n } else {\n return undefined;\n }\n }\n\n private unsigned(value: unknown) {\n if (value == true) {\n return [`--unsigned`];\n } else {\n return undefined;\n }\n }\n\n private littleEndian(value: unknown) {\n if (value == true) {\n return [`--little-endian`];\n } else {\n return undefined;\n }\n }\n\n private bigEndian(value: unknown) {\n if (value == true) {\n return [`--big-endian`];\n } else {\n return undefined;\n }\n }\n\n private mp1Input(value: unknown) {\n if (value == true) {\n return [`--mp1input`];\n } else {\n return undefined;\n }\n }\n\n private mp2Input(value: unknown) {\n if (value == true) {\n return [`--mp2input`];\n } else {\n return undefined;\n }\n }\n\n private mp3Input(value: unknown) {\n if (value == true) {\n return [`--mp3input`];\n } else {\n return undefined;\n }\n }\n\n private mode(value: unknown) {\n if (\n value == \"s\" ||\n value == \"j\" ||\n value == \"f\" ||\n value == \"d\" ||\n value == \"m\" ||\n value == \"l\" ||\n value == \"r\" ||\n value == \"a\"\n ) {\n return [`-m`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'mode' is not in range of 's', 'j', 'f', 'd', 'm', 'l', 'r' or 'a'.\",\n );\n }\n }\n\n private toMono(value: unknown) {\n if (value == true) {\n return [`-a`];\n } else {\n return undefined;\n }\n }\n\n private channelDifferentBlockSize(value: unknown) {\n if (value == true) {\n return [`-d`];\n } else {\n return undefined;\n }\n }\n\n private freeformat(value: unknown) {\n if (value == null || value === false) {\n return undefined;\n }\n\n if (typeof value === \"string\") {\n if (\n value === \"FreeAmp\" ||\n value === \"in_mpg123\" ||\n value === \"l3dec\" ||\n value === \"LAME\" ||\n value === \"MAD\"\n ) {\n return [`--freeformat`];\n }\n\n throw new Error(\n \"lame: Invalid option: 'freeformat' string value must be one of 'FreeAmp', 'in_mpg123', 'l3dec', 'LAME', 'MAD'.\",\n );\n }\n\n if (value == true) {\n return [`--freeformat`];\n }\n\n throw new Error(\"lame: Invalid option: 'freeformat' must be boolean.\");\n }\n\n private disableInfoTag(value: unknown) {\n if (value == true) {\n return [`-t`];\n } else {\n return undefined;\n }\n }\n\n private nogap(value: unknown) {\n if (value == null) {\n return undefined;\n }\n\n if (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((item) => typeof item === \"string\" && item.trim() !== \"\")\n ) {\n return [`--nogap`, ...value];\n }\n\n throw new Error(\n \"lame: Invalid option: 'nogap' must be a non-empty array of file paths.\",\n );\n }\n\n private nogapout(value: unknown) {\n if (value == null) {\n return undefined;\n }\n\n if (typeof value === \"string\" && value.trim() !== \"\") {\n return [`--nogapout`, value];\n }\n\n throw new Error(\n \"lame: Invalid option: 'nogapout' must be a non-empty string path.\",\n );\n }\n\n private nogaptags(value: unknown) {\n if (value === undefined || value === false) {\n return undefined;\n }\n\n if (value == true) {\n return [`--nogaptags`];\n }\n\n throw new Error(\"lame: Invalid option: 'nogaptags' must be boolean.\");\n }\n\n private outDir(value: unknown) {\n if (value == null) {\n return undefined;\n }\n\n if (typeof value === \"string\" && value.trim() !== \"\") {\n return [`--out-dir`, value];\n }\n\n throw new Error(\n \"lame: Invalid option: 'out-dir' must be a non-empty string path.\",\n );\n }\n\n private comp(value: unknown) {\n return [`--comp`, String(value)];\n }\n\n private scale(value: unknown) {\n return [`--scale`, String(value)];\n }\n\n private scaleL(value: unknown) {\n return [`--scale-l`, String(value)];\n }\n\n private scaleR(value: unknown) {\n return [`--scale-r`, String(value)];\n }\n\n private replaygainFast(value: unknown) {\n if (value == true) {\n return [`--replaygain-fast`];\n } else {\n return undefined;\n }\n }\n\n private replaygainAccurate(value: unknown) {\n if (value == true) {\n return [`--replaygain-accurate`];\n } else {\n return undefined;\n }\n }\n\n private noreplaygain(value: unknown) {\n if (value == true) {\n return [`--noreplaygain`];\n } else {\n return undefined;\n }\n }\n\n private clipDetect(value: unknown) {\n if (value == true) {\n return [`--clipdetect`];\n } else {\n return undefined;\n }\n }\n\n private preset(value: unknown) {\n if (value == null) {\n return undefined;\n }\n\n if (typeof value === \"number\" && value >= 8 && value <= 640) {\n return [`--preset`, String(value)];\n }\n\n if (typeof value !== \"string\") {\n return this.invalidPreset();\n }\n\n const trimmed = value.trim();\n if (trimmed === \"\") {\n throw new Error(\"lame: Invalid option: 'preset' cannot be empty.\");\n }\n\n const tokens = trimmed.split(/\\s+/);\n const [first, second] = tokens;\n\n const singleValuePresets = [\n \"medium\",\n \"standard\",\n \"extreme\",\n \"insane\",\n \"phone\",\n \"phon+\",\n \"lw\",\n \"mw-eu\",\n \"mw-us\",\n \"voice\",\n \"fm\",\n \"radio\",\n \"hifi\",\n \"cd\",\n \"studio\",\n ];\n\n if (tokens.length === 1) {\n if (singleValuePresets.includes(first) || /^[0-9]+$/.test(first)) {\n return [`--preset`, first];\n }\n\n return this.invalidPreset();\n }\n\n if (\n tokens.length === 2 &&\n first === \"fast\" &&\n (second === \"medium\" ||\n second === \"standard\" ||\n second === \"extreme\" ||\n /^[0-9]+$/.test(second))\n ) {\n return [`--preset`, \"fast\", second];\n }\n\n if (tokens.length === 2 && first === \"cbr\" && /^[0-9]+$/.test(second)) {\n return [`--preset`, \"cbr\", second];\n }\n\n return this.invalidPreset();\n }\n\n private invalidPreset(): never {\n throw new Error(\n \"lame: Invalid option: 'preset' must be a supported preset keyword, numeric bitrate, or preset tuple like 'fast <value>' or 'cbr <bitrate>'.\",\n );\n }\n\n private noasm(value: unknown) {\n if (value == \"mmx\" || value == \"3dnow\" || value == \"sse\") {\n return [`--noasm`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'noasm' is not in range of 'mmx', '3dnow' or 'sse'.\",\n );\n }\n }\n\n private quality(value: unknown) {\n if (typeof value === \"number\" && value >= 0 && value <= 9) {\n return [`-q`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'quality' is not in range of 0 to 9.\",\n );\n }\n }\n\n private qualityHigh(value: unknown) {\n if (value == true) {\n return [`-h`];\n }\n\n if (value === undefined || value === false) {\n /* c8 ignore next */\n return undefined;\n }\n\n throw new Error(\n \"lame: Invalid option: 'quality-high' must be boolean.\",\n );\n }\n\n private fastEncoding(value: unknown) {\n if (value == true) {\n return [`-f`];\n }\n\n if (value === undefined || value === false) {\n return undefined;\n }\n\n throw new Error(\n \"lame: Invalid option: 'fast-encoding' must be boolean.\",\n );\n }\n\n private bitrate(value: unknown) {\n if (\n value == 8 ||\n value == 16 ||\n value == 24 ||\n value == 32 ||\n value == 40 ||\n value == 48 ||\n value == 56 ||\n value == 64 ||\n value == 80 ||\n value == 96 ||\n value == 112 ||\n value == 128 ||\n value == 144 ||\n value == 160 ||\n value == 192 ||\n value == 224 ||\n value == 256 ||\n value == 320\n ) {\n return [`-b`, String(value)];\n } else {\n throw new Error(\n \"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.\",\n );\n }\n }\n\n private maxBitrate(value: unknown) {\n if (\n value == 8 ||\n value == 16 ||\n value == 24 ||\n value == 32 ||\n value == 40 ||\n value == 48 ||\n value == 56 ||\n value == 64 ||\n value == 80 ||\n value == 96 ||\n value == 112 ||\n value == 128 ||\n value == 144 ||\n value == 160 ||\n value == 192 ||\n value == 224 ||\n value == 256 ||\n value == 320\n ) {\n return [`-B`, String(value)];\n } else if (value === undefined) {\n return undefined;\n }\n\n throw new Error(\n \"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.\",\n );\n }\n\n private forceBitrate(value: unknown) {\n if (value == true) {\n return [`-F`];\n } else {\n return undefined;\n }\n }\n\n private cbr(value: unknown) {\n if (value == true) {\n return [`--cbr`];\n } else {\n return undefined;\n }\n }\n\n private abr(value: unknown) {\n if (typeof value === \"number\" && value >= 8 && value <= 310) {\n return [`--abr`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'abr' is not in range of 8 to 310.\",\n );\n }\n }\n\n private vbr(value: unknown) {\n if (value == true) {\n return [`-v`];\n } else {\n return undefined;\n }\n }\n\n private vbrQuality(value: unknown) {\n if (typeof value === \"number\" && value >= 0 && value <= 9) {\n return [`-V`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'vbrQuality' is not in range of 0 to 9.\",\n );\n }\n }\n\n private vbrOld(value: unknown) {\n if (value == true) {\n return [`--vbr-old`];\n }\n\n if (value === undefined || value === false) {\n return undefined;\n }\n\n throw new Error(\n \"lame: Invalid option: 'vbr-old' must be boolean.\",\n );\n }\n\n private vbrNew(value: unknown) {\n if (value == true) {\n return [`--vbr-new`];\n }\n\n if (value === undefined || value === false) {\n return undefined;\n }\n\n throw new Error(\n \"lame: Invalid option: 'vbr-new' must be boolean.\",\n );\n }\n\n private ignoreNoiseInSfb21(value: unknown) {\n if (value == true) {\n return [`-Y`];\n } else {\n return undefined;\n }\n }\n\n private emp(value: unknown) {\n if (value == \"n\" || value == 5 || value == \"c\") {\n return [`-e`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'emp' is not in range of 'n', 5 or 'c'.\",\n );\n }\n }\n\n private markAsCopyrighted(value: unknown) {\n if (value == true) {\n return [`-c`];\n } else {\n return undefined;\n }\n }\n\n private markAsCopy(value: unknown) {\n if (value == true) {\n return [`-o`];\n } else {\n return undefined;\n }\n }\n\n private crcErrorProtection(value: unknown) {\n if (value == true) {\n return [`-p`];\n } else {\n return undefined;\n }\n }\n\n private nores(value: unknown) {\n if (value == true) {\n return [`--nores`];\n } else {\n return undefined;\n }\n }\n\n private strictlyEnforceIso(value: unknown) {\n if (value == true) {\n return [`--strictly-enforce-ISO`];\n } else {\n return undefined;\n }\n }\n\n private lowpass(value: unknown) {\n return [`--lowpass`, String(value)];\n }\n\n private lowpassWidth(value: unknown) {\n return [`--lowpass-width`, String(value)];\n }\n\n private highpass(value: unknown) {\n return [`--highpass`, String(value)];\n }\n\n private highpassWidth(value: unknown) {\n return [`--highpass-width`, String(value)];\n }\n\n private resample(value: unknown) {\n if (\n value == 8 ||\n value == 11.025 ||\n value == 12 ||\n value == 16 ||\n value == 22.05 ||\n value == 24 ||\n value == 32 ||\n value == 44.1 ||\n value == 48\n ) {\n return [`--resample`, String(value)];\n } else {\n throw new Error(\n \"lame: Invalid option: 'resample' is not in range of 8, 11.025, 12, 16, 22.05, 24, 32, 44.1 or 48.\",\n );\n }\n }\n\n private decodeMp3Delay(value: unknown) {\n if (value == null) {\n return undefined;\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return [`--decode-mp3delay`, String(value)];\n }\n\n throw new Error(\n \"lame: Invalid option: 'decode-mp3delay' must be a finite number.\",\n );\n }\n\n private priority(value: unknown) {\n if (value == null) {\n return undefined;\n }\n\n if (\n typeof value === \"number\" &&\n Number.isInteger(value) &&\n value >= 0 &&\n value <= 4\n ) {\n return [`--priority`, String(value as PriorityLevel)];\n }\n\n throw new Error(\n \"lame: Invalid option: 'priority' must be an integer between 0 and 4.\",\n );\n }\n\n private disptime(value: unknown) {\n if (value === false) {\n this.useDefaultDisptime = false;\n return undefined;\n }\n\n if (value == null) {\n return undefined;\n }\n\n if (typeof value === \"number\" && value > 0) {\n this.useDefaultDisptime = false;\n return [`--disptime`, String(value)];\n }\n\n throw new Error(\n \"lame: Invalid option: 'disptime' must be a positive number of seconds or false to disable progress output.\",\n );\n }\n\n private silent(value: unknown) {\n if (value === undefined || value === false) {\n return undefined;\n }\n\n if (value == true) {\n return [`--silent`];\n }\n\n throw new Error(\n \"lame: Invalid option: 'silent' must be boolean.\",\n );\n }\n\n private quiet(value: unknown) {\n if (value === undefined || value === false) {\n return undefined;\n }\n\n if (value == true) {\n return [`--quiet`];\n }\n\n throw new Error(\n \"lame: Invalid option: 'quiet' must be boolean.\",\n );\n }\n\n private verbose(value: unknown) {\n if (value === undefined || value === false) {\n return undefined;\n }\n\n if (value == true) {\n return [`--verbose`];\n }\n\n throw new Error(\n \"lame: Invalid option: 'verbose' must be boolean.\",\n );\n }\n\n private help(value: unknown) {\n return this.helpLike(\"--help\", value);\n }\n\n private usage(value: unknown) {\n re