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
-129
View File
@@ -6,16 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { isIso88591Compatible, textEncoder } from '../misc';
import { Writer } from '../writer';
import {
computeMp3FrameSize,
encodeSynchsafe,
getXingOffset,
KILOBIT_RATES,
XING,
} from '../../shared/mp3-misc';
import { Id3V2TextEncoding } from './mp3-reader';
export type XingFrameData = {
mpegVersionId: number;
@@ -39,28 +36,11 @@ export class Mp3Writer {
constructor(private writer: Writer) {}
writeU8(value: number) {
this.helper[0] = value;
this.writer.write(this.helper.subarray(0, 1));
}
writeU16(value: number) {
this.helperView.setUint16(0, value, false);
this.writer.write(this.helper.subarray(0, 2));
}
writeU32(value: number) {
this.helperView.setUint32(0, value, false);
this.writer.write(this.helper.subarray(0, 4));
}
writeAscii(text: string) {
for (let i = 0; i < text.length; i++) {
this.helper[i] = text.charCodeAt(i);
}
this.writer.write(this.helper.subarray(0, text.length));
}
writeXingFrame(data: XingFrameData) {
const startPos = this.writer.getPos();
@@ -137,113 +117,4 @@ export class Mp3Writer {
);
this.writer.seek(startPos + frameSize);
}
writeSynchsafeU32(value: number) {
this.writeU32(encodeSynchsafe(value));
}
writeIsoString(text: string) {
const bytes = new Uint8Array(text.length + 1);
for (let i = 0; i < text.length; i++) {
bytes[i] = text.charCodeAt(i);
}
bytes[text.length] = 0x00;
this.writer.write(bytes);
}
writeUtf8String(text: string) {
const utf8Data = textEncoder.encode(text);
this.writer.write(utf8Data);
this.writeU8(0x00);
}
writeId3V2TextFrame(frameId: string, text: string) {
const useIso88591 = isIso88591Compatible(text);
const textDataLength = useIso88591 ? text.length : textEncoder.encode(text).byteLength;
const frameSize = 1 + textDataLength + 1;
this.writeAscii(frameId);
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
if (useIso88591) {
this.writeIsoString(text);
} else {
this.writeUtf8String(text);
}
}
writeId3V2LyricsFrame(lyrics: string) {
const useIso88591 = isIso88591Compatible(lyrics);
const shortDescription = '';
const frameSize = 1 + 3 + shortDescription.length + 1 + lyrics.length + 1;
this.writeAscii('USLT');
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
this.writeAscii('und');
if (useIso88591) {
this.writeIsoString(shortDescription);
this.writeIsoString(lyrics);
} else {
this.writeUtf8String(shortDescription);
this.writeUtf8String(lyrics);
}
}
writeId3V2CommentFrame(comment: string) {
const useIso88591 = isIso88591Compatible(comment);
const textDataLength = useIso88591 ? comment.length : textEncoder.encode(comment).byteLength;
const shortDescription = '';
const frameSize = 1 + 3 + shortDescription.length + 1 + textDataLength + 1;
this.writeAscii('COMM');
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
this.writeU8(0x75); // 'u'
this.writeU8(0x6E); // 'n'
this.writeU8(0x64); // 'd'
if (useIso88591) {
this.writeIsoString(shortDescription);
this.writeIsoString(comment);
} else {
this.writeUtf8String(shortDescription);
this.writeUtf8String(comment);
}
}
writeId3V2ApicFrame(mimeType: string, pictureType: number, description: string, imageData: Uint8Array) {
const useIso88591 = isIso88591Compatible(mimeType) && isIso88591Compatible(description);
const descriptionDataLength = useIso88591 ? description.length : textEncoder.encode(description).byteLength;
const frameSize = 1 + mimeType.length + 1 + 1 + descriptionDataLength + 1 + imageData.byteLength;
this.writeAscii('APIC');
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
if (useIso88591) {
this.writeIsoString(mimeType);
} else {
this.writeUtf8String(mimeType);
}
this.writeU8(pictureType);
if (useIso88591) {
this.writeIsoString(description);
} else {
this.writeUtf8String(description);
}
this.writer.write(imageData);
}
}