diff --git a/README.md b/README.md index 3e0e61fd9..fe4a77c66 100644 --- a/README.md +++ b/README.md @@ -105,28 +105,26 @@ import * as mm from 'music-metadata/lib/core'; Dependency diagram: ```mermaid graph TD; - MM(music-metadata)-->S(strtok3) - MM-->TY(token-types) - MM-->FT(file-type) + MMN("music-metadata (Node.js entry point)")-->MMP + MMN-->FTN + MMP("music-metadata (primary entry point)")-->S(strtok3) + MMP-->TY(token-types) + MMP-->FTP + MMP-->UAE + FTN("file-type (Node.js entry point)")-->FTP + FTP("file-type (primary entry point)")-->S S(strtok3)-->P(peek-readable) - S-->TO("@tokenizer/token") - TY-->TO + S(strtok3)-->TO("@tokenizer/token") + TY(token-types)-->TO TY-->IE("ieee754") - FT-->RWNS(readable-web-to-node-stream) - FT-->S - FT-->TY - TY-->NB(node:buffer) - RWNS-->RS(readable-stream) - RS-->SD(string_decoder) - SD-->SB(safe-buffer) - RS-->UD(util-deprecate) - RS-->I(inherits) - style NB fill:#F88,stroke:#A44 - style SB fill:#F88,stroke:#A44 - style SD fill:#CCC,stroke:#888 + FTP-->TY + NS("node:stream") + FTN-->NS + FTP-->UAE(uint8array-extras) + style NS fill:#F88,stroke:#A44 style IE fill:#CCC,stroke:#888 - style UD fill:#CCC,stroke:#888 - style I fill:#CCC,stroke:#888 + style FTN fill:#FAA,stroke:#A44 + style MMN fill:#FAA,stroke:#A44 ``` Dependency list: diff --git a/doc/common_metadata.md b/doc/common_metadata.md index 2bc328c7f..f62a4fd45 100644 --- a/doc/common_metadata.md +++ b/doc/common_metadata.md @@ -21,7 +21,7 @@ The tag mapping is strongly inspired on the [MusicBrainz Picard tag-mapping](htt | genre | * | Genres | genre | TCO | TCON, TXXX:STYLE | TCON, TXXX:STYLE | ©GEN, GNRE | GENRE, STYLE | GENRE | WM/Genre | GNRE, IGNR | TRACK:GENRE | | | picture | * | Embedded cover art | | PIC | APIC | APIC | COVR | METADATA_BLOCK_PICTURE | COVER ART (FRONT), COVER ART (BACK) | WM/Picture | | PICTURE | | | composer | * | Composer | | TCM | TCOM | TCOM | ©WRT | COMPOSER | COMPOSER | WM/Composer | | | | -| lyrics | * | Lyricist | | | USLT:DESCRIPTION, SYLT | USLT:DESCRIPTION, SYLT | ©LYR | LYRICS | LYRICS | WM/Lyrics | | | | +| lyrics | * | Lyricist | | | USLT, SYLT | USLT, SYLT | ©LYR | LYRICS | LYRICS | WM/Lyrics | | | | | albumsort | 1 | Album title, formatted for alphabetic ordering | | TSA | TSOA | TSOA | SOAL | ALBUMSORT | ALBUMSORT | WM/AlbumSortOrder | | | | | titlesort | 1 | Track title, formatted for alphabetic ordering | | TST | TSOT | TSOT | SONM | TITLESORT | TITLESORT | WM/TitleSortOrder | | | | | work | 1 | The canonical title of the [work](https://musicbrainz.org/doc/Work) | | | | | ©WRK | WORK | WORK | WM/Work | | | | diff --git a/lib/ParserFactory.ts b/lib/ParserFactory.ts index 9e4904196..50b064606 100644 --- a/lib/ParserFactory.ts +++ b/lib/ParserFactory.ts @@ -2,7 +2,6 @@ import { fileTypeFromBuffer } from 'file-type'; import ContentType from 'content-type'; import MimeType from 'media-typer'; import initDebug from 'debug'; -import { Buffer } from 'node:buffer'; import { INativeMetadataCollector, MetadataCollector } from './common/MetadataCollector.js'; import { AIFFParser } from './aiff/AiffParser.js'; @@ -89,7 +88,7 @@ export class ParserFactory { // Parser could not be determined on MIME-type or extension debug('Guess parser on content...'); - const buf = Buffer.alloc(4100); + const buf = new Uint8Array(4100); await tokenizer.peekBuffer(buf, {mayBeLess: true}); if (tokenizer.fileInfo.path) { parserId = this.getParserIdForExtension(tokenizer.fileInfo.path); diff --git a/lib/aiff/AiffParser.ts b/lib/aiff/AiffParser.ts index 3c47c9202..124b946d1 100644 --- a/lib/aiff/AiffParser.ts +++ b/lib/aiff/AiffParser.ts @@ -115,9 +115,8 @@ export class AIFFParser extends BasicParser { public async readTextChunk(header: iff.IChunkHeader): Promise { const value = await this.tokenizer.readToken(new Token.StringType(header.chunkSize, 'ascii')); - value.split('\0').map(v => v.trim()).filter(v => v && v.length > 0).forEach(v => { - this.metadata.addTag('AIFF', header.chunkID, v.trim()); - }); + const values = value.split('\0').map(v => v.trim()).filter(v => v && v.length); + await Promise.all(values.map(v => this.metadata.addTag('AIFF', header.chunkID, v))); return header.chunkSize; } diff --git a/lib/aiff/AiffToken.ts b/lib/aiff/AiffToken.ts index 6602a4209..13ea1baff 100644 --- a/lib/aiff/AiffToken.ts +++ b/lib/aiff/AiffToken.ts @@ -1,10 +1,9 @@ import * as Token from 'token-types'; -import { Buffer } from 'node:buffer'; import { FourCcToken } from '../common/FourCC.js'; import * as iff from '../iff/index.js'; -import { IGetToken } from 'strtok3'; +import type { IGetToken } from 'strtok3'; /** * The Common Chunk. @@ -30,27 +29,27 @@ export class Common implements IGetToken { this.len = header.chunkSize; } - public get(buf: Buffer, off: number): ICommon { + public get(buf: Uint8Array, off: number): ICommon { // see: https://cycling74.com/forums/aiffs-80-bit-sample-rate-value - const shift = buf.readUInt16BE(off + 8) - 16398; - const baseSampleRate = buf.readUInt16BE(off + 8 + 2); + const shift = Token.UINT16_BE.get(buf, off + 8) - 16398; + const baseSampleRate = Token.UINT16_BE.get(buf, off + 8 + 2); const res: ICommon = { - numChannels: buf.readUInt16BE(off), - numSampleFrames: buf.readUInt32BE(off + 2), - sampleSize: buf.readUInt16BE(off + 6), + numChannels: Token.UINT16_BE.get(buf, off), + numSampleFrames: Token.UINT32_BE.get(buf, off + 2), + sampleSize: Token.UINT16_BE.get(buf, off + 6), sampleRate: shift < 0 ? baseSampleRate >> Math.abs(shift) : baseSampleRate << shift }; if (this.isAifc) { res.compressionType = FourCcToken.get(buf, off + 18); if (this.len > 22) { - const strLen = buf.readInt8(off + 22); + const strLen = Token.UINT8.get(buf, off + 22); if (strLen > 0) { const padding = (strLen + 1) % 2; if (23 + strLen + padding === this.len) { - res.compressionName = new Token.StringType(strLen, 'binary').get(buf, off + 23); + res.compressionName = new Token.StringType(strLen, 'latin1').get(buf, off + 23); } else { throw new Error('Illegal pstring length'); } diff --git a/lib/apev2/APEv2Parser.ts b/lib/apev2/APEv2Parser.ts index 6015bc594..ea6ae894c 100644 --- a/lib/apev2/APEv2Parser.ts +++ b/lib/apev2/APEv2Parser.ts @@ -1,7 +1,7 @@ import initDebug from 'debug'; import * as strtok3 from 'strtok3/core'; import { StringType } from 'token-types'; -import { Buffer } from 'node:buffer'; +import { uint8ArrayToString } from 'uint8array-extras'; import * as util from '../common/Util.js'; import { IOptions, IRandomReader, IApeHeader } from '../type.js'; @@ -56,7 +56,7 @@ export class APEv2Parser extends BasicParser { */ public static async findApeFooterOffset(reader: IRandomReader, offset: number): Promise { // Search for APE footer header at the end of the file - const apeBuf = Buffer.alloc(TagFooter.len); + const apeBuf = new Uint8Array(TagFooter.len); await reader.randomRead(apeBuf, 0, TagFooter.len, offset - TagFooter.len); const tagFooter = TagFooter.get(apeBuf, 0); if (tagFooter.ID === 'APETAGEX') { @@ -65,7 +65,7 @@ export class APEv2Parser extends BasicParser { } } - private static parseTagFooter(metadata: INativeMetadataCollector, buffer: Buffer, options: IOptions): Promise { + private static parseTagFooter(metadata: INativeMetadataCollector, buffer: Uint8Array, options: IOptions): Promise { const footer = TagFooter.get(buffer, buffer.length - TagFooter.len); if (footer.ID !== preamble) throw new Error('Unexpected APEv2 Footer ID preamble value.'); strtok3.fromBuffer(buffer); @@ -95,7 +95,7 @@ export class APEv2Parser extends BasicParser { if (this.tokenizer.fileInfo.size) { // Try to read the APEv2 header using just the footer-header const remaining = this.tokenizer.fileInfo.size - this.tokenizer.position; // ToDo: take ID3v1 into account - const buffer = Buffer.alloc(remaining); + const buffer = new Uint8Array(remaining); await this.tokenizer.readBuffer(buffer); return APEv2Parser.parseTagFooter(this.metadata, buffer, this.options); } @@ -117,7 +117,7 @@ export class APEv2Parser extends BasicParser { public async parseTags(footer: IFooter): Promise { - const keyBuffer = Buffer.alloc(256); // maximum tag key length + const keyBuffer = new Uint8Array(256); // maximum tag key length let bytesRemaining = footer.size - TagFooter.len; @@ -144,9 +144,7 @@ export class APEv2Parser extends BasicParser { const value = await this.tokenizer.readToken(new StringType(tagItemHeader.size, 'utf8')); const values = value.split(/\x00/g); - for (const val of values) { - this.metadata.addTag(tagFormat, key, val); - } + await Promise.all(values.map(val => this.metadata.addTag(tagFormat, key, val))); break; } @@ -154,14 +152,14 @@ export class APEv2Parser extends BasicParser { if (this.options.skipCovers) { await this.tokenizer.ignore(tagItemHeader.size); } else { - const picData = Buffer.alloc(tagItemHeader.size); + const picData = new Uint8Array(tagItemHeader.size); await this.tokenizer.readBuffer(picData); zero = util.findZero(picData, 0, picData.length); - const description = picData.toString('utf8', 0, zero); + const description = uint8ArrayToString(picData.slice(0, zero)); + const data = picData.slice(zero + 1); - const data = Buffer.from(picData.slice(zero + 1)); - this.metadata.addTag(tagFormat, key, { + await this.metadata.addTag(tagFormat, key, { description, data }); diff --git a/lib/apev2/APEv2Token.ts b/lib/apev2/APEv2Token.ts index 70c6e1a36..e27f977c1 100644 --- a/lib/apev2/APEv2Token.ts +++ b/lib/apev2/APEv2Token.ts @@ -1,6 +1,5 @@ import * as Token from 'token-types'; import { IGetToken } from 'strtok3/core'; -import { Buffer } from 'node:buffer'; import { FourCcToken } from '../common/FourCC.js'; @@ -158,7 +157,7 @@ export const Header: IGetToken = { export const TagFooter: IGetToken = { len: 32, - get: (buf: Buffer, off) => { + get: (buf, off) => { return { // should equal 'APETAGEX' ID: new Token.StringType(8, 'ascii').get(buf, off), diff --git a/lib/asf/AsfObject.ts b/lib/asf/AsfObject.ts index 1a9194007..32a306424 100644 --- a/lib/asf/AsfObject.ts +++ b/lib/asf/AsfObject.ts @@ -2,13 +2,13 @@ import * as Token from 'token-types'; import { IGetToken, ITokenizer } from 'strtok3/core'; -import { Buffer } from 'node:buffer'; import * as util from '../common/Util.js'; import { IPicture, ITag } from '../type.js'; import GUID from './GUID.js'; import { AsfUtil } from './AsfUtil.js'; import { AttachedPictureType } from '../id3v2/ID3v2Token.js'; +import { base64ToUint8Array } from 'uint8array-extras'; /** * Data Type: Specifies the type of information being stored. The following values are recognized. @@ -68,13 +68,13 @@ export interface IAsfTopLevelObjectHeader extends IAsfObjectHeader { * Token for: 3. ASF top-level Header Object * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3 */ -export const TopLevelHeaderObjectToken: IGetToken = { +export const TopLevelHeaderObjectToken: IGetToken = { len: 30, get: (buf, off): IAsfTopLevelObjectHeader => { return { - objectId: GUID.fromBin(new Token.BufferType(16).get(buf, off)), + objectId: GUID.fromBin(buf, off), objectSize: Number(Token.UINT64_LE.get(buf, off + 16)), numberOfHeaderObjects: Token.UINT32_LE.get(buf, off + 24) // Reserved: 2 bytes @@ -86,13 +86,13 @@ export const TopLevelHeaderObjectToken: IGetToken = { +export const HeaderObjectToken: IGetToken = { len: 24, get: (buf, off): IAsfObjectHeader => { return { - objectId: GUID.fromBin(new Token.BufferType(16).get(buf, off)), + objectId: GUID.fromBin(buf, off), objectSize: Number(Token.UINT64_LE.get(buf, off + 16)) }; } @@ -106,7 +106,7 @@ export abstract class State implements IGetToken { this.len = Number(header.objectSize) - HeaderObjectToken.len; } - public abstract get(buf: Buffer, off: number): T; + public abstract get(buf: Uint8Array, off: number): T; protected postProcessTag(tags: ITag[], name: string, valueType: number, data: any) { if (name === 'WM/Picture') { @@ -128,7 +128,7 @@ export class IgnoreObjectState extends State { super(header); } - public get(buf: Buffer, off: number): null { + public get(buf: Uint8Array, off: number): null { return null; } } @@ -245,7 +245,7 @@ export class FilePropertiesObject extends State { super(header); } - public get(buf: Buffer, off: number): IFilePropertiesObject { + public get(buf: Uint8Array, off: number): IFilePropertiesObject { return { fileId: GUID.fromBin(buf, off), @@ -296,7 +296,7 @@ export class StreamPropertiesObject extends State { super(header); } - public get(buf: Buffer, off: number): IStreamPropertiesObject { + public get(buf: Uint8Array, off: number): IStreamPropertiesObject { return { streamType: GUID.decodeMediaType(GUID.fromBin(buf, off)), @@ -326,11 +326,12 @@ export class HeaderExtensionObject implements IGetToken this.len = 22; } - public get(buf: Buffer, off: number): IHeaderExtensionObject { + public get(buf: Uint8Array, off: number): IHeaderExtensionObject { + const view = new DataView(buf.buffer, off); return { reserved1: GUID.fromBin(buf, off), - reserved2: buf.readUInt16LE(off + 16), - extensionDataSize: buf.readUInt32LE(off + 18) + reserved2: view.getUint16(16, true), + extensionDataSize: view.getUint16(18, true) }; } } @@ -348,9 +349,10 @@ interface ICodecListObjectHeader { */ const CodecListObjectHeader: IGetToken = { len: 20, - get: (buf: Buffer, off: number): ICodecListObjectHeader => { + get: (buf: Uint8Array, off: number): ICodecListObjectHeader => { + const view = new DataView(buf.buffer, off); return { - entryCount: buf.readUInt16LE(off + 16) + entryCount: view.getUint16(16, true) }; } }; @@ -362,12 +364,12 @@ export interface ICodecEntry { }, codecName: string, description: string, - information: Buffer + information: Uint8Array } async function readString(tokenizer: ITokenizer): Promise { const length = await tokenizer.readNumber(Token.UINT16_LE); - return (await tokenizer.readToken(new Token.StringType(length * 2, 'utf16le'))).replace('\0', ''); + return (await tokenizer.readToken(new Token.StringType(length * 2, 'utf-16le'))).replace('\0', ''); } /** @@ -383,9 +385,9 @@ export async function readCodecEntries(tokenizer: ITokenizer): Promise { +async function readInformation(tokenizer: ITokenizer): Promise { const length = await tokenizer.readNumber(Token.UINT16_LE); - const buf = Buffer.alloc(length); + const buf = new Uint8Array(length); await tokenizer.readBuffer(buf); return buf; } @@ -421,17 +423,17 @@ export class ContentDescriptionObjectState extends State { super(header); } - public get(buf: Buffer, off: number): ITag[] { - + public get(buf: Uint8Array, off: number): ITag[] { const tags: ITag[] = []; - let pos = off + 10; + const view = new DataView(buf.buffer, off); + let pos = 10; for (let i = 0; i < ContentDescriptionObjectState.contentDescTags.length; ++i) { - const length = buf.readUInt16LE(off + i * 2); + const length = view.getUint16(i * 2, true); if (length > 0) { const tagName = ContentDescriptionObjectState.contentDescTags[i]; const end = pos + length; - tags.push({id: tagName, value: AsfUtil.parseUnicodeAttr(buf.slice(pos, end))}); + tags.push({id: tagName, value: AsfUtil.parseUnicodeAttr(buf.slice(off + pos, off + end))}); pos = end; } } @@ -451,20 +453,21 @@ export class ExtendedContentDescriptionObjectState extends State { super(header); } - public get(buf: Buffer, off: number): ITag[] { + public get(buf: Uint8Array, off: number): ITag[] { const tags: ITag[] = []; - const attrCount = buf.readUInt16LE(off); - let pos = off + 2; + const view = new DataView(buf.buffer, off); + const attrCount = view.getUint16(0, true); + let pos = 2; for (let i = 0; i < attrCount; i += 1) { - const nameLen = buf.readUInt16LE(pos); + const nameLen = view.getUint16(pos, true); pos += 2; - const name = AsfUtil.parseUnicodeAttr(buf.slice(pos, pos + nameLen)); + const name = AsfUtil.parseUnicodeAttr(buf.slice(off + pos, off + pos + nameLen)); pos += nameLen; - const valueType = buf.readUInt16LE(pos); + const valueType = view.getUint16(pos, true); pos += 2; - const valueLen = buf.readUInt16LE(pos); + const valueLen = view.getUint16(pos, true); pos += 2; - const value = buf.slice(pos, pos + valueLen); + const value = buf.slice(off + pos, off + pos + valueLen); pos += valueLen; this.postProcessTag(tags, name, valueType, value); } @@ -518,28 +521,29 @@ export class ExtendedStreamPropertiesObjectState extends State { public get(uint8Array: Uint8Array, off: number): ITag[] { const tags: ITag[] = []; - const buf = Buffer.from(uint8Array); - const descriptionRecordsCount = buf.readUInt16LE(off); - let pos = off + 2; + + const view = new DataView(uint8Array.buffer, off); + const descriptionRecordsCount = view.getUint16(0, true); + let pos =2; for (let i = 0; i < descriptionRecordsCount; i += 1) { pos += 4; - const nameLen = buf.readUInt16LE(pos); + const nameLen = view.getUint16(pos, true); pos += 2; - const dataType = buf.readUInt16LE(pos); + const dataType = view.getUint16(pos, true); pos += 2; - const dataLen = buf.readUInt32LE(pos); + const dataLen = view.getUint32(pos, true); pos += 4; - const name = AsfUtil.parseUnicodeAttr(buf.slice(pos, pos + nameLen)); + const name = AsfUtil.parseUnicodeAttr(uint8Array.slice(off + pos, off + pos + nameLen)); pos += nameLen; - const data = buf.slice(pos, pos + dataLen); + const data = uint8Array.slice(off + pos, off + pos + dataLen); pos += dataLen; this.postProcessTag(tags, name, dataType, data); } @@ -596,7 +601,7 @@ export interface IWmPicture extends IPicture { format: string, description: string, size: number - data: Buffer; + data: Uint8Array; } /** @@ -605,10 +610,10 @@ export interface IWmPicture extends IPicture { export class WmPictureToken implements IGetToken { public static fromBase64(base64str: string): IPicture { - return this.fromBuffer(Buffer.from(base64str, 'base64')); + return this.fromBuffer(base64ToUint8Array(base64str)); } - public static fromBuffer(buffer: Buffer): IWmPicture { + public static fromBuffer(buffer: Uint8Array): IWmPicture { const pic = new WmPictureToken(buffer.length); return pic.get(buffer, 0); } @@ -616,21 +621,22 @@ export class WmPictureToken implements IGetToken { constructor(public len) { } - public get(buffer: Buffer, offset: number): IWmPicture { + public get(buffer: Uint8Array, offset: number): IWmPicture { + const view = new DataView(buffer.buffer, offset); - const typeId = buffer.readUInt8(offset++); - const size = buffer.readInt32LE(offset); + const typeId = view.getUint8(0); + const size = view.getInt32(1, true); let index = 5; - while (buffer.readUInt16BE(index) !== 0) { + while (view.getUint16(index) !== 0) { index += 2; } - const format = buffer.slice(5, index).toString('utf16le'); + const format = new Token.StringType(index - 5, 'utf-16le').get(buffer, 5); - while (buffer.readUInt16BE(index) !== 0) { + while (view.getUint16(index) !== 0) { index += 2; } - const description = buffer.slice(5, index).toString('utf16le'); + const description = new Token.StringType(index - 5, 'utf-16le').get(buffer, 5); return { type: AttachedPictureType[typeId], diff --git a/lib/asf/AsfParser.ts b/lib/asf/AsfParser.ts index 4c2bae8d8..8aaee8290 100644 --- a/lib/asf/AsfParser.ts +++ b/lib/asf/AsfParser.ts @@ -60,12 +60,12 @@ export class AsfParser extends BasicParser { case AsfObject.ContentDescriptionObjectState.guid.str: // 3.10 tags = await this.tokenizer.readToken(new AsfObject.ContentDescriptionObjectState(header)); - this.addTags(tags); + await this.addTags(tags); break; case AsfObject.ExtendedContentDescriptionObjectState.guid.str: // 3.11 tags = await this.tokenizer.readToken(new AsfObject.ExtendedContentDescriptionObjectState(header)); - this.addTags(tags); + await this.addTags(tags); break; case GUID.CodecListObject.str: @@ -100,10 +100,8 @@ export class AsfParser extends BasicParser { // done } - private addTags(tags: ITag[]) { - tags.forEach(tag => { - this.metadata.addTag(headerType, tag.id, tag.value); - }); + private async addTags(tags: ITag[]): Promise { + await Promise.all(tags.map(({ id, value }) => this.metadata.addTag(headerType, id, value))); } private async parseExtensionObject(extensionSize: number): Promise { @@ -122,12 +120,12 @@ export class AsfParser extends BasicParser { case AsfObject.MetadataObjectState.guid.str: // 4.7 const moTags = await this.tokenizer.readToken(new AsfObject.MetadataObjectState(header)); - this.addTags(moTags); + await this.addTags(moTags); break; case AsfObject.MetadataLibraryObjectState.guid.str: // 4.8 const mlTags = await this.tokenizer.readToken(new AsfObject.MetadataLibraryObjectState(header)); - this.addTags(mlTags); + await this.addTags(mlTags); break; case GUID.PaddingObject.str: diff --git a/lib/asf/AsfUtil.ts b/lib/asf/AsfUtil.ts index 8337877ab..42e41964e 100644 --- a/lib/asf/AsfUtil.ts +++ b/lib/asf/AsfUtil.ts @@ -1,10 +1,9 @@ import * as Token from 'token-types'; -import { Buffer } from 'node:buffer'; import * as util from '../common/Util.js'; import { DataType } from './AsfObject.js'; -export type AttributeParser = (buf: Buffer) => boolean | string | number | bigint | Buffer; +export type AttributeParser = (buf: Uint8Array) => boolean | string | number | bigint | Uint8Array; export class AsfUtil { @@ -13,7 +12,7 @@ export class AsfUtil { } public static parseUnicodeAttr(uint8Array: Uint8Array): string { - return util.stripNulls(util.decodeString(uint8Array, 'utf16le')); + return util.stripNulls(util.decodeString(uint8Array, 'utf-16le')); } private static attributeParsers: AttributeParser[] = [ @@ -26,23 +25,23 @@ export class AsfUtil { AsfUtil.parseByteArrayAttr ]; - private static parseByteArrayAttr(buf: Uint8Array): Buffer { - return Buffer.from(buf); + private static parseByteArrayAttr(buf: Uint8Array): Uint8Array { + return new Uint8Array(buf); } - private static parseBoolAttr(buf: Buffer, offset: number = 0): boolean { + private static parseBoolAttr(buf: Uint8Array, offset: number = 0): boolean { return AsfUtil.parseWordAttr(buf, offset) === 1; } - private static parseDWordAttr(buf: Buffer, offset: number = 0): number { - return buf.readUInt32LE(offset); + private static parseDWordAttr(buf: Uint8Array, offset: number = 0): number { + return Token.UINT32_LE.get(buf, offset); } - private static parseQWordAttr(buf: Buffer, offset: number = 0): bigint { + private static parseQWordAttr(buf: Uint8Array, offset: number = 0): bigint { return Token.UINT64_LE.get(buf, offset); } - private static parseWordAttr(buf: Buffer, offset: number = 0): number { - return buf.readUInt16LE(offset); + private static parseWordAttr(buf: Uint8Array, offset: number = 0): number { + return Token.UINT16_LE.get(buf, offset); } } diff --git a/lib/asf/GUID.ts b/lib/asf/GUID.ts index a14f50de8..2522a6028 100644 --- a/lib/asf/GUID.ts +++ b/lib/asf/GUID.ts @@ -1,3 +1,5 @@ +import {hexToUint8Array, uint8ArrayToHex} from 'uint8array-extras'; + /** * Ref: * - https://tools.ietf.org/html/draft-fleischman-asf-01, Appendix A: ASF GUIDs @@ -66,7 +68,7 @@ export default class GUID { public static ASF_Index_Placeholder_Object = new GUID("D9AADE20-7C17-4F9C-BC28-8555DD98E2A2"); - public static fromBin(bin: Buffer, offset: number = 0) { + public static fromBin(bin: Uint8Array, offset: number = 0) { return new GUID(this.decode(bin, offset)); } @@ -76,12 +78,13 @@ export default class GUID { * @param offset Read offset in bytes, default 0 * @returns GUID as dashed hexadecimal representation */ - public static decode(objectId: Buffer, offset: number = 0): string { - const guid = objectId.readUInt32LE(offset).toString(16) + "-" + - objectId.readUInt16LE(offset + 4).toString(16) + "-" + - objectId.readUInt16LE(offset + 6).toString(16) + "-" + - objectId.readUInt16BE(offset + 8).toString(16) + "-" + - objectId.slice(offset + 10, offset + 16).toString('hex'); + public static decode(objectId: Uint8Array, offset: number = 0): string { + const view = new DataView(objectId.buffer, offset); + const guid = view.getUint32(0, true).toString(16) + "-" + + view.getUint16(4, true).toString(16) + "-" + + view.getUint16(6, true).toString(16) + "-" + + view.getUint16(8).toString(16) + "-" + + uint8ArrayToHex(objectId.slice(offset + 10, offset + 16)); return guid.toUpperCase(); } @@ -107,13 +110,14 @@ export default class GUID { * @param guid GUID like: "B503BF5F-2EA9-CF11-8EE3-00C00C205365" * @returns Encoded Binary GUID */ - public static encode(str: string): Buffer { - const bin = Buffer.alloc(16); - bin.writeUInt32LE(parseInt(str.slice(0, 8), 16), 0); - bin.writeUInt16LE(parseInt(str.slice(9, 13), 16), 4); - bin.writeUInt16LE(parseInt(str.slice(14, 18), 16), 6); - Buffer.from(str.slice(19, 23), "hex").copy(bin, 8); - Buffer.from(str.slice(24), "hex").copy(bin, 10); + public static encode(str: string): Uint8Array { + const bin = new Uint8Array(16); + const view = new DataView(bin.buffer); + view.setUint32(0, parseInt(str.slice(0, 8), 16), true); + view.setUint16(4, parseInt(str.slice(9, 13), 16), true); + view.setUint16(6, parseInt(str.slice(14, 18), 16), true); + bin.set(hexToUint8Array(str.slice(19, 23)), 8); + bin.set(hexToUint8Array(str.slice(24)), 10); return bin; } @@ -125,7 +129,7 @@ export default class GUID { return this.str === guid.str; } - public toBin(): Buffer { + public toBin(): Uint8Array { return GUID.encode(this.str); } diff --git a/lib/common/FourCC.ts b/lib/common/FourCC.ts index 8e857cda5..790999af5 100644 --- a/lib/common/FourCC.ts +++ b/lib/common/FourCC.ts @@ -1,4 +1,5 @@ import { IToken } from 'strtok3/core'; +import { stringToUint8Array, uint8ArrayToString } from 'uint8array-extras'; import * as util from './Util.js'; @@ -11,18 +12,19 @@ const validFourCC = /^[\x21-\x7e©][\x20-\x7e\x00()]{3}/; export const FourCcToken: IToken = { len: 4, - get: (buf: Buffer, off: number): string => { - const id = buf.toString('binary', off, off + FourCcToken.len); + get: (buf: Uint8Array, off: number): string => { + const id = uint8ArrayToString(buf.slice(off, off + FourCcToken.len), 'latin1'); if (!id.match(validFourCC)) { throw new Error(`FourCC contains invalid characters: ${util.a2hex(id)} "${id}"`); } return id; }, - put: (buffer: Buffer, offset: number, id: string) => { - const str = Buffer.from(id, 'binary'); + put: (buffer: Uint8Array, offset: number, id: string) => { + const str = stringToUint8Array(id); if (str.length !== 4) throw new Error('Invalid length'); - return str.copy(buffer, offset); + buffer.set(str, offset); + return offset + 4; } }; diff --git a/lib/common/MetadataCollector.ts b/lib/common/MetadataCollector.ts index ec0c87c0c..c2bb11d12 100644 --- a/lib/common/MetadataCollector.ts +++ b/lib/common/MetadataCollector.ts @@ -47,7 +47,7 @@ export interface INativeMetadataCollector extends IWarningCollector { setFormat(key: FormatId, value: any): void; - addTag(tagType: TagType, tagId: string, value: any): void; + addTag(tagType: TagType, tagId: string, value: any): Promise; addStreamInfo(streamInfo: ITrackInfo): void; } @@ -121,7 +121,7 @@ export class MetadataCollector implements INativeMetadataCollector { } } - public addTag(tagType: TagType, tagId: string, value: any) { + public async addTag(tagType: TagType, tagId: string, value: any): Promise { debug(`tag ${tagType}.${tagId} = ${value}`); if (!this.native[tagType]) { this.format.tagTypes.push(tagType); @@ -129,14 +129,14 @@ export class MetadataCollector implements INativeMetadataCollector { } this.native[tagType].push({id: tagId, value}); - this.toCommon(tagType, tagId, value); + await this.toCommon(tagType, tagId, value); } public addWarning(warning: string) { this.quality.warnings.push({message: warning}); } - public postMap(tagType: TagType | 'artificial', tag: IGenericTag) { + public async postMap(tagType: TagType | 'artificial', tag: IGenericTag): Promise { // Common tag (alias) found @@ -171,13 +171,12 @@ export class MetadataCollector implements INativeMetadataCollector { break; case 'picture': - this.postFixPicture(tag.value as IPicture).then(picture => { + return this.postFixPicture(tag.value as IPicture).then(picture => { if (picture !== null) { tag.value = picture; this.setGenericTag(tagType, tag); } }); - return; case 'totaltracks': this.common.track.of = CommonTagMapper.toIntOrNull(tag.value); @@ -278,10 +277,10 @@ export class MetadataCollector implements INativeMetadataCollector { * Fix some common issues with picture object * @param picture Picture */ - private async postFixPicture(picture: IPicture): Promise { + private async postFixPicture(picture: IPicture): Promise { if (picture.data && picture.data.length > 0) { if (!picture.format) { - const fileType = await fileTypeFromBuffer(picture.data); + const fileType = await fileTypeFromBuffer(Uint8Array.from(picture.data)); // ToDO: remove Buffer if (fileType) { picture.format = fileType.mime; } else { @@ -302,14 +301,14 @@ export class MetadataCollector implements INativeMetadataCollector { /** * Convert native tag to common tags */ - private toCommon(tagType: TagType, tagId: string, value: any) { + private async toCommon(tagType: TagType, tagId: string, value: any): Promise { const tag = {id: tagId, value}; const genericTag = this.tagMapper.mapTag(tagType, tag, this); if (genericTag) { - this.postMap(tagType, genericTag); + await this.postMap(tagType, genericTag); } } diff --git a/lib/common/Util.ts b/lib/common/Util.ts index c844c5f1b..79e2a7e99 100644 --- a/lib/common/Util.ts +++ b/lib/common/Util.ts @@ -1,10 +1,11 @@ +import { StringType } from 'token-types'; import { IRatio } from '../type.js'; export type StringEncoding = - 'ascii' // Use 'utf-8' or latin1 instead + 'ascii' // Use 'utf-8' or latin1 instead | 'utf8' // alias: 'utf-8' - | 'utf16le' // alias: 'ucs2', 'ucs-2' - | 'ucs2' // 'utf16le' + | 'utf-16le' // alias: 'ucs2', 'ucs-2' + | 'ucs2' // 'utf-16le' | 'base64url' | 'latin1' // Same as ISO-8859-1 (alias: 'binary') | 'hex'; @@ -28,7 +29,7 @@ export function getBit(buf: Uint8Array, off: number, bit: number): boolean { */ export function findZero(uint8Array: Uint8Array, start: number, end: number, encoding?: StringEncoding): number { let i = start; - if (encoding === 'utf16le') { + if (encoding === 'utf-16le') { while (uint8Array[i] !== 0 || uint8Array[i + 1] !== 0) { if (i >= end) return end; i += 2; @@ -68,13 +69,13 @@ export function decodeString(uint8Array: Uint8Array, encoding: StringEncoding): // https://github.com/leetreveil/musicmetadata/issues/84 if (uint8Array[0] === 0xFF && uint8Array[1] === 0xFE) { // little endian return decodeString(uint8Array.subarray(2), encoding); - } else if (encoding === 'utf16le' && uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) { + } else if (encoding === 'utf-16le' && uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) { // BOM, indicating big endian decoding if ((uint8Array.length & 1) !== 0) throw new Error('Expected even number of octets for 16-bit unicode string'); return decodeString(swapBytes(uint8Array), encoding); } - return Buffer.from(uint8Array).toString(encoding); + return new StringType(uint8Array.length, encoding).get(uint8Array, 0); } export function stripNulls(str: string): string { diff --git a/lib/flac/FlacParser.ts b/lib/flac/FlacParser.ts index 1edc36b11..ad251686f 100644 --- a/lib/flac/FlacParser.ts +++ b/lib/flac/FlacParser.ts @@ -68,7 +68,7 @@ export class FlacParser extends AbstractID3Parser { } } - private parseDataBlock(blockHeader: IBlockHeader): Promise { + private async parseDataBlock(blockHeader: IBlockHeader): Promise { debug(`blockHeader type=${blockHeader.type}, length=${blockHeader.length}`); switch (blockHeader.type) { case BlockType.STREAMINFO: @@ -85,7 +85,8 @@ export class FlacParser extends AbstractID3Parser { case BlockType.CUESHEET: break; case BlockType.PICTURE: - return this.parsePicture(blockHeader.length).then(); + await this.parsePicture(blockHeader.length); + return; default: this.metadata.addWarning('Unknown block type: ' + blockHeader.type); } @@ -122,10 +123,11 @@ export class FlacParser extends AbstractID3Parser { const decoder = new VorbisDecoder(data, 0); decoder.readStringUtf8(); // vendor (skip) const commentListLength = decoder.readInt32(); + const tags = new Array(commentListLength); for (let i = 0; i < commentListLength; i++) { - const tag = decoder.parseUserComment(); - this.vorbisParser.addTag(tag.key, tag.value); + tags[i] = decoder.parseUserComment(); } + await Promise.all(tags.map(tag => this.vorbisParser.addTag(tag.key, tag.value))); } private async parsePicture(dataLen: number) { diff --git a/lib/id3v1/ID3v1Parser.ts b/lib/id3v1/ID3v1Parser.ts index ce9350544..409c61c5f 100644 --- a/lib/id3v1/ID3v1Parser.ts +++ b/lib/id3v1/ID3v1Parser.ts @@ -72,7 +72,7 @@ const Iid3v1Token: IGetToken = { * @param off Offset in buffer in bytes * @returns ID3v1.1 header if first 3 bytes equals 'TAG', otherwise null is returned */ - get: (buf: Buffer, off): IId3v1Header => { + get: (buf: Uint8Array, off): IId3v1Header => { const header = new Id3v1StringType(3).get(buf, off); return header === 'TAG' ? { header, @@ -93,10 +93,10 @@ const Iid3v1Token: IGetToken = { class Id3v1StringType extends StringType { constructor(len: number) { - super(len, 'binary'); + super(len, 'latin1'); } - public get(buf: Buffer, off: number): string { + public get(buf: Uint8Array, off: number): string { let value = super.get(buf, off); value = util.trimRightNull(value); value = value.trim(); @@ -137,18 +137,18 @@ export class ID3v1Parser extends BasicParser { debug('ID3v1 header found at: pos=%s', this.tokenizer.fileInfo.size - Iid3v1Token.len); for (const id of ['title', 'artist', 'album', 'comment', 'track', 'year']) { if (header[id] && header[id] !== '') - this.addTag(id, header[id]); + await this.addTag(id, header[id]); } const genre = ID3v1Parser.getGenre(header.genre); if (genre) - this.addTag('genre', genre); + await this.addTag('genre', genre); } else { debug('ID3v1 header not found at: pos=%s', this.tokenizer.fileInfo.size - Iid3v1Token.len); } } - private addTag(id: string, value: any) { - this.metadata.addTag('ID3v1', id, value); + private async addTag(id: string, value: any): Promise { + await this.metadata.addTag('ID3v1', id, value); } } @@ -157,7 +157,7 @@ export async function hasID3v1Header(reader: IRandomReader): Promise { if (reader.fileSize >= 128) { const tag = Buffer.alloc(3); await reader.randomRead(tag, 0, tag.length, reader.fileSize - 128); - return tag.toString('binary') === 'TAG'; + return tag.toString('latin1') === 'TAG'; } return false; } diff --git a/lib/id3v2/FrameParser.ts b/lib/id3v2/FrameParser.ts index f1dc512f4..2e5ddbc84 100644 --- a/lib/id3v2/FrameParser.ts +++ b/lib/id3v2/FrameParser.ts @@ -22,7 +22,7 @@ interface IPicture { data?: Uint8Array; } -const defaultEnc: util.StringEncoding = 'latin1'; // latin1 == iso-8859-1; +const defaultEnc = 'latin1'; // latin1 == iso-8859-1; export function parseGenre(origVal: string): string[] { // match everything inside parentheses @@ -286,7 +286,7 @@ export class FrameParser { // Decode URL fzero = util.findZero(uint8Array, offset + 1, length, encoding); const description = util.decodeString(uint8Array.slice(offset + 1, fzero), encoding); - offset = fzero + (encoding === 'utf16le' ? 2 : 1); + offset = fzero + (encoding === 'utf-16le' ? 2 : 1); output = {description, url: util.decodeString(uint8Array.slice(offset, length), defaultEnc)}; break; } @@ -370,7 +370,7 @@ export class FrameParser { } private static getNullTerminatorLength(enc: util.StringEncoding): number { - return enc === 'utf16le' ? 2 : 1; + return enc === 'utf-16le' ? 2 : 1; } } diff --git a/lib/id3v2/ID3v24TagMapper.ts b/lib/id3v2/ID3v24TagMapper.ts index 12df2007d..5d4e54417 100644 --- a/lib/id3v2/ID3v24TagMapper.ts +++ b/lib/id3v2/ID3v24TagMapper.ts @@ -1,10 +1,11 @@ -import { INativeTagMap } from '../common/GenericTagTypes.js'; +import { UINT32_LE } from 'token-types'; import { CommonTagMapper } from '../common/GenericTagMapper.js'; import { CaseInsensitiveTagMap } from '../common/CaseInsensitiveTagMap.js'; -import * as util from '../common/Util.js'; +import { decodeString } from '../common/Util.js'; -import { INativeMetadataCollector } from '../common/MetadataCollector.js'; -import { IRating, ITag } from '../type.js'; +import type { INativeTagMap } from '../common/GenericTagTypes.js'; +import type { INativeMetadataCollector } from '../common/MetadataCollector.js'; +import type { IRating, ITag } from '../type.js'; /** * ID3v2.3/ID3v2.4 tag mappings @@ -171,7 +172,6 @@ export class ID3v24TagMapper extends CaseInsensitiveTagMap { * Handle post mapping exceptions / correction * @param tag to post map * @param warnings Wil be used to register (collect) warnings - * @return Common value e.g. "Buena Vista Social Club" */ protected postMap(tag: ITag, warnings: INativeMetadataCollector): void { @@ -180,7 +180,7 @@ export class ID3v24TagMapper extends CaseInsensitiveTagMap { case 'UFID': // decode MusicBrainz Recording Id if (tag.value.owner_identifier === 'http://musicbrainz.org') { tag.id += ':' + tag.value.owner_identifier; - tag.value = util.decodeString(tag.value.identifier, 'latin1'); // latin1 == iso-8859-1 + tag.value = decodeString(tag.value.identifier, 'latin1'); // latin1 == iso-8859-1 } break; @@ -190,7 +190,7 @@ export class ID3v24TagMapper extends CaseInsensitiveTagMap { case 'AverageLevel': case 'PeakValue': tag.id += ':' + tag.value.owner_identifier; - tag.value = tag.value.data.length === 4 ? tag.value.data.readUInt32LE(0) : null; + tag.value = tag.value.data.length === 4 ? UINT32_LE.get(tag.value.data, 0) : null; if (tag.value === null) { warnings.addWarning(`Failed to parse PRIV:PeakValue`); } diff --git a/lib/id3v2/ID3v2Parser.ts b/lib/id3v2/ID3v2Parser.ts index e8bd6380c..b91d4cdf8 100644 --- a/lib/id3v2/ID3v2Parser.ts +++ b/lib/id3v2/ID3v2Parser.ts @@ -148,30 +148,28 @@ export class ID3v2Parser { for (const tag of this.parseMetadata(uint8Array)) { if (tag.id === 'TXXX') { if (tag.value) { - for (const text of tag.value.text) { - this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, tag.value.description), text); - } + await Promise.all(tag.value.text.map(text => + this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, tag.value.description), text) + )); } } else if (tag.id === 'COM') { - for (const value of tag.value) { - this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value.text); - } + await Promise.all(tag.value.map(value => + this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value.text) + )); } else if (tag.id === 'COMM') { - for (const value of tag.value) { - this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value); - } + await Promise.all(tag.value.map(value => + this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value) + )); } else if (Array.isArray(tag.value)) { - for (const value of tag.value) { - this.addTag(tag.id, value); - } + await Promise.all(tag.value.map(value => this.addTag(tag.id, value))); } else { - this.addTag(tag.id, tag.value); + await this.addTag(tag.id, tag.value); } } } - private addTag(id: string, value: any) { - this.metadata.addTag(this.headerType, id, value); + private async addTag(id: string, value: any): Promise { + await this.metadata.addTag(this.headerType, id, value); } private parseMetadata(data: Uint8Array): ITag[] { diff --git a/lib/id3v2/ID3v2Token.ts b/lib/id3v2/ID3v2Token.ts index 2a8f54a99..0d21eb86d 100644 --- a/lib/id3v2/ID3v2Token.ts +++ b/lib/id3v2/ID3v2Token.ts @@ -141,9 +141,9 @@ export const TextEncodingToken: IGetToken = { case 0x00: return {encoding: 'latin1'}; // binary case 0x01: - return {encoding: 'utf16le', bom: true}; + return {encoding: 'utf-16le', bom: true}; case 0x02: - return {encoding: 'utf16le', bom: false}; + return {encoding: 'utf-16le', bom: false}; case 0x03: return {encoding: 'utf8', bom: false}; default: diff --git a/lib/lyrics3/Lyrics3.ts b/lib/lyrics3/Lyrics3.ts index 0391cd8ab..7c4687bec 100644 --- a/lib/lyrics3/Lyrics3.ts +++ b/lib/lyrics3/Lyrics3.ts @@ -6,7 +6,7 @@ export async function getLyricsHeaderLength(reader: IRandomReader): Promise= 143) { const buf = Buffer.alloc(15); await reader.randomRead(buf, 0, buf.length, reader.fileSize - 143); - const txt = buf.toString('binary'); + const txt = buf.toString('latin1'); const tag = txt.substr(6); if (tag === endTag2) { return parseInt(txt.substr(0, 6), 10) + 15; diff --git a/lib/matroska/MatroskaParser.ts b/lib/matroska/MatroskaParser.ts index 1cd824709..25c64dcd2 100644 --- a/lib/matroska/MatroskaParser.ts +++ b/lib/matroska/MatroskaParser.ts @@ -60,7 +60,7 @@ export class MatroskaParser extends BasicParser { const timecodeScale = info.timecodeScale ? info.timecodeScale : 1000000; if (typeof info.duration === 'number') { const duration = info.duration * timecodeScale / 1000000000; - this.addTag('segment:title', info.title); + await this.addTag('segment:title', info.title); this.metadata.setFormat('duration', duration); } } @@ -108,29 +108,25 @@ export class MatroskaParser extends BasicParser { } if (matroska.segment.tags) { - matroska.segment.tags.tag.forEach(tag => { + await Promise.all(matroska.segment.tags.tag.map(async tag => { const target = tag.target; const targetType = target?.targetTypeValue ? TargetType[target.targetTypeValue] : (target?.targetType ? target.targetType : 'track'); - tag.simpleTags.forEach(simpleTag => { + await Promise.all(tag.simpleTags.map(async simpleTag => { const value = simpleTag.string ? simpleTag.string : simpleTag.binary; - this.addTag(`${targetType}:${simpleTag.name}`, value); - }); - }); + await this.addTag(`${targetType}:${simpleTag.name}`, value); + })); + })); } if (matroska.segment.attachments) { - matroska.segment.attachments.attachedFiles + await Promise.all(matroska.segment.attachments.attachedFiles .filter(file => file.mimeType.startsWith('image/')) - .map(file => { - return { - data: file.data, - format: file.mimeType, - description: file.description, - name: file.name - }; - }).forEach(picture => { - this.addTag('picture', picture); - }); + .map(file => this.addTag('picture', { + data: file.data, + format: file.mimeType, + description: file.description, + name: file.name + }))); } } } @@ -255,7 +251,7 @@ export class MatroskaParser extends BasicParser { return buf; } - private addTag(tagId: string, value: any) { - this.metadata.addTag('matroska', tagId, value); + private async addTag(tagId: string, value: any): Promise { + await this.metadata.addTag('matroska', tagId, value); } } diff --git a/lib/mp4/AtomToken.ts b/lib/mp4/AtomToken.ts index 58284243c..e1e5b8b4b 100644 --- a/lib/mp4/AtomToken.ts +++ b/lib/mp4/AtomToken.ts @@ -144,7 +144,7 @@ export const Header: IToken = { return { length: BigInt(length), - name: new Token.StringType(4, 'binary').get(buf, off + 4) + name: new Token.StringType(4, 'latin1').get(buf, off + 4) }; }, diff --git a/lib/mp4/MP4Parser.ts b/lib/mp4/MP4Parser.ts index 223e446ae..9fe2e4909 100644 --- a/lib/mp4/MP4Parser.ts +++ b/lib/mp4/MP4Parser.ts @@ -8,6 +8,7 @@ import * as AtomToken from './AtomToken.js'; import { IChapter, ITrackInfo, TrackType } from '../type.js'; import { IGetToken } from '@tokenizer/token'; +import { uint8ArrayToHex, uint8ArrayToString } from 'uint8array-extras'; const debug = initDebug('music-metadata:parser:MP4'); const tagFormat = 'iTunes'; @@ -271,8 +272,8 @@ export class MP4Parser extends BasicParser { } } - private addTag(id: string, value: any) { - this.metadata.addTag(tagFormat, id, value); + private async addTag(id: string, value: any): Promise { + await this.metadata.addTag(tagFormat, id, value); } private addWarning(message: string) { @@ -303,8 +304,8 @@ export class MP4Parser extends BasicParser { break; default: - const dataAtom = await this.tokenizer.readToken(new Token.BufferType(payLoadLength)); - this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + dataAtom.toString('hex') + ' ascii=' + dataAtom.toString('ascii')); + const uint8Array = await this.tokenizer.readToken(new Token.Uint8ArrayType(payLoadLength)); + this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + uint8ArrayToHex(uint8Array) + ' ascii=' + uint8ArrayToString(uint8Array, 'ascii')); } }, metaAtom.getPayloadLength(0)); @@ -328,19 +329,19 @@ export class MP4Parser extends BasicParser { const num = Token.UINT8.get(dataAtom.value, 3); const of = Token.UINT8.get(dataAtom.value, 5); // console.log(" %s[data] = %s/%s", tagKey, num, of); - this.addTag(tagKey, num + '/' + of); + await this.addTag(tagKey, num + '/' + of); break; case 'gnre': const genreInt = Token.UINT8.get(dataAtom.value, 1); const genreStr = Genres[genreInt - 1]; // console.log(" %s[data] = %s", tagKey, genreStr); - this.addTag(tagKey, genreStr); + await this.addTag(tagKey, genreStr); break; case 'rate': const rate = dataAtom.value.toString('ascii'); - this.addTag(tagKey, rate); + await this.addTag(tagKey, rate); break; default: @@ -350,13 +351,13 @@ export class MP4Parser extends BasicParser { case 1: // UTF-8: Without any count or NULL terminator case 18: // Unknown: Found in m4b in combination with a '©gen' tag - this.addTag(tagKey, dataAtom.value.toString('utf-8')); + await this.addTag(tagKey, dataAtom.value.toString('utf-8')); break; case 13: // JPEG if (this.options.skipCovers) break; - this.addTag(tagKey, { + await this.addTag(tagKey, { format: 'image/jpeg', data: Buffer.from(dataAtom.value) }); @@ -365,30 +366,30 @@ export class MP4Parser extends BasicParser { case 14: // PNG if (this.options.skipCovers) break; - this.addTag(tagKey, { + await this.addTag(tagKey, { format: 'image/png', data: Buffer.from(dataAtom.value) }); break; case 21: // BE Signed Integer - this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true)); + await this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true)); break; case 22: // BE Unsigned Integer - this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false)); + await this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false)); break; case 65: // An 8-bit signed integer - this.addTag(tagKey, dataAtom.value.readInt8(0)); + await this.addTag(tagKey, dataAtom.value.readInt8(0)); break; case 66: // A big-endian 16-bit signed integer - this.addTag(tagKey, dataAtom.value.readInt16BE(0)); + await this.addTag(tagKey, dataAtom.value.readInt16BE(0)); break; case 67: // A big-endian 32-bit signed integer - this.addTag(tagKey, dataAtom.value.readInt32BE(0)); + await this.addTag(tagKey, dataAtom.value.readInt32BE(0)); break; default: @@ -521,7 +522,7 @@ export class MP4Parser extends BasicParser { date: async (len: number) => { const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8')); - this.addTag('date', date); + await this.addTag('date', date); } }; diff --git a/lib/musepack/index.ts b/lib/musepack/index.ts index ecc124133..777cc2745 100644 --- a/lib/musepack/index.ts +++ b/lib/musepack/index.ts @@ -12,7 +12,7 @@ const debug = initDebug('music-metadata:parser:musepack'); class MusepackParser extends AbstractID3Parser { public async postId3v2Parse(): Promise { - const signature = await this.tokenizer.peekToken(new Token.StringType(3, 'binary')); + const signature = await this.tokenizer.peekToken(new Token.StringType(3, 'latin1')); let mpcParser: ITokenParser; switch (signature) { case 'MP+': { diff --git a/lib/musepack/sv8/StreamVersion8.ts b/lib/musepack/sv8/StreamVersion8.ts index 67f7a57f8..cc4976d95 100644 --- a/lib/musepack/sv8/StreamVersion8.ts +++ b/lib/musepack/sv8/StreamVersion8.ts @@ -6,7 +6,7 @@ import * as util from '../../common/Util.js'; const debug = initDebug('music-metadata:parser:musepack:sv8'); -const PacketKey = new Token.StringType(2, 'binary'); +const PacketKey = new Token.StringType(2, 'latin1'); interface IVarSize { len: number, diff --git a/lib/ogg/Ogg.ts b/lib/ogg/Ogg.ts index f0dc6fcdd..844689607 100644 --- a/lib/ogg/Ogg.ts +++ b/lib/ogg/Ogg.ts @@ -64,7 +64,7 @@ export interface IPageConsumer { * @param {IPageHeader} header Ogg page header * @param {Buffer} pageData Ogg page data */ - parsePage(header: IPageHeader, pageData: Uint8Array); + parsePage(header: IPageHeader, pageData: Uint8Array): Promise; /** * Calculate duration of provided header @@ -75,5 +75,5 @@ export interface IPageConsumer { /** * Force to parse pending segments */ - flush(); + flush(): Promise; } diff --git a/lib/ogg/OggParser.ts b/lib/ogg/OggParser.ts index 55b00e601..d862c8c05 100644 --- a/lib/ogg/OggParser.ts +++ b/lib/ogg/OggParser.ts @@ -57,7 +57,7 @@ export class OggParser extends BasicParser { firstPage: util.getBit(buf, off + 5, 1), lastPage: util.getBit(buf, off + 5, 2) }, - // packet_flag: buf.readUInt8(off + 5), + // packet_flag: Token.UINT8.get(buf, off + 5), absoluteGranulePosition: Number(Token.UINT64_LE.get(buf, off + 6)), streamSerialNumber: Token.UINT32_LE.get(buf, off + 14), pageSequenceNo: Token.UINT32_LE.get(buf, off + 18), @@ -117,7 +117,7 @@ export class OggParser extends BasicParser { throw new Error('gg audio-codec not recognized (id=' + id + ')'); } } - this.pageConsumer.parsePage(header, pageData); + await this.pageConsumer.parsePage(header, pageData); } while (!header.headerType.lastPage); } catch (err) { if (err instanceof EndOfStreamError) { @@ -131,7 +131,7 @@ export class OggParser extends BasicParser { if (this.pageNumber > 0) { // ignore this error: work-around if last OGG-page is not marked with last-page flag this.metadata.addWarning('Invalid FourCC ID, maybe last OGG-page is not marked with last-page flag'); - this.pageConsumer.flush(); + await this.pageConsumer.flush(); } } else { throw err; diff --git a/lib/ogg/opus/Opus.ts b/lib/ogg/opus/Opus.ts index bd458850c..b58caec38 100644 --- a/lib/ogg/opus/Opus.ts +++ b/lib/ogg/opus/Opus.ts @@ -55,12 +55,12 @@ export class IdHeader implements IGetToken { public get(buf, off): IIdHeader { return { magicSignature: new Token.StringType(8, 'ascii').get(buf, off + 0), - version: buf.readUInt8(off + 8), - channelCount: buf.readUInt8(off + 9), - preSkip: buf.readInt16LE(off + 10), - inputSampleRate: buf.readInt32LE(off + 12), - outputGain: buf.readInt16LE(off + 16), - channelMapping: buf.readUInt8(off + 18) + version: Token.UINT8.get(buf, off + 8), + channelCount: Token.UINT8.get(buf, off + 9), + preSkip: Token.UINT16_LE.get(buf, off + 10), + inputSampleRate: Token.UINT32_LE.get(buf, off + 12), + outputGain: Token.UINT16_LE.get(buf, off + 16), + channelMapping: Token.UINT8.get(buf, off + 18) }; } } diff --git a/lib/ogg/opus/OpusParser.ts b/lib/ogg/opus/OpusParser.ts index 80be0d635..18ecf2b82 100644 --- a/lib/ogg/opus/OpusParser.ts +++ b/lib/ogg/opus/OpusParser.ts @@ -37,12 +37,12 @@ export class OpusParser extends VorbisParser { this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount); } - protected parseFullPage(pageData: Buffer) { + protected async parseFullPage(pageData: Buffer): Promise { const magicSignature = new Token.StringType(8, 'ascii').get(pageData, 0); switch (magicSignature) { case 'OpusTags': - this.parseUserCommentList(pageData, 8); + await this.parseUserCommentList(pageData, 8); this.lastPos = this.tokenizer.position - pageData.length; break; diff --git a/lib/ogg/speex/Speex.ts b/lib/ogg/speex/Speex.ts index 280e7c412..9e3ef7341 100644 --- a/lib/ogg/speex/Speex.ts +++ b/lib/ogg/speex/Speex.ts @@ -47,19 +47,19 @@ export const Header: IGetToken = { return { speex: new Token.StringType(8, 'ascii').get(buf, off + 0), version: util.trimRightNull(new Token.StringType(20, 'ascii').get(buf, off + 8)), - version_id: buf.readInt32LE(off + 28), - header_size: buf.readInt32LE(off + 32), - rate: buf.readInt32LE(off + 36), - mode: buf.readInt32LE(off + 40), - mode_bitstream_version: buf.readInt32LE(off + 44), - nb_channels: buf.readInt32LE(off + 48), - bitrate: buf.readInt32LE(off + 52), - frame_size: buf.readInt32LE(off + 56), - vbr: buf.readInt32LE(off + 60), - frames_per_packet: buf.readInt32LE(off + 64), - extra_headers: buf.readInt32LE(off + 68), - reserved1: buf.readInt32LE(off + 72), - reserved2: buf.readInt32LE(off + 76) + version_id: Token.INT32_LE.get(buf, off + 28), + header_size: Token.INT32_LE.get(buf, off + 32), + rate: Token.INT32_LE.get(buf, off + 36), + mode: Token.INT32_LE.get(buf, off + 40), + mode_bitstream_version: Token.INT32_LE.get(buf, off + 44), + nb_channels: Token.INT32_LE.get(buf, off + 48), + bitrate: Token.INT32_LE.get(buf, off + 52), + frame_size: Token.INT32_LE.get(buf, off + 56), + vbr:Token.INT32_LE.get(buf, off + 60), + frames_per_packet: Token.INT32_LE.get(buf, off + 64), + extra_headers: Token.INT32_LE.get(buf, off + 68), + reserved1: Token.INT32_LE.get(buf, off + 72), + reserved2: Token.INT32_LE.get(buf, off + 76) }; } }; diff --git a/lib/ogg/theora/Theora.ts b/lib/ogg/theora/Theora.ts index f9285aaf0..803236f35 100644 --- a/lib/ogg/theora/Theora.ts +++ b/lib/ogg/theora/Theora.ts @@ -34,13 +34,13 @@ export const IdentificationHeader: IGetToken = { get: (buf: Buffer, off): IIdentificationHeader => { return { id: new Token.StringType(7, 'ascii').get(buf, off), - vmaj: buf.readUInt8(off + 7), - vmin: buf.readUInt8(off + 8), - vrev: buf.readUInt8(off + 9), - vmbw: buf.readUInt16BE(off + 10), - vmbh: buf.readUInt16BE(off + 17), + vmaj: Token.UINT8.get(buf, off + 7), + vmin: Token.UINT8.get(buf, off + 8), + vrev: Token.UINT8.get(buf, off + 9), + vmbw: Token.UINT16_BE.get(buf, off + 10), + vmbh: Token.UINT16_BE.get(buf, off + 17), nombr: Token.UINT24_BE.get(buf, off + 37), - nqual: buf.readUInt8(off + 40) + nqual: Token.UINT8.get(buf, off + 40) }; } }; diff --git a/lib/ogg/theora/TheoraParser.ts b/lib/ogg/theora/TheoraParser.ts index 405720269..1f9ac7551 100644 --- a/lib/ogg/theora/TheoraParser.ts +++ b/lib/ogg/theora/TheoraParser.ts @@ -22,13 +22,13 @@ export class TheoraParser implements Ogg.IPageConsumer { * @param header Ogg Page Header * @param pageData Page data */ - public parsePage(header: Ogg.IPageHeader, pageData: Buffer) { + public async parsePage(header: Ogg.IPageHeader, pageData: Buffer): Promise { if (header.headerType.firstPage) { - this.parseFirstPage(header, pageData); + await this.parseFirstPage(header, pageData); } } - public flush() { + public async flush(): Promise { debug('flush'); } @@ -41,7 +41,7 @@ export class TheoraParser implements Ogg.IPageConsumer { * @param {IPageHeader} header * @param {Buffer} pageData */ - protected parseFirstPage(header: Ogg.IPageHeader, pageData: Buffer) { + protected async parseFirstPage(header: Ogg.IPageHeader, pageData: Buffer): Promise { debug('First Ogg/Theora page'); this.metadata.setFormat('codec', 'Theora'); const idHeader = IdentificationHeader.get(pageData, 0); diff --git a/lib/ogg/vorbis/Vorbis.ts b/lib/ogg/vorbis/Vorbis.ts index c875dec47..61a1a9b48 100644 --- a/lib/ogg/vorbis/Vorbis.ts +++ b/lib/ogg/vorbis/Vorbis.ts @@ -34,10 +34,10 @@ export interface IVorbisPicture extends IPicture { export class VorbisPictureToken implements IGetToken { public static fromBase64(base64str: string): IVorbisPicture { - return this.fromBuffer(Buffer.from(base64str, 'base64')); + return this.fromBuffer(Uint8Array.from(atob(base64str), c => c.charCodeAt(0))); } - public static fromBuffer(buffer: Buffer): IVorbisPicture { + public static fromBuffer(buffer: Uint8Array): IVorbisPicture { const pic = new VorbisPictureToken(buffer.length); return pic.get(buffer, 0); } @@ -45,15 +45,15 @@ export class VorbisPictureToken implements IGetToken { constructor(public len) { } - public get(buffer: Buffer, offset: number): IVorbisPicture { + public get(buffer: Uint8Array, offset: number): IVorbisPicture { const type = AttachedPictureType[Token.UINT32_BE.get(buffer, offset)]; const mimeLen = Token.UINT32_BE.get(buffer, offset += 4); - const format = buffer.toString('utf-8', offset += 4, offset + mimeLen); + const format = new Token.StringType(mimeLen, 'utf-8').get(buffer, offset += 4); const descLen = Token.UINT32_BE.get(buffer, offset += mimeLen); - const description = buffer.toString('utf-8', offset += 4, offset + descLen); + const description = new Token.StringType(descLen, 'utf-8').get(buffer, offset += 4); const width = Token.UINT32_BE.get(buffer, offset += descLen); const height = Token.UINT32_BE.get(buffer, offset += 4); @@ -103,9 +103,9 @@ export interface ICommonHeader { export const CommonHeader: IGetToken = { len: 7, - get: (buf: Buffer, off): ICommonHeader => { + get: (buf: Uint8Array, off): ICommonHeader => { return { - packetType: buf.readUInt8(off), + packetType: Token.UINT8.get(buf, off), vorbis: new Token.StringType(6, 'ascii').get(buf, off + 1) }; } @@ -132,14 +132,13 @@ export const IdentificationHeader: IGetToken = { len: 23, get: (uint8Array, off): IFormatInfo => { - const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset); return { - version: dataView.getUint32(off + 0, true), - channelMode: dataView.getUint8(off + 4), - sampleRate: dataView.getUint32(off + 5, true), - bitrateMax: dataView.getUint32(off + 9, true), - bitrateNominal: dataView.getUint32(off + 13, true), - bitrateMin: dataView.getUint32(off + 17, true) + version: Token.UINT32_LE.get(uint8Array, off + 0), + channelMode: Token.UINT8.get(uint8Array, off + 4), + sampleRate: Token.UINT32_LE.get(uint8Array, off + 5), + bitrateMax: Token.UINT32_LE.get(uint8Array, off + 9), + bitrateNominal: Token.UINT32_LE.get(uint8Array, off + 13), + bitrateMin: Token.UINT32_LE.get(uint8Array, off + 17) }; } }; diff --git a/lib/ogg/vorbis/VorbisParser.ts b/lib/ogg/vorbis/VorbisParser.ts index 0e57b1208..719d7ff9c 100644 --- a/lib/ogg/vorbis/VorbisParser.ts +++ b/lib/ogg/vorbis/VorbisParser.ts @@ -26,7 +26,7 @@ export class VorbisParser implements IPageConsumer { * @param header Ogg Page Header * @param pageData Page data */ - public parsePage(header: IPageHeader, pageData: Buffer) { + public async parsePage(header: IPageHeader, pageData: Buffer): Promise { if (header.headerType.firstPage) { this.parseFirstPage(header, pageData); } else { @@ -40,7 +40,7 @@ export class VorbisParser implements IPageConsumer { // Flush page segments if (this.pageSegments.length > 0) { const fullPage = Buffer.concat(this.pageSegments); - this.parseFullPage(fullPage); + await this.parseFullPage(fullPage); } // Reset page segments this.pageSegments = header.headerType.lastPage ? [] : [pageData]; @@ -51,20 +51,20 @@ export class VorbisParser implements IPageConsumer { } } - public flush() { - this.parseFullPage(Buffer.concat(this.pageSegments)); + public async flush(): Promise { + await this.parseFullPage(Buffer.concat(this.pageSegments)); } - public parseUserComment(pageData: Buffer, offset: number): number { + public async parseUserComment(pageData: Buffer, offset: number): Promise { const decoder = new VorbisDecoder(pageData, offset); const tag = decoder.parseUserComment(); - this.addTag(tag.key, tag.value); + await this.addTag(tag.key, tag.value); return tag.len; } - public addTag(id: string, value: string | IVorbisPicture) { + public async addTag(id: string, value: string | IVorbisPicture): Promise { if (id === 'METADATA_BLOCK_PICTURE' && (typeof value === 'string')) { if (this.options.skipCovers) { debug(`Ignore picture`); @@ -76,7 +76,7 @@ export class VorbisParser implements IPageConsumer { debug(`Push tag: id=${id}, value=${value}`); } - this.metadata.addTag('vorbis', id, value); + await this.metadata.addTag('vorbis', id, value); } public calculateDuration(header: IPageHeader) { @@ -109,7 +109,7 @@ export class VorbisParser implements IPageConsumer { } else throw new Error('First Ogg page should be type 1: the identification header'); } - protected parseFullPage(pageData: Buffer) { + protected async parseFullPage(pageData: Buffer): Promise { // New page const commonHeader = CommonHeader.get(pageData, 0); debug('Parse full page: type=%s, byteLength=%s', commonHeader.packetType, pageData.byteLength); @@ -127,7 +127,7 @@ export class VorbisParser implements IPageConsumer { /** * Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2 */ - protected parseUserCommentList(pageData: Buffer, offset: number) { + protected async parseUserCommentList(pageData: Buffer, offset: number): Promise { const strLen = Token.UINT32_LE.get(pageData, offset); offset += 4; @@ -137,7 +137,7 @@ export class VorbisParser implements IPageConsumer { offset += 4; while (userCommentListLength-- > 0) { - offset += this.parseUserComment(pageData, offset); + offset += (await this.parseUserComment(pageData, offset)); } } } diff --git a/lib/riff/RiffChunk.ts b/lib/riff/RiffChunk.ts index 37d9f3957..8b3337b69 100644 --- a/lib/riff/RiffChunk.ts +++ b/lib/riff/RiffChunk.ts @@ -10,12 +10,12 @@ export { IChunkHeader } from '../iff/index.js'; export const Header: IGetToken = { len: 8, - get: (buf: Buffer, off): IChunkHeader => { + get: (buf: Uint8Array, off): IChunkHeader => { return { // Group-ID - chunkID: buf.toString('binary', off, off + 4), + chunkID: new Token.StringType(4, 'latin1').get(buf, off), // Size - chunkSize: Token.UINT32_LE.get(buf, 4) + chunkSize: Token.UINT32_LE.get(buf, off + 4) }; } }; diff --git a/lib/type.ts b/lib/type.ts index b61a15b32..b912f0521 100644 --- a/lib/type.ts +++ b/lib/type.ts @@ -1,5 +1,3 @@ -import { Buffer } from 'node:buffer'; - import { GenericTagId, TagType } from './common/GenericTagTypes.js'; import { IFooter } from './apev2/APEv2Token.js'; import { TrackType } from './matroska/types.js'; @@ -17,7 +15,7 @@ export interface IPicture { /** * Image data */ - data: Buffer; + data: Uint8Array; /** * Optional description */ @@ -394,7 +392,7 @@ export interface IAudioTrack { samplingFrequency?: number; outputSamplingFrequency?: number; channels?: number; - channelPositions?: Buffer; + channelPositions?: Uint8Array; bitDepth?: number; } @@ -407,7 +405,7 @@ export interface IVideoTrack { displayHeight?: number; displayUnit?: number; aspectRatioType?: number; - colourSpace?: Buffer; + colourSpace?: Uint8Array; gammaValue?: number; } @@ -490,7 +488,7 @@ export interface IFormat { /** * 16-byte MD5 of raw audio */ - readonly audioMD5?: Buffer; + readonly audioMD5?: Uint8Array; /** * Chapters in audio stream @@ -683,11 +681,11 @@ export interface IRandomReader { /** * Read from a given position of an abstracted file or buffer. - * @param buffer {Buffer} is the buffer that the data will be written to. - * @param offset {number} is the offset in the buffer to start writing at. - * @param length {number}is an integer specifying the number of bytes to read. - * @param position {number} is an argument specifying where to begin reading from in the file. + * @param {Uint8Array} buffer the buffer that the data will be written to. + * @param {number} offset the offset in the buffer to start writing at. + * @param {number} length an integer specifying the number of bytes to read. + * @param {number} position an argument specifying where to begin reading from in the file. * @return {Promise} bytes read */ - randomRead(buffer: Buffer, offset: number, length: number, position: number): Promise; + randomRead(buffer: Uint8Array, offset: number, length: number, position: number): Promise; } diff --git a/lib/wav/WaveChunk.ts b/lib/wav/WaveChunk.ts index d83dd8bee..6ac95c769 100644 --- a/lib/wav/WaveChunk.ts +++ b/lib/wav/WaveChunk.ts @@ -1,5 +1,6 @@ import { IGetToken } from 'strtok3/core'; import { IChunkHeader } from '../iff/index.js'; +import * as Token from 'token-types'; /** * Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx @@ -58,14 +59,14 @@ export class Format implements IGetToken { this.len = header.chunkSize; } - public get(buf: Buffer, off: number): IWaveFormat { + public get(buf: Uint8Array, off: number): IWaveFormat { return { - wFormatTag: buf.readUInt16LE(off), - nChannels: buf.readUInt16LE(off + 2), - nSamplesPerSec: buf.readUInt32LE(off + 4), - nAvgBytesPerSec: buf.readUInt32LE(off + 8), - nBlockAlign: buf.readUInt16LE(off + 12), - wBitsPerSample: buf.readUInt16LE(off + 14) + wFormatTag: Token.UINT16_LE.get(buf, off), + nChannels: Token.UINT16_LE.get(buf,off + 2), + nSamplesPerSec: Token.UINT32_LE.get(buf,off + 4), + nAvgBytesPerSec: Token.UINT32_LE.get(buf,off + 8), + nBlockAlign: Token.UINT16_LE.get(buf,off + 12), + wBitsPerSample: Token.UINT16_LE.get(buf,off + 14) }; } } @@ -92,7 +93,7 @@ export class FactChunk implements IGetToken { public get(buf: Buffer, off: number): IFactChunk { return { - dwSampleLength: buf.readUInt32LE(off) + dwSampleLength: Token.UINT32_LE.get(buf, off) }; } diff --git a/lib/wav/WaveParser.ts b/lib/wav/WaveParser.ts index 22685d19b..a869ab09e 100644 --- a/lib/wav/WaveParser.ts +++ b/lib/wav/WaveParser.ts @@ -156,7 +156,7 @@ export class WaveParser extends BasicParser { } public async parseListTag(listHeader: riff.IChunkHeader): Promise { - const listType = await this.tokenizer.readToken(new Token.StringType(4, 'binary')); + const listType = await this.tokenizer.readToken(new Token.StringType(4, 'latin1')); debug('pos=%s, parseListTag: chunkID=RIFF/WAVE/LIST/%s', this.tokenizer.position, listType); switch (listType) { case 'INFO': diff --git a/lib/wavpack/WavPackParser.ts b/lib/wavpack/WavPackParser.ts index 84761bf47..359f0d3ef 100644 --- a/lib/wavpack/WavPackParser.ts +++ b/lib/wavpack/WavPackParser.ts @@ -83,7 +83,7 @@ export class WavPackParser extends BasicParser { case 0xe: // ID_DSD_BLOCK debug('ID_DSD_BLOCK'); // https://github.com/dbry/WavPack/issues/71#issuecomment-483094813 - const mp = 1 << data.readUInt8(0); + const mp = 1 << Token.UINT8.get(data, 0); const samplingRate = header.flags.samplingRate * mp * 8; // ToDo: second factor should be read from DSD-metadata block https://github.com/dbry/WavPack/issues/71#issuecomment-483094813 if (!header.flags.isDSD) throw new Error('Only expect DSD block if DSD-flag is set'); diff --git a/package.json b/package.json index 447833776..265f7f599 100644 --- a/package.json +++ b/package.json @@ -92,10 +92,11 @@ "@tokenizer/token": "^0.3.0", "content-type": "^1.0.5", "debug": "^4.3.4", - "file-type": "^18.6.0", + "file-type": "^19.1.0", "media-typer": "^1.1.0", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" + "strtok3": "^7.1.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" }, "devDependencies": { "@types/chai": "^4.3.16", diff --git a/test/test-common.ts b/test/test-common.ts index 4e163f44d..858a34dee 100644 --- a/test/test-common.ts +++ b/test/test-common.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert, expect } from 'chai'; import path from 'node:path'; import { commonTags, isSingleton } from '../lib/common/GenericTagTypes.js'; @@ -90,7 +90,7 @@ describe('function selectCover()', () => { for (const multiCoverFile of multiCoverFiles) { const filePath = path.join(samplePath, multiCoverFile); const {common} = await mm.parseFile(filePath); - assert.isTrue(common.picture.length > 1); + expect(common.picture).to.have.lengthOf.above(1, multiCoverFile); const cover = mm.selectCover(common.picture); if (cover.type) { assert.equal(cover.type, 'Cover (front)', 'cover.type'); diff --git a/test/test-file-asf.ts b/test/test-file-asf.ts index 07a873296..6aabcb1e5 100644 --- a/test/test-file-asf.ts +++ b/test/test-file-asf.ts @@ -13,7 +13,7 @@ describe('Parse ASF', () => { describe('GUID', () => { it('should construct GUID from string', () => { - const Header_GUID = Buffer.from([ + const Header_GUID = Uint8Array.from([ 0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C ]); @@ -23,7 +23,7 @@ describe('Parse ASF', () => { it('should construct GUID from string', () => { - const guid_data = Buffer.from([48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108]); + const guid_data = new Uint8Array([48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108]); assert.deepEqual(GUID.fromBin(guid_data).str, '75B22630-668E-11CF-A6D9-00AA0062CE6C'); }); }); @@ -34,41 +34,41 @@ describe('Parse ASF', () => { */ it('should be able to roughly decode a 64-bit QWord', () => { - const tests: { raw: string, expected: number, description: string }[] = [ + const tests: { raw: number[], expected: number, description: string }[] = [ { - raw: '\xFF\x00\x00\x00\x00\x00\x00\x00', + raw: [0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], expected: 0xFF, description: '8-bit' }, { - raw: '\xFF\xFF\x00\x00\x00\x00\x00\x00', + raw: [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], expected: 0xFFFF, description: '16-bit' }, { - raw: '\xFF\xFF\xFF\xFF\x00\x00\x00\x00', + raw: [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00], expected: 0xFFFFFFFF, description: '32-bit' }, { - raw: '\xFF\xFF\xFF\xFF\xFF\x00\x00\x00', + raw: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00], expected: 0xFFFFFFFFFF, description: '40-bit' }, { - raw: '\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00', + raw: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00], expected: 0xFFFFFFFFFFFF, description: '48-bit' }, { - raw: '\xFF\xFF\xFF\xFF\xFF\xFF\x0F\x00', + raw: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00], expected: 0xFFFFFFFFFFFFF, description: '52-bit' } ]; tests.forEach(test => { - const buf = Buffer.from(test.raw, 'binary'); + const buf = Uint8Array.from(test.raw); assert.strictEqual(Number(AsfUtil.getParserForAttr(DataType.QWord)(buf)), test.expected, test.description); }); diff --git a/test/test-file-flac.ts b/test/test-file-flac.ts index 476aaecae..a71f6656b 100644 --- a/test/test-file-flac.ts +++ b/test/test-file-flac.ts @@ -99,7 +99,7 @@ describe('Parse FLAC Vorbis comment', () => { it('should handle a corrupt data', () => { const emptyStreamSize = 10 * 1024; - const buf = Buffer.alloc(emptyStreamSize).fill(0); + const buf = new Uint8Array(emptyStreamSize).fill(0); const tmpFilePath = path.join(samplePath, 'zeroes.flac'); fs.writeFileSync(tmpFilePath, buf); diff --git a/test/test-file-mpeg.ts b/test/test-file-mpeg.ts index cf04c080c..99c183ff7 100644 --- a/test/test-file-mpeg.ts +++ b/test/test-file-mpeg.ts @@ -48,7 +48,7 @@ describe('Parse MPEG', () => { describe('MPEG frame sync efficiency', () => { const emptyStreamSize = 5 * 1024 * 1024; - const buf = Buffer.alloc(emptyStreamSize).fill(0); + const buf = new Uint8Array(emptyStreamSize).fill(0); it('should sync efficient from a stream', async function() { diff --git a/test/test-id3v2-utf16encoded.ts b/test/test-id3v2-utf16encoded.ts index d4ace0fca..664ae78c4 100644 --- a/test/test-id3v2-utf16encoded.ts +++ b/test/test-id3v2-utf16encoded.ts @@ -17,7 +17,7 @@ it("decode id3v2-utf16", async () => { assert.strictEqual(common.year, 2014, 'year'); assert.strictEqual(common.picture[0].format, 'image/jpeg', 'picture 0 format'); assert.strictEqual(common.picture[0].data.length, 214219, 'picture 0 length'); - assert.deepEqual(common.picture[0].data.slice(0, 2), Buffer.from([0xFF, 0xD8]), 'picture 0 JFIF magic header'); + assert.deepEqual(common.picture[0].data.slice(0, 2), Uint8Array.from([0xFF, 0xD8]), 'picture 0 JFIF magic header'); const native = metadata.native['ID3v2.3']; assert.ok(native, 'Native id3v2.3 tags should be present'); diff --git a/test/test-id3v2.2.ts b/test/test-id3v2.2.ts index 025a9dea2..88365f4d8 100644 --- a/test/test-id3v2.2.ts +++ b/test/test-id3v2.2.ts @@ -11,8 +11,8 @@ describe('ID3v2Parser', () => { const mp3Path = path.join(samplePath, 'mp3'); it('should be able to remove unsynchronisation bytes from buffer', () => { - const expected = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0, 0x00]); - const sample = Buffer.from([0xFF, 0xD8, 0xFF, 0x00, 0xE0, 0x00]); + const expected = Uint8Array.from([0xFF, 0xD8, 0xFF, 0xE0, 0x00]); + const sample = Uint8Array.from([0xFF, 0xD8, 0xFF, 0x00, 0xE0, 0x00]); const output = ID3v2Parser.removeUnsyncBytes(sample); assert.deepEqual(output, expected, 'bytes'); }); diff --git a/test/test-id3v2.4-musicbrainz.ts b/test/test-id3v2.4-musicbrainz.ts index af3e2b645..feefdba72 100644 --- a/test/test-id3v2.4-musicbrainz.ts +++ b/test/test-id3v2.4-musicbrainz.ts @@ -66,11 +66,11 @@ it('should MusicBrainz tags with id3v2.4', async () => { assert.deepEqual(native[i++], { id: 'PRIV', - value: {data: Buffer.from([0x02, 0x00, 0x00, 0x00]), owner_identifier: 'AverageLevel'} + value: {data: Uint8Array.from([0x02, 0x00, 0x00, 0x00]), owner_identifier: 'AverageLevel'} }, '[\'ID3v2.4\'].PRIV.AverageLevel'); assert.deepEqual(native[i++], { id: 'PRIV', - value: {data: Buffer.from([0x08, 0x00, 0x00, 0x00]), owner_identifier: 'PeakValue'} + value: {data: Uint8Array.from([0x08, 0x00, 0x00, 0x00]), owner_identifier: 'PeakValue'} }, '[\'ID3v2.4\'].PRIV.PeakValue'); assert.deepEqual(native[i++], {id: 'TCOM', value: 'Explosions in the Sky'}, '[\'ID3v2.4\'].TCOM'); assert.deepEqual(native[i++], {id: 'TDOR', value: '2004-10-12'}, '[\'ID3v2.4\'].TDOR'); diff --git a/test/test-mime.ts b/test/test-mime.ts index 628388999..f3b60ea44 100644 --- a/test/test-mime.ts +++ b/test/test-mime.ts @@ -8,7 +8,7 @@ import { SourceStream, samplePath } from './util.js'; describe('MIME & extension mapping', () => { - const buf = Buffer.alloc(30).fill(0); + const buf = new Uint8Array(30).fill(0); const audioExtension = ['.aac', '.mp3', '.ogg', '.wav', '.flac', '.m4a']; // ToDo: ass ".ac3" diff --git a/test/test-picard-parsing.ts b/test/test-picard-parsing.ts index b30882aef..1eff15ab4 100644 --- a/test/test-picard-parsing.ts +++ b/test/test-picard-parsing.ts @@ -28,7 +28,7 @@ describe('Parsing of metadata saved by \'Picard\' in audio files', () => { } } - function calcHash(buf: Buffer): string { + function calcHash(buf: Uint8Array): string { const hash = crypto.createHash('md5'); hash.update(buf); return hash.digest('hex'); diff --git a/test/test-ufid.ts b/test/test-ufid.ts index 6947e67ae..9908c8daa 100644 --- a/test/test-ufid.ts +++ b/test/test-ufid.ts @@ -18,7 +18,7 @@ it('ID3v2.4', async () => { t.deepEqual(nativeTags.UFID[0], { owner_identifier: 'http://musicbrainz.org', - identifier: Buffer.from([0x33, 0x66, 0x32, 0x33, 0x66, 0x32, 0x63, 0x66, 0x2d, + identifier: new Uint8Array([0x33, 0x66, 0x32, 0x33, 0x66, 0x32, 0x63, 0x66, 0x2d, 0x32, 0x61, 0x34, 0x36, 0x2d, 0x34, 0x38, 0x65, 0x63, 0x2d, 0x38, 0x36, 0x33, 0x65, 0x2d, 0x36, 0x65, 0x63, 0x34, 0x33, 0x31, 0x62, 0x35, 0x66, 0x65, 0x63, 0x61]) diff --git a/test/test-util.ts b/test/test-util.ts index 84d9d1203..3a6091ac5 100644 --- a/test/test-util.ts +++ b/test/test-util.ts @@ -12,28 +12,28 @@ describe('shared utility functionality', () => { const findZero = util.findZero; it('should find terminator in ascii encoded string', () => { - const buf = Buffer.from([0xFF, 0xFF, 0xFF, 0x00]); + const buf = Uint8Array.from([0xFF, 0xFF, 0xFF, 0x00]); t.equal(findZero(buf, 0, buf.length, 'ascii'), 3); }); it('find terminator in middle of ascii encoded string', () => { - const buf = Buffer.from([0xFF, 0xFF, 0x00, 0xFF, 0xFF]); + const buf = Uint8Array.from([0xFF, 0xFF, 0x00, 0xFF, 0xFF]); t.equal(findZero(buf, 0, buf.length, 'ascii'), 2); }); it('return offset to end if nothing is found', () => { - const buf = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + const buf = Uint8Array.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); t.equal(findZero(buf, 0, buf.length, 'ascii'), buf.length); }); it('find terminator in utf16le encoded string', () => { - const buf = Buffer.from([0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x00, 0x00]); - t.equal(findZero(buf, 0, buf.length, 'utf16le'), 10); + const buf = Uint8Array.from([0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x00, 0x00]); + t.equal(findZero(buf, 0, buf.length, 'utf-16le'), 10); }); it('find terminator in utf16be encoded string', () => { - const buf = Buffer.from([0x00, 0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x00]); - t.equal(findZero(buf, 0, buf.length, 'utf16le'), 8); + const buf = Uint8Array.from([0x00, 0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x00]); + t.equal(findZero(buf, 0, buf.length, 'utf-16le'), 8); }); }); @@ -102,7 +102,7 @@ describe('shared utility functionality', () => { it('should be able to encode FourCC token', () => { const buffer = Buffer.alloc(4); FourCcToken.put(buffer, 0, 'abcd'); - t.deepEqual(buffer.toString('binary'), 'abcd'); + t.deepEqual(buffer.toString('latin1'), 'abcd'); }); }); diff --git a/test/tsconfig.json b/test/tsconfig.json index c4224fed5..b2f667dcd 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "types": ["mocha"] + "types": ["mocha", "node"] } } diff --git a/test/util.ts b/test/util.ts index ff23927dc..c4c9b5b1f 100644 --- a/test/util.ts +++ b/test/util.ts @@ -12,7 +12,7 @@ const dirname = path.dirname(filename); */ export class SourceStream extends Readable { - constructor(private buf: Buffer) { + constructor(private buf: Uint8Array) { super(); } diff --git a/yarn.lock b/yarn.lock index 2d439c3ac..62c6a227f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1630,7 +1630,7 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-type@*, file-type@^18.6.0: +file-type@*: version "18.6.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-18.6.0.tgz#02f323cf0210291984280288d5eff2fcc1e98961" integrity sha512-uLqXnIAIyy8K9rnvdU9IYi3WIL+6qVBWn24kThYOPlnyU+6yrr2oarn+j7seMLh1wOEG4hEjRP6a30IiKR9OaA== @@ -1639,6 +1639,15 @@ file-type@*, file-type@^18.6.0: strtok3 "^7.0.0" token-types "^5.0.1" +file-type@^19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-19.1.0.tgz#c813c1575b30ec6a2689881a228fa447225c2367" + integrity sha512-5rzeC2/GeStiAlYCenfrbKrQCiEzJTetCExFinFCH1UUz1XL7NlxRpLTwdWXzlVhLReRrWkfkNCH1Ap5zqOXtg== + dependencies: + strtok3 "^7.1.0" + token-types "^6.0.0" + uint8array-extras "^1.3.0" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -3272,6 +3281,11 @@ peek-readable@^5.0.0: resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" integrity sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A== +peek-readable@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.1.1.tgz#7dbeafa1ce271a3eba3fba808883bdb03769b822" + integrity sha512-4hEOSH7KeEaZpMDF/xfm1W9fS5rT7Ett3BkXWHqAEzRLLwLaHkwOL+GvvpIEh9UrvX9BDhzfkvteslgraoH69w== + picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" @@ -4441,6 +4455,14 @@ strtok3@^7.0.0: "@tokenizer/token" "^0.3.0" peek-readable "^5.0.0" +strtok3@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-7.1.0.tgz#98856ed6651c2f7f0e8a436a54536a53528ecfb2" + integrity sha512-19dQEwG6Jd+VabjPRyBhymIF069vZiqWSZa2jJBoKJTsqGKnTxowGoQaLnz+yLARfDI041IUQekyPUMWElOgsQ== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^5.1.1" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -4514,6 +4536,14 @@ token-types@^5.0.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +token-types@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-6.0.0.tgz#1ab26be1ef9c434853500c071acfe5c8dd6544a3" + integrity sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA== + dependencies: + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" + trim-newlines@^4.0.2: version "4.1.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" @@ -4644,6 +4674,11 @@ typescript@^5.5.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== +uint8array-extras@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.3.0.tgz#0b2e511cfdd91e00b8f011ba6b850d7671f36fa2" + integrity sha512-npBAT0ZIX6mAIG7SF6G4LF1BIoRx3h+HVajSplHx0XmOD0Ug4qio5Yhcajn72i5OEj/qkk1OFaYh2PhqHBV33w== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"