UNPKG

taglib-wasm

Version:

TagLib-Wasm is the universal tagging library for TypeScript/JavaScript platforms: Browsers, Node.js, Deno, Bun, Cloudflare Workers, and Electron apps

552 lines (418 loc) 21.4 kB
# TagLib-Wasm [![Tests](https://github.com/CharlesWiltgen/TagLib-Wasm/actions/workflows/ci.yml/badge.svg)](https://github.com/CharlesWiltgen/TagLib-Wasm/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/taglib-wasm.svg)](https://www.npmjs.com/package/taglib-wasm) [![npm downloads](https://img.shields.io/npm/dm/taglib-wasm.svg)](https://www.npmjs.com/package/taglib-wasm) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CharlesWiltgen/TagLib-Wasm/blob/main/LICENSE) <br>[![Built with](https://img.shields.io/badge/TypeScript-5-3178c6.svg?logo=typescript&logoColor=f5f5f5)](https://www.typescriptlang.org/) [![Built with Emscripten](https://img.shields.io/badge/Built%20with-Emscripten-4B9BFF.svg)](https://emscripten.org/) [![Built with WebAssembly](https://img.shields.io/badge/Built%20with-WebAssembly-654ff0.svg?logo=webassembly&logoColor=white)](https://webassembly.org/) [![Built with TagLib](https://img.shields.io/badge/Built%20with-TagLib-brightgreen.svg)](https://taglib.org/) <br>[![Node.js](https://img.shields.io/badge/Node.js-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![Deno](https://img.shields.io/badge/Deno-000000?logo=deno&logoColor=white)](https://deno.land/) [![Bun](https://img.shields.io/badge/Bun-000000?logo=bun&logoColor=white)](https://bun.sh/) [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white)](https://workers.cloudflare.com/) [![Electron (Node.js)](https://img.shields.io/badge/Electron%20%28Node.js%29-47848F?logo=electron&logoColor=white)](https://www.electronjs.org/) [![Browsers](https://img.shields.io/badge/Browsers-E34C26?logo=html5&logoColor=white)](https://html.spec.whatwg.org/multipage/) TagLib-Wasm is the **universal tagging library for TypeScript/JavaScript** (TS|JS) platforms: **Node.js**, **Deno**, **Bun**, **Cloudflare Workers**, **Electron** (via Node.js), and **browsers**. ## Features - **Local filesystem support** – On Node.js and Deno, WASI enables seek-based I/O that reads only headers and tags from disk — not entire files - **Automatic runtime optimization** – Auto-selects WASI (server) or Emscripten (browser) for optimal performance with no configuration - **Full audio format support** – Supports all audio formats supported by TagLib - **TypeScript first** – Complete type definitions and modern API - **Wide TS/JS runtime support** – Node.js, Deno, Bun, Electron (Node.js), Cloudflare Workers, and browsers - **Format abstraction** – Handles container format details automatically when possible - **Rich metadata** – Cover art, ratings, chapters (MP3 `CHAP`, MP4 QuickTime/Nero), and broadcast metadata (BWF `bext`/iXML for WAV/FLAC) - **Value fidelity** – Tag values round-trip verbatim: a `"3/12"` track or a zero-padded `"03"` is preserved, and MP4 freeform atoms keep their exact casing and vendor `mean`, so other tools still recognise them - **Zero dependencies** – Self-contained Wasm bundle - **Tested** – 448 tests across all formats, with cross-backend parity coverage - **Two API styles** – Use the "Simple" API (3 functions), or the full "Core" API for more advanced applications - **Batch folder operations** – Scan directories, process multiple files, find duplicates, and export metadata catalogs ## Installation ### Node.js ```bash npm install taglib-wasm ``` > **Note:** Requires Node.js v24 or higher (the Active LTS line) for WASI and > WebAssembly exception handling support. To consume the package as TypeScript source > (e.g., via `tsx`), see the > [installation guide](https://charleswiltgen.github.io/TagLib-Wasm/guide/installation.html). ### Deno ```typescript import { TagLib } from "@charlesw/taglib-wasm"; ``` ### Bun ```bash bun add taglib-wasm ``` ### Electron (Node.js) ```bash npm install taglib-wasm ``` taglib-wasm works in Electron's main process (which is Node.js). For the renderer process, expose metadata through IPC: ```typescript // Main process import { TagLib } from "taglib-wasm"; ``` See [Platform Examples](docs/guide/platform-examples.md#electron) for full IPC setup. ### Deno Compiled Binaries (Offline Support) For Deno compiled binaries that need to work offline, you can embed the WASM file: ```typescript // 1. Prepare your build by copying the WASM file import { prepareWasmForEmbedding } from "@charlesw/taglib-wasm"; await prepareWasmForEmbedding("./taglib-web.wasm"); // 2. In your application, use the helper for automatic handling import { initializeForDenoCompile } from "@charlesw/taglib-wasm"; const taglib = await initializeForDenoCompile(); // 3. Compile with the embedded WASM // deno compile --allow-read --include taglib-web.wasm myapp.ts ``` See the [complete Deno compile guide](https://charleswiltgen.github.io/TagLib-Wasm/guide/deno-compile.html) for more options including CDN loading. For manual control: ```typescript // Load embedded WASM in compiled binaries const wasmBinary = await Deno.readFile( new URL("./taglib-web.wasm", import.meta.url), ); const taglib = await TagLib.initialize({ wasmBinary }); ``` Supplying `wasmBinary` selects the Emscripten backend (the bytes above are `taglib-web.wasm`, its artifact). The WASI backend loads from a filesystem path or URL instead — use `wasmUrl` with it. ## Quick Start > **Import paths:** Deno uses `@charlesw/taglib-wasm`, npm uses `taglib-wasm`. > Examples below use npm paths — substitute accordingly. ### Simple API ```typescript import { applyTags, applyTagsToFile, readTags } from "taglib-wasm/simple"; // Read tags (string fields are arrays to support multi-value metadata) const tags = await readTags("song.mp3"); console.log(tags.title?.[0], tags.artist?.[0], tags.album?.[0]); // Apply tags and get modified buffer (in-memory) const modifiedBuffer = await applyTags("song.mp3", { title: "New Title", artist: "New Artist", album: "New Album", }); // Or update tags on disk (requires file path) await applyTagsToFile("song.mp3", { title: "New Title", artist: "New Artist", }); ``` ### High-Performance Batch Processing ```typescript import { readMetadataBatch, readTagsBatch } from "taglib-wasm/simple"; // Process multiple files in parallel const files = ["track01.mp3", "track02.mp3", /* ... */ "track20.mp3"]; // Read just tags (18x faster than sequential) const tags = await readTagsBatch(files, { concurrency: 8 }); // Read complete metadata including cover art detection (15x faster) const metadata = await readMetadataBatch(files, { concurrency: 8 }); // Real-world performance: // Sequential: ~100 seconds for 20 files // Batch: ~5 seconds for 20 files (20x speedup!) ``` ### Full API The Full API might be a better choice for apps and utilities focused on advanced metadata management. ```typescript import { TagLib } from "taglib-wasm"; // Initialize taglib-wasm const taglib = await TagLib.initialize(); // Load audio file (automatically cleaned up when scope exits) using file = await taglib.open("song.mp3"); // Read and update metadata const tag = file.tag(); tag.setTitle("New Title"); tag.setArtist("New Artist"); // Save changes file.save(); ``` ### Batch Folder Operations Process entire music collections efficiently: ```typescript import { findDuplicates, scanFolder } from "taglib-wasm"; // Scan a music library const result = await scanFolder("/path/to/music", { recursive: true, onProgress: (processed, total, file) => { console.log(`Processing ${processed}/${total}: ${file}`); }, }); console.log(`Found ${result.items.length} audio files`); console.log( `Successfully processed ${ result.items.filter((i) => i.status === "ok").length } files`, ); // Process results (narrow the ok/error union first) for (const file of result.items) { if (file.status !== "ok") continue; console.log( `${file.path}: ${file.tags.artist?.[0]} - ${file.tags.title?.[0]}`, ); console.log(`Duration: ${file.properties?.duration}s`); } // Find duplicates const duplicates = await findDuplicates("/path/to/music", { criteria: ["artist", "title"], }); console.log(`Found ${duplicates.length} groups of duplicates`); ``` ### Working with Cover Art ```typescript import { applyCoverArt, readCoverArt } from "taglib-wasm/simple"; // Extract cover art const coverData = await readCoverArt("song.mp3"); if (coverData) { await Deno.writeFile("cover.jpg", coverData); } // Set new cover art const imageData = await Deno.readFile("new-cover.jpg"); const modifiedBuffer = await applyCoverArt("song.mp3", imageData, "image/jpeg"); // Save modifiedBuffer to file if needed ``` ### Working with Ratings ```typescript import { RatingUtils, TagLib } from "taglib-wasm"; const taglib = await TagLib.initialize(); using file = await taglib.open("song.mp3"); // Read rating (normalized 0.0-1.0) const rating = file.getRating(); if (rating !== undefined) { console.log(`Rating: ${RatingUtils.toStars(rating)} stars`); } // Set rating (4 out of 5 stars) file.setRating(0.8); file.save(); ``` See the [Track Ratings Guide](https://charleswiltgen.github.io/TagLib-Wasm/guide/ratings.html) for RatingUtils API and cross-format conversion details. ### Working with Chapters ```typescript import { TagLib } from "taglib-wasm"; const taglib = await TagLib.initialize(); using file = await taglib.open("audiobook.m4b"); // Read chapters (ordered by start time) for (const ch of file.getChapters()) { console.log(`${ch.startTimeMs}–${ch.endTimeMs} ${ch.title} (${ch.source})`); } // Replace all chapters file.setChapters([ { startTimeMs: 0, title: "Intro" }, { startTimeMs: 95_000, title: "Chapter 1" }, ]); file.save(); ``` Chapters are read from ID3v2 `CHAP` frames (MP3) or, for MP4, a QuickTime chapter track (preferred when present) or a Nero `chpl` atom — each chapter reports its `source`. `setChapters()` supports MP3 and MP4 only; for MP4, `mp4ChapterStyle` (`"quicktime"` default, `"nero"`, or `"both"`) selects which structure(s) to write (the Nero atom is capped at 255 chapters). `endTimeMs` is explicit for ID3v2 chapters and inferred for MP4 (the next chapter's start, or the track duration for the last chapter). ### Broadcast metadata (BWF `bext` / iXML) ```typescript import { TagLib } from "taglib-wasm"; const taglib = await TagLib.initialize(); using file = await taglib.open("recording.wav"); const bext = file.getBext(); // parsed EBU 3285 bext chunk, or undefined console.log(bext?.description, bext?.timeReferenceSamples, bext?.codingHistory); console.log(file.getIxml()); // raw iXML string, or undefined file.setBext({ ...bext!, description: "Updated", version: 2, loudnessValueDb: -16, }); file.setIxml("<BWFXML>…</BWFXML>"); file.save(); ``` `getBext()` / `setBext()` (WAV and FLAC only) parse and serialize the BWF Broadcast Audio Extension chunk; `getBextData()` / `setBextData()` expose the raw chunk bytes for vendor extensions or malformed chunks, and `setBextData(null)` removes the chunk. iXML is passed through verbatim as a string (`setIxml(null)` removes it). The `bext` v2 loudness fields are EBU R128-style measurements, distinct from ReplayGain tags. `bwf.decodeBext` / `bwf.encodeBext` are also exported for working with raw `bext` bytes directly. ### Raw ID3v2 Frames (Escape Hatch) ```typescript import { TagLib } from "taglib-wasm"; const taglib = await TagLib.initialize(); using file = await taglib.open("song.mp3"); // Escape hatch: read/write raw ID3v2 frame bytes by ID (MP3 only) const frames = file.getId3v2Frames("TXXX"); // [{ id, data, flags? }] const rgadBody = new Uint8Array([/* raw frame body bytes */]); file.setId3v2Frames("RGAD", [rgadBody]); // replaces ALL RGAD frames file.removeId3v2Frames("NCON"); // removes every NCON frame file.save(); ``` `data` is the frame body without the 10-byte header; the caller owns the body encoding. Bytes round-trip verbatim for frames TagLib does not model. For TagLib-modeled IDs (`TIT2`, `APIC`, …): typed getters see a raw write only after save+reload; bytes may be normalized by later saves after that reload; and raw reads reflect persisted state plus pending raw writes — not pending typed edits (backend-dependent). A typed write to the same ID as an existing raw write is silently ignored until that raw frame is removed or the file is saved and reloaded — raw writes always win within a save. Frames with compression/encryption flags are not supported for write (writes emit zero flags). `flags` exists on the returned frames for forward compatibility, but reads never populate it today — TagLib always blanks header flags when re-rendering a frame. On both backends, a raw write to an ID3v1-mapped frame ID (`TIT2`, `TPE1`, `TALB`, `COMM`, `TCON`, `TDRC`, `TRCK`) suspends the usual ID3v1↔ID3v2 duplicate-sync on `save()` until that raw frame is removed. ### Container Format and Codec Detection ```typescript import { readProperties } from "taglib-wasm/simple"; // Get detailed audio properties including container and codec info const props = await readProperties("song.m4a"); console.log(props.containerFormat); // "MP4" (container format) console.log(props.codec); // "AAC" or "ALAC" (compressed media format) console.log(props.isLossless); // false for AAC, true for ALAC console.log(props.bitsPerSample); // 16 for most formats console.log(props.bitrate); // 256 (kbps) console.log(props.bitrateMode); // "CBR" | "VBR" | "ABR" | undefined (MP3 only) console.log(props.sampleRate); // 44100 (Hz) console.log(props.duration); // 180 (duration in seconds) ``` For Opus files, `audioProperties()` additionally exposes `outputGainDb` — the OpusHead output gain in decibels (RFC 7845). Players apply this unconditionally; it is separate from, and stacks with, ReplayGain / R128 tags, and is almost always `0`. Container format vs Codec: - **Container format** – How audio data and metadata are packaged (e.g., MP4, OGG) - **Codec** – How audio is compressed/encoded (e.g., AAC, Vorbis) Supported formats: - **MP4 container** (.mp4, .m4a) – Can contain AAC (lossy) or ALAC (lossless) - **OGG container** (.ogg) – Can contain Vorbis, Opus, FLAC, or Speex - **MP3** – Both container and codec (lossy) - **FLAC** – Both container and codec (lossless) - **WAV** – Container for PCM (uncompressed) audio - **AIFF** – Container for PCM (uncompressed) audio ## Documentation **[View Full Documentation](https://charleswiltgen.github.io/TagLib-Wasm/)** ### Getting Started - [Installation Guide](https://charleswiltgen.github.io/TagLib-Wasm/guide/installation.html) - [Quick Start Tutorial](https://charleswiltgen.github.io/TagLib-Wasm/guide/quick-start.html) - [All Examples](https://charleswiltgen.github.io/TagLib-Wasm/guide/examples.html) ### Guides - [API Reference](https://charleswiltgen.github.io/TagLib-Wasm/api/) - [Performance Guide](https://charleswiltgen.github.io/TagLib-Wasm/concepts/performance.html) - [Album Processing Guide](https://charleswiltgen.github.io/TagLib-Wasm/guide/album-processing.html) - [Platform Examples](https://charleswiltgen.github.io/TagLib-Wasm/guide/platform-examples.html) - [Working with Cover Art](https://charleswiltgen.github.io/TagLib-Wasm/guide/cover-art.html) - [Track Ratings](https://charleswiltgen.github.io/TagLib-Wasm/guide/ratings.html) - [Chapters](https://charleswiltgen.github.io/TagLib-Wasm/guide/chapters.html) - [Broadcast Metadata (BWF bext / iXML)](https://charleswiltgen.github.io/TagLib-Wasm/guide/broadcast-metadata.html) - [Cloudflare Workers](https://charleswiltgen.github.io/TagLib-Wasm/advanced/cloudflare-workers.html) - [Error Handling](https://charleswiltgen.github.io/TagLib-Wasm/concepts/error-handling.html) - [Contributing](CONTRIBUTING.md) - [AI Agent Documentation](AGENTS.md) ## Supported Formats `taglib-wasm` is designed to support all formats supported by TagLib: - **.mp3** – ID3v2 and ID3v1 tags - **.m4a/.mp4** – MPEG-4/AAC metadata for AAC and Apple Lossless audio - **.flac** – Vorbis comments and audio properties (plus BWF `bext`/iXML) - **.ogg** – Ogg Vorbis format with full metadata support - **.wav** – INFO chunk metadata, plus BWF `bext` and iXML - **Additional formats** – Opus, APE, MPC, WavPack, TrueAudio, AIFF, WMA, and more ## Performance and Best Practices ### Batch Processing for Multiple Files When processing multiple audio files, use the optimized batch APIs for better performance: ```typescript import { readMetadataBatch, readTagsBatch } from "taglib-wasm/simple"; // Processing files one by one (can take 90+ seconds for 19 files) for (const file of files) { const tags = await readTags(file); // Re-initializes for each file } // Batch processing (10-20x faster) const result = await readTagsBatch(files, { concurrency: 8, // Process 8 files in parallel onProgress: (processed, total) => { console.log(`${processed}/${total} files processed`); }, }); // Read complete metadata in one batch const metadata = await readMetadataBatch(files, { concurrency: 8 }); ``` **Performance comparison for 19 audio files:** - Sequential: ~90 seconds (4.7s per file) - Batch (concurrency=4): ~8 seconds (11x faster) - Batch (concurrency=8): ~5 seconds (18x faster) ### Smart Partial Loading For large audio files (>50MB), enable partial loading to reduce memory usage: ```typescript // Enable partial loading for large files using file = await taglib.open("large-concert.flac", { partial: true, maxHeaderSize: 2 * 1024 * 1024, // 2MB header maxFooterSize: 256 * 1024, // 256KB footer }); // Read operations work normally const tags = file.tag(); console.log(tags.title, tags.artist); // Smart save - automatically loads full file when needed await file.saveToFile(); // Full file loaded only here ``` **Performance gains:** - **500MB file**: ~450x less memory usage (1.1MB vs 500MB) - **Initial load**: 50x faster (50ms vs 2500ms) - **Memory peak**: 3.3MB instead of 1.5GB ### Runtime Optimization Tiers taglib-wasm auto-selects the fastest available backend — no configuration needed: | Environment | Backend | How it works | Performance | | ------------------------ | ----------------- | ------------------------------------------------------ | ----------- | | **Node.js / Deno / Bun** | WASI (auto) | Seek-based filesystem I/O; reads only headers and tags | Fastest | | **Browsers / Workers** | Emscripten (auto) | Entire file loaded into memory as buffer | Baseline | On Node.js, Deno, and Bun you get WASI automatically — nothing to configure. ## Runtime Compatibility `taglib-wasm` works across all major JavaScript runtimes: | Runtime | Status | Installation | Notes | | ---------------------- | ------- | ------------------------- | -------------------------------------------------------------------------------------------------- | | **Node.js** | Full | `npm install taglib-wasm` | TypeScript via tsx | | **Deno** | Full | `npm:taglib-wasm` | Native TypeScript | | **Bun** | Partial | `bun add taglib-wasm` | Import + init verified; full test suite is Deno-only | | **Browser** | Full | Via bundler | Full API support | | **Cloudflare Workers** | Full | `npm install taglib-wasm` | Buffer-based (Emscripten); no filesystem. See [Workers guide](docs/advanced/cloudflare-workers.md) | | **Electron** | Node.js | `npm install taglib-wasm` | Main process; renderer via IPC | ## Known Limitations - **Memory Usage (browsers)** – In browser environments, entire files are loaded into memory. On Node.js/Deno, WASI reads only headers and tags from disk. - **Concurrent Access** – Not thread-safe (JavaScript single-threaded nature mitigates this) ## Contributing Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. ## License This project uses dual licensing: - **TypeScript/JavaScript code** – MIT License (see [LICENSE](LICENSE)) - **WebAssembly binaries (taglib-web.wasm, taglib-wasi.wasm)** – LGPL-2.1-or-later (inherited from TagLib) The TagLib library is dual-licensed under LGPL/MPL. When compiled to WebAssembly, the resulting binary must comply with LGPL requirements. This means: - You can use taglib-wasm in commercial projects - If you modify the TagLib C++ code, you must share those changes - You must provide a way for users to relink with a modified TagLib For details, see [lib/taglib/COPYING.LGPL](lib/taglib/COPYING.LGPL) ## Acknowledgments - [TagLib](https://taglib.org/) – Excellent audio metadata library - [Emscripten](https://emscripten.org/) – WebAssembly compilation toolchain - [WASI](https://wasi.dev/) – WebAssembly System Interface for server-side runtimes