mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
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:
co-authored by
David P.
parent
cab8319051
commit
6aee4e6a6a
+5
-136
@@ -6,8 +6,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { assert, assertNever, keyValueIterator, textEncoder, toDataView } from '../misc';
|
||||
import { metadataTagsAreEmpty, MetadataTags } from '../tags';
|
||||
import { assert, toDataView } from '../misc';
|
||||
import { metadataTagsAreEmpty } from '../tags';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack } from '../output';
|
||||
import { Mp3OutputFormat } from '../output-format';
|
||||
@@ -15,7 +15,7 @@ import { EncodedPacket } from '../packet';
|
||||
import { Writer } from '../writer';
|
||||
import { getXingOffset, INFO, readFrameHeader, XING } from '../../shared/mp3-misc';
|
||||
import { Mp3Writer, XingFrameData } from './mp3-writer';
|
||||
import { Id3V2TextEncoding } from './mp3-reader';
|
||||
import { Id3V2Writer } from '../id3';
|
||||
|
||||
export class Mp3Muxer extends Muxer {
|
||||
private format: Mp3OutputFormat;
|
||||
@@ -36,7 +36,8 @@ export class Mp3Muxer extends Muxer {
|
||||
|
||||
async start() {
|
||||
if (!metadataTagsAreEmpty(this.output._metadataTags)) {
|
||||
this.writeId3v2Tag(this.output._metadataTags);
|
||||
const id3Writer = new Id3V2Writer(this.writer);
|
||||
id3Writer.writeId3V2Tag(this.output._metadataTags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,138 +124,6 @@ export class Mp3Muxer extends Muxer {
|
||||
throw new Error('MP3 does not support subtitles.');
|
||||
}
|
||||
|
||||
writeId3v2Tag(tags: MetadataTags) {
|
||||
this.mp3Writer.writeAscii('ID3');
|
||||
this.mp3Writer.writeU8(0x04); // Version 2.4
|
||||
this.mp3Writer.writeU8(0x00); // Revision 0
|
||||
this.mp3Writer.writeU8(0x00); // Flags
|
||||
this.mp3Writer.writeSynchsafeU32(0); // Size placeholder
|
||||
|
||||
const startPos = this.writer.getPos();
|
||||
const writtenTags = new Set<string>();
|
||||
|
||||
for (const { key, value } of keyValueIterator(tags)) {
|
||||
switch (key) {
|
||||
case 'title': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TIT2', value);
|
||||
writtenTags.add('TIT2');
|
||||
}; break;
|
||||
|
||||
case 'description': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TIT3', value);
|
||||
writtenTags.add('TIT3');
|
||||
}; break;
|
||||
|
||||
case 'artist': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TPE1', value);
|
||||
writtenTags.add('TPE1');
|
||||
}; break;
|
||||
|
||||
case 'album': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TALB', value);
|
||||
writtenTags.add('TALB');
|
||||
}; break;
|
||||
|
||||
case 'albumArtist': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TPE2', value);
|
||||
writtenTags.add('TPE2');
|
||||
}; break;
|
||||
|
||||
case 'trackNumber': {
|
||||
const string = tags.tracksTotal !== undefined
|
||||
? `${value}/${tags.tracksTotal}`
|
||||
: value.toString();
|
||||
|
||||
this.mp3Writer.writeId3V2TextFrame('TRCK', string);
|
||||
writtenTags.add('TRCK');
|
||||
}; break;
|
||||
|
||||
case 'discNumber': {
|
||||
const string = tags.discsTotal !== undefined
|
||||
? `${value}/${tags.discsTotal}`
|
||||
: value.toString();
|
||||
|
||||
this.mp3Writer.writeId3V2TextFrame('TPOS', string);
|
||||
writtenTags.add('TPOS');
|
||||
}; break;
|
||||
|
||||
case 'genre': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TCON', value);
|
||||
writtenTags.add('TCON');
|
||||
}; break;
|
||||
|
||||
case 'date': {
|
||||
this.mp3Writer.writeId3V2TextFrame('TDRC', value.toISOString().slice(0, 10));
|
||||
writtenTags.add('TDRC');
|
||||
}; break;
|
||||
|
||||
case 'lyrics': {
|
||||
this.mp3Writer.writeId3V2LyricsFrame(value);
|
||||
writtenTags.add('USLT');
|
||||
}; break;
|
||||
|
||||
case 'comment': {
|
||||
this.mp3Writer.writeId3V2CommentFrame(value);
|
||||
writtenTags.add('COMM');
|
||||
}; break;
|
||||
|
||||
case 'images': {
|
||||
const pictureTypeMap = { coverFront: 0x03, coverBack: 0x04, unknown: 0x00 };
|
||||
for (const image of value) {
|
||||
const pictureType = pictureTypeMap[image.kind];
|
||||
const description = image.description ?? '';
|
||||
this.mp3Writer.writeId3V2ApicFrame(image.mimeType, pictureType, description, image.data);
|
||||
}
|
||||
}; break;
|
||||
|
||||
case 'tracksTotal':
|
||||
case 'discsTotal': {
|
||||
// Handled with trackNumber and discNumber respectively
|
||||
}; break;
|
||||
|
||||
case 'raw': {
|
||||
// Handled later
|
||||
}; break;
|
||||
|
||||
default: assertNever(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (tags.raw) {
|
||||
for (const key in tags.raw) {
|
||||
const value = tags.raw[key];
|
||||
if (value == null || key.length !== 4 || writtenTags.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bytes: Uint8Array;
|
||||
if (typeof value === 'string') {
|
||||
const encoded = textEncoder.encode(value);
|
||||
bytes = new Uint8Array(encoded.byteLength + 2);
|
||||
bytes[0] = Id3V2TextEncoding.UTF_8;
|
||||
bytes.set(encoded, 1);
|
||||
// Last byte is the null terminator
|
||||
} else if (value instanceof Uint8Array) {
|
||||
bytes = value;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.mp3Writer.writeAscii(key);
|
||||
this.mp3Writer.writeSynchsafeU32(bytes.byteLength);
|
||||
this.mp3Writer.writeU16(0x0000);
|
||||
this.writer.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
const endPos = this.writer.getPos();
|
||||
const framesSize = endPos - startPos;
|
||||
|
||||
this.writer.seek(6);
|
||||
this.mp3Writer.writeSynchsafeU32(framesSize);
|
||||
this.writer.seek(endPos);
|
||||
}
|
||||
|
||||
async finalize() {
|
||||
if (!this.xingFrameData || this.xingFramePos === null) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user