Fix WAV files with embedded ID3 metadata returning empty tags (#110)

* Read ID3v2 tags from WAV file

* remove js import

Co-authored-by: David P. <[email protected]>

* revert slicing in wave-demuxer.ts and add id3.ts for further sharing

* Add `writeId3Tag` to the `WavOutputFormat` constructor options

* add more context in private and public APIs that it's non-standard

* compare actual values in the test with real file

* remove unnecesarry double slice requesting

* trim test audio file (i tried my best to preserve ID3, including artwork)

* Couple of code cleanups

* const -> using

* Fix linting errors

---------

Co-authored-by: David P. <[email protected]>
This commit is contained in:
Igor Samokhovets
2025-09-22 14:06:06 +02:00
committed by GitHub
co-authored by David P.
parent cab8319051
commit 6aee4e6a6a
11 changed files with 1099 additions and 898 deletions
+18
View File
@@ -15,6 +15,7 @@ import { MetadataTags } from '../tags';
import { assert, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
import { parseId3V2Tag, readId3V2Header } from '../id3';
export enum WaveFormat {
PCM = 0x0001,
@@ -107,6 +108,8 @@ export class WaveDemuxer extends Demuxer {
totalFileSize = Math.min(riffChunkSize + 8, this.reader.fileSize ?? Infinity);
} else if (chunkId === 'LIST') {
await this.parseListChunk(startPos, chunkSize, littleEndian);
} else if (chunkId === 'ID3 ' || chunkId === 'id3 ') {
await this.parseId3Chunk(startPos, chunkSize);
}
currentPos = startPos + chunkSize + (chunkSize & 1); // Handle padding
@@ -271,6 +274,21 @@ export class WaveDemuxer extends Demuxer {
}
}
private async parseId3Chunk(startPos: number, size: number) {
// Parse ID3 tag embedded in WAV file (non-default, but used a lot in practice anyway)
let slice = this.reader.requestSlice(startPos, size);
if (slice instanceof Promise) slice = await slice;
if (!slice) return; // File too short
const id3V2Header = readId3V2Header(slice);
if (id3V2Header) {
// Extract the content portion (skip the 10-byte header)
const contentSlice = slice.slice(startPos + 10, id3V2Header.size);
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
}
}
getCodec(): AudioCodec | null {
assert(this.audioInfo);