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
Markdown
# TagLib-Wasm
[](https://github.com/CharlesWiltgen/TagLib-Wasm/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/taglib-wasm)
[](https://www.npmjs.com/package/taglib-wasm)
[](https://github.com/CharlesWiltgen/TagLib-Wasm/blob/main/LICENSE)
<br>[](https://www.typescriptlang.org/)
[](https://emscripten.org/)
[](https://webassembly.org/)
[](https://taglib.org/)
<br>[](https://nodejs.org/)
[](https://deno.land/)
[](https://bun.sh/)
[](https://workers.cloudflare.com/)
[](https://www.electronjs.org/)
[](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