Initial working version
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
import * as strtok3 from 'strtok3/lib/core';
|
||||
import { IOptions, IRandomReader, IApeHeader } from '../type';
|
||||
import { INativeMetadataCollector } from '../common/MetadataCollector';
|
||||
import { BasicParser } from '../common/BasicParser';
|
||||
import { IFooter, IHeader } from './APEv2Token';
|
||||
export declare class APEv2Parser extends BasicParser {
|
||||
static tryParseApeHeader(metadata: INativeMetadataCollector, tokenizer: strtok3.ITokenizer, options: IOptions): Promise<void>;
|
||||
/**
|
||||
* Calculate the media file duration
|
||||
* @param ah ApeHeader
|
||||
* @return {number} duration in seconds
|
||||
*/
|
||||
static calculateDuration(ah: IHeader): number;
|
||||
/**
|
||||
* Calculates the APEv1 / APEv2 first field offset
|
||||
* @param reader
|
||||
* @param offset
|
||||
*/
|
||||
static findApeFooterOffset(reader: IRandomReader, offset: number): Promise<IApeHeader>;
|
||||
private static parseTagFooter;
|
||||
private ape;
|
||||
/**
|
||||
* Parse APEv1 / APEv2 header if header signature found
|
||||
*/
|
||||
tryParseApeHeader(): Promise<void>;
|
||||
parse(): Promise<void>;
|
||||
parseTags(footer: IFooter): Promise<void>;
|
||||
private parseDescriptorExpansion;
|
||||
private parseHeader;
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.APEv2Parser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const strtok3 = require("strtok3/lib/core");
|
||||
const util = require("../common/Util");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const APEv2Token_1 = require("./APEv2Token");
|
||||
const token_types_1 = require("token-types");
|
||||
const debug = initDebug('music-metadata:parser:APEv2');
|
||||
const tagFormat = 'APEv2';
|
||||
const preamble = 'APETAGEX';
|
||||
class APEv2Parser extends BasicParser_1.BasicParser {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.ape = {};
|
||||
}
|
||||
static tryParseApeHeader(metadata, tokenizer, options) {
|
||||
const apeParser = new APEv2Parser();
|
||||
apeParser.init(metadata, tokenizer, options);
|
||||
return apeParser.tryParseApeHeader();
|
||||
}
|
||||
/**
|
||||
* Calculate the media file duration
|
||||
* @param ah ApeHeader
|
||||
* @return {number} duration in seconds
|
||||
*/
|
||||
static calculateDuration(ah) {
|
||||
let duration = ah.totalFrames > 1 ? ah.blocksPerFrame * (ah.totalFrames - 1) : 0;
|
||||
duration += ah.finalFrameBlocks;
|
||||
return duration / ah.sampleRate;
|
||||
}
|
||||
/**
|
||||
* Calculates the APEv1 / APEv2 first field offset
|
||||
* @param reader
|
||||
* @param offset
|
||||
*/
|
||||
static async findApeFooterOffset(reader, offset) {
|
||||
// Search for APE footer header at the end of the file
|
||||
const apeBuf = Buffer.alloc(APEv2Token_1.TagFooter.len);
|
||||
await reader.randomRead(apeBuf, 0, APEv2Token_1.TagFooter.len, offset - APEv2Token_1.TagFooter.len);
|
||||
const tagFooter = APEv2Token_1.TagFooter.get(apeBuf, 0);
|
||||
if (tagFooter.ID === 'APETAGEX') {
|
||||
debug(`APE footer header at offset=${offset}`);
|
||||
return { footer: tagFooter, offset: offset - tagFooter.size };
|
||||
}
|
||||
}
|
||||
static parseTagFooter(metadata, buffer, options) {
|
||||
const footer = APEv2Token_1.TagFooter.get(buffer, buffer.length - APEv2Token_1.TagFooter.len);
|
||||
if (footer.ID !== preamble)
|
||||
throw new Error('Unexpected APEv2 Footer ID preamble value.');
|
||||
strtok3.fromBuffer(buffer);
|
||||
const apeParser = new APEv2Parser();
|
||||
apeParser.init(metadata, strtok3.fromBuffer(buffer), options);
|
||||
return apeParser.parseTags(footer);
|
||||
}
|
||||
/**
|
||||
* Parse APEv1 / APEv2 header if header signature found
|
||||
*/
|
||||
async tryParseApeHeader() {
|
||||
if (this.tokenizer.fileInfo.size && this.tokenizer.fileInfo.size - this.tokenizer.position < APEv2Token_1.TagFooter.len) {
|
||||
debug(`No APEv2 header found, end-of-file reached`);
|
||||
return;
|
||||
}
|
||||
const footer = await this.tokenizer.peekToken(APEv2Token_1.TagFooter);
|
||||
if (footer.ID === preamble) {
|
||||
await this.tokenizer.ignore(APEv2Token_1.TagFooter.len);
|
||||
return this.parseTags(footer);
|
||||
}
|
||||
else {
|
||||
debug(`APEv2 header not found at offset=${this.tokenizer.position}`);
|
||||
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);
|
||||
await this.tokenizer.readBuffer(buffer);
|
||||
return APEv2Parser.parseTagFooter(this.metadata, buffer, this.options);
|
||||
}
|
||||
}
|
||||
}
|
||||
async parse() {
|
||||
const descriptor = await this.tokenizer.readToken(APEv2Token_1.DescriptorParser);
|
||||
if (descriptor.ID !== 'MAC ')
|
||||
throw new Error('Unexpected descriptor ID');
|
||||
this.ape.descriptor = descriptor;
|
||||
const lenExp = descriptor.descriptorBytes - APEv2Token_1.DescriptorParser.len;
|
||||
const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader());
|
||||
await this.tokenizer.ignore(header.forwardBytes);
|
||||
return this.tryParseApeHeader();
|
||||
}
|
||||
async parseTags(footer) {
|
||||
const keyBuffer = Buffer.alloc(256); // maximum tag key length
|
||||
let bytesRemaining = footer.size - APEv2Token_1.TagFooter.len;
|
||||
debug(`Parse APE tags at offset=${this.tokenizer.position}, size=${bytesRemaining}`);
|
||||
for (let i = 0; i < footer.fields; i++) {
|
||||
if (bytesRemaining < APEv2Token_1.TagItemHeader.len) {
|
||||
this.metadata.addWarning(`APEv2 Tag-header: ${footer.fields - i} items remaining, but no more tag data to read.`);
|
||||
break;
|
||||
}
|
||||
// Only APEv2 tag has tag item headers
|
||||
const tagItemHeader = await this.tokenizer.readToken(APEv2Token_1.TagItemHeader);
|
||||
bytesRemaining -= APEv2Token_1.TagItemHeader.len + tagItemHeader.size;
|
||||
await this.tokenizer.peekBuffer(keyBuffer, { length: Math.min(keyBuffer.length, bytesRemaining) });
|
||||
let zero = util.findZero(keyBuffer, 0, keyBuffer.length);
|
||||
const key = await this.tokenizer.readToken(new token_types_1.StringType(zero, 'ascii'));
|
||||
await this.tokenizer.ignore(1);
|
||||
bytesRemaining -= key.length + 1;
|
||||
switch (tagItemHeader.flags.dataType) {
|
||||
case APEv2Token_1.DataType.text_utf8: { // utf-8 textstring
|
||||
const value = await this.tokenizer.readToken(new token_types_1.StringType(tagItemHeader.size, 'utf8'));
|
||||
const values = value.split(/\x00/g);
|
||||
/*jshint loopfunc:true */
|
||||
for (const val of values) {
|
||||
this.metadata.addTag(tagFormat, key, val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APEv2Token_1.DataType.binary: // binary (probably artwork)
|
||||
if (this.options.skipCovers) {
|
||||
await this.tokenizer.ignore(tagItemHeader.size);
|
||||
}
|
||||
else {
|
||||
const picData = Buffer.alloc(tagItemHeader.size);
|
||||
await this.tokenizer.readBuffer(picData);
|
||||
zero = util.findZero(picData, 0, picData.length);
|
||||
const description = picData.toString('utf8', 0, zero);
|
||||
const data = Buffer.from(picData.slice(zero + 1));
|
||||
this.metadata.addTag(tagFormat, key, {
|
||||
description,
|
||||
data
|
||||
});
|
||||
}
|
||||
break;
|
||||
case APEv2Token_1.DataType.external_info:
|
||||
debug(`Ignore external info ${key}`);
|
||||
await this.tokenizer.ignore(tagItemHeader.size);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected data-type: ${tagItemHeader.flags.dataType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
async parseDescriptorExpansion(lenExp) {
|
||||
await this.tokenizer.ignore(lenExp);
|
||||
return this.parseHeader();
|
||||
}
|
||||
async parseHeader() {
|
||||
const header = await this.tokenizer.readToken(APEv2Token_1.Header);
|
||||
// ToDo before
|
||||
this.metadata.setFormat('lossless', true);
|
||||
this.metadata.setFormat('container', 'Monkey\'s Audio');
|
||||
this.metadata.setFormat('bitsPerSample', header.bitsPerSample);
|
||||
this.metadata.setFormat('sampleRate', header.sampleRate);
|
||||
this.metadata.setFormat('numberOfChannels', header.channel);
|
||||
this.metadata.setFormat('duration', APEv2Parser.calculateDuration(header));
|
||||
return {
|
||||
forwardBytes: this.ape.descriptor.seekTableBytes + this.ape.descriptor.headerDataBytes +
|
||||
this.ape.descriptor.apeFrameDataBytes + this.ape.descriptor.terminatingDataBytes
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.APEv2Parser = APEv2Parser;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { CaseInsensitiveTagMap } from '../common/CaseInsensitiveTagMap';
|
||||
export declare class APEv2TagMapper extends CaseInsensitiveTagMap {
|
||||
constructor();
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.APEv2TagMapper = void 0;
|
||||
const CaseInsensitiveTagMap_1 = require("../common/CaseInsensitiveTagMap");
|
||||
/**
|
||||
* ID3v2.2 tag mappings
|
||||
*/
|
||||
const apev2TagMap = {
|
||||
Title: 'title',
|
||||
Artist: 'artist',
|
||||
Artists: 'artists',
|
||||
'Album Artist': 'albumartist',
|
||||
Album: 'album',
|
||||
Year: 'date',
|
||||
Originalyear: 'originalyear',
|
||||
Originaldate: 'originaldate',
|
||||
Comment: 'comment',
|
||||
Track: 'track',
|
||||
Disc: 'disk',
|
||||
DISCNUMBER: 'disk',
|
||||
Genre: 'genre',
|
||||
'Cover Art (Front)': 'picture',
|
||||
'Cover Art (Back)': 'picture',
|
||||
Composer: 'composer',
|
||||
Lyrics: 'lyrics',
|
||||
ALBUMSORT: 'albumsort',
|
||||
TITLESORT: 'titlesort',
|
||||
WORK: 'work',
|
||||
ARTISTSORT: 'artistsort',
|
||||
ALBUMARTISTSORT: 'albumartistsort',
|
||||
COMPOSERSORT: 'composersort',
|
||||
Lyricist: 'lyricist',
|
||||
Writer: 'writer',
|
||||
Conductor: 'conductor',
|
||||
// 'Performer=artist (instrument)': 'performer:instrument',
|
||||
MixArtist: 'remixer',
|
||||
Arranger: 'arranger',
|
||||
Engineer: 'engineer',
|
||||
Producer: 'producer',
|
||||
DJMixer: 'djmixer',
|
||||
Mixer: 'mixer',
|
||||
Label: 'label',
|
||||
Grouping: 'grouping',
|
||||
Subtitle: 'subtitle',
|
||||
DiscSubtitle: 'discsubtitle',
|
||||
Compilation: 'compilation',
|
||||
BPM: 'bpm',
|
||||
Mood: 'mood',
|
||||
Media: 'media',
|
||||
CatalogNumber: 'catalognumber',
|
||||
MUSICBRAINZ_ALBUMSTATUS: 'releasestatus',
|
||||
MUSICBRAINZ_ALBUMTYPE: 'releasetype',
|
||||
RELEASECOUNTRY: 'releasecountry',
|
||||
Script: 'script',
|
||||
Language: 'language',
|
||||
Copyright: 'copyright',
|
||||
LICENSE: 'license',
|
||||
EncodedBy: 'encodedby',
|
||||
EncoderSettings: 'encodersettings',
|
||||
Barcode: 'barcode',
|
||||
ISRC: 'isrc',
|
||||
ASIN: 'asin',
|
||||
musicbrainz_trackid: 'musicbrainz_recordingid',
|
||||
musicbrainz_releasetrackid: 'musicbrainz_trackid',
|
||||
MUSICBRAINZ_ALBUMID: 'musicbrainz_albumid',
|
||||
MUSICBRAINZ_ARTISTID: 'musicbrainz_artistid',
|
||||
MUSICBRAINZ_ALBUMARTISTID: 'musicbrainz_albumartistid',
|
||||
MUSICBRAINZ_RELEASEGROUPID: 'musicbrainz_releasegroupid',
|
||||
MUSICBRAINZ_WORKID: 'musicbrainz_workid',
|
||||
MUSICBRAINZ_TRMID: 'musicbrainz_trmid',
|
||||
MUSICBRAINZ_DISCID: 'musicbrainz_discid',
|
||||
Acoustid_Id: 'acoustid_id',
|
||||
ACOUSTID_FINGERPRINT: 'acoustid_fingerprint',
|
||||
MUSICIP_PUID: 'musicip_puid',
|
||||
Weblink: 'website',
|
||||
REPLAYGAIN_TRACK_GAIN: 'replaygain_track_gain',
|
||||
REPLAYGAIN_TRACK_PEAK: 'replaygain_track_peak',
|
||||
MP3GAIN_MINMAX: 'replaygain_track_minmax',
|
||||
MP3GAIN_UNDO: 'replaygain_undo'
|
||||
};
|
||||
class APEv2TagMapper extends CaseInsensitiveTagMap_1.CaseInsensitiveTagMap {
|
||||
constructor() {
|
||||
super(['APEv2'], apev2TagMap);
|
||||
}
|
||||
}
|
||||
exports.APEv2TagMapper = APEv2TagMapper;
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import * as Token from 'token-types';
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* APETag versionIndex history / supported formats
|
||||
*
|
||||
* 1.0 (1000) - Original APE tag spec. Fully supported by this code.
|
||||
* 2.0 (2000) - Refined APE tag spec (better streaming support, UTF StringEncoding). Fully supported by this code.
|
||||
*
|
||||
* Notes:
|
||||
* - also supports reading of ID3v1.1 tags
|
||||
* - all saving done in the APE Tag format using CURRENT_APE_TAG_VERSION
|
||||
*
|
||||
* APE File Format Overview: (pieces in order -- only valid for the latest versionIndex APE files)
|
||||
*
|
||||
* JUNK - any amount of "junk" before the APE_DESCRIPTOR (so people that put ID3v2 tags on the files aren't hosed)
|
||||
* APE_DESCRIPTOR - defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
|
||||
* APE_HEADER - describes all of the necessary information about the APE file
|
||||
* SEEK TABLE - the table that represents seek offsets [optional]
|
||||
* HEADER DATA - the pre-audio data from the original file [optional]
|
||||
* APE FRAMES - the actual compressed audio (broken into frames for seekability)
|
||||
* TERMINATING DATA - the post-audio data from the original file [optional]
|
||||
* TAG - describes all the properties of the file [optional]
|
||||
*/
|
||||
export interface IDescriptor {
|
||||
ID: string;
|
||||
version: number;
|
||||
descriptorBytes: number;
|
||||
headerBytes: number;
|
||||
seekTableBytes: number;
|
||||
headerDataBytes: number;
|
||||
apeFrameDataBytes: number;
|
||||
apeFrameDataBytesHigh: number;
|
||||
terminatingDataBytes: number;
|
||||
fileMD5: Uint8Array;
|
||||
}
|
||||
/**
|
||||
* APE_HEADER: describes all of the necessary information about the APE file
|
||||
*/
|
||||
export interface IHeader {
|
||||
compressionLevel: number;
|
||||
formatFlags: number;
|
||||
blocksPerFrame: number;
|
||||
finalFrameBlocks: number;
|
||||
totalFrames: number;
|
||||
bitsPerSample: number;
|
||||
channel: number;
|
||||
sampleRate: number;
|
||||
}
|
||||
export interface IFooter {
|
||||
ID: string;
|
||||
version: number;
|
||||
size: number;
|
||||
fields: number;
|
||||
flags: ITagFlags;
|
||||
}
|
||||
export declare enum DataType {
|
||||
text_utf8 = 0,
|
||||
binary = 1,
|
||||
external_info = 2,
|
||||
reserved = 3
|
||||
}
|
||||
/**
|
||||
* APE_DESCRIPTOR: defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
|
||||
*/
|
||||
export declare const DescriptorParser: IGetToken<IDescriptor>;
|
||||
/**
|
||||
* APE_HEADER: describes all of the necessary information about the APE file
|
||||
*/
|
||||
export declare const Header: IGetToken<IHeader>;
|
||||
/**
|
||||
* APE Tag Header/Footer Version 2.0
|
||||
* TAG: describes all the properties of the file [optional]
|
||||
*/
|
||||
export declare const TagFooter: IGetToken<IFooter>;
|
||||
/**
|
||||
* APE Tag v2.0 Item Header
|
||||
*/
|
||||
export interface ITagItemHeader {
|
||||
size: number;
|
||||
flags: ITagFlags;
|
||||
}
|
||||
/**
|
||||
* APE Tag v2.0 Item Header
|
||||
*/
|
||||
export declare const TagItemHeader: IGetToken<ITagItemHeader>;
|
||||
export declare const TagField: (footer: any) => Token.Uint8ArrayType;
|
||||
export interface ITagFlags {
|
||||
containsHeader: boolean;
|
||||
containsFooter: boolean;
|
||||
isHeader: boolean;
|
||||
readOnly: boolean;
|
||||
dataType: DataType;
|
||||
}
|
||||
export declare function parseTagFlags(flags: any): ITagFlags;
|
||||
/**
|
||||
* @param num {number}
|
||||
* @param bit 0 is least significant bit (LSB)
|
||||
* @return {boolean} true if bit is 1; otherwise false
|
||||
*/
|
||||
export declare function isBitSet(num: any, bit: any): boolean;
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isBitSet = exports.parseTagFlags = exports.TagField = exports.TagItemHeader = exports.TagFooter = exports.Header = exports.DescriptorParser = exports.DataType = void 0;
|
||||
const Token = require("token-types");
|
||||
const FourCC_1 = require("../common/FourCC");
|
||||
var DataType;
|
||||
(function (DataType) {
|
||||
DataType[DataType["text_utf8"] = 0] = "text_utf8";
|
||||
DataType[DataType["binary"] = 1] = "binary";
|
||||
DataType[DataType["external_info"] = 2] = "external_info";
|
||||
DataType[DataType["reserved"] = 3] = "reserved";
|
||||
})(DataType = exports.DataType || (exports.DataType = {}));
|
||||
/**
|
||||
* APE_DESCRIPTOR: defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum
|
||||
*/
|
||||
exports.DescriptorParser = {
|
||||
len: 52,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
// should equal 'MAC '
|
||||
ID: FourCC_1.FourCcToken.get(buf, off),
|
||||
// versionIndex number * 1000 (3.81 = 3810) (remember that 4-byte alignment causes this to take 4-bytes)
|
||||
version: Token.UINT32_LE.get(buf, off + 4) / 1000,
|
||||
// the number of descriptor bytes (allows later expansion of this header)
|
||||
descriptorBytes: Token.UINT32_LE.get(buf, off + 8),
|
||||
// the number of header APE_HEADER bytes
|
||||
headerBytes: Token.UINT32_LE.get(buf, off + 12),
|
||||
// the number of header APE_HEADER bytes
|
||||
seekTableBytes: Token.UINT32_LE.get(buf, off + 16),
|
||||
// the number of header data bytes (from original file)
|
||||
headerDataBytes: Token.UINT32_LE.get(buf, off + 20),
|
||||
// the number of bytes of APE frame data
|
||||
apeFrameDataBytes: Token.UINT32_LE.get(buf, off + 24),
|
||||
// the high order number of APE frame data bytes
|
||||
apeFrameDataBytesHigh: Token.UINT32_LE.get(buf, off + 28),
|
||||
// the terminating data of the file (not including tag data)
|
||||
terminatingDataBytes: Token.UINT32_LE.get(buf, off + 32),
|
||||
// the MD5 hash of the file (see notes for usage... it's a little tricky)
|
||||
fileMD5: new Token.Uint8ArrayType(16).get(buf, off + 36)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* APE_HEADER: describes all of the necessary information about the APE file
|
||||
*/
|
||||
exports.Header = {
|
||||
len: 24,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
// the compression level (see defines I.E. COMPRESSION_LEVEL_FAST)
|
||||
compressionLevel: Token.UINT16_LE.get(buf, off),
|
||||
// any format flags (for future use)
|
||||
formatFlags: Token.UINT16_LE.get(buf, off + 2),
|
||||
// the number of audio blocks in one frame
|
||||
blocksPerFrame: Token.UINT32_LE.get(buf, off + 4),
|
||||
// the number of audio blocks in the final frame
|
||||
finalFrameBlocks: Token.UINT32_LE.get(buf, off + 8),
|
||||
// the total number of frames
|
||||
totalFrames: Token.UINT32_LE.get(buf, off + 12),
|
||||
// the bits per sample (typically 16)
|
||||
bitsPerSample: Token.UINT16_LE.get(buf, off + 16),
|
||||
// the number of channels (1 or 2)
|
||||
channel: Token.UINT16_LE.get(buf, off + 18),
|
||||
// the sample rate (typically 44100)
|
||||
sampleRate: Token.UINT32_LE.get(buf, off + 20)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* APE Tag Header/Footer Version 2.0
|
||||
* TAG: describes all the properties of the file [optional]
|
||||
*/
|
||||
exports.TagFooter = {
|
||||
len: 32,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
// should equal 'APETAGEX'
|
||||
ID: new Token.StringType(8, 'ascii').get(buf, off),
|
||||
// equals CURRENT_APE_TAG_VERSION
|
||||
version: Token.UINT32_LE.get(buf, off + 8),
|
||||
// the complete size of the tag, including this footer (excludes header)
|
||||
size: Token.UINT32_LE.get(buf, off + 12),
|
||||
// the number of fields in the tag
|
||||
fields: Token.UINT32_LE.get(buf, off + 16),
|
||||
// reserved for later use (must be zero),
|
||||
flags: parseTagFlags(Token.UINT32_LE.get(buf, off + 20))
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* APE Tag v2.0 Item Header
|
||||
*/
|
||||
exports.TagItemHeader = {
|
||||
len: 8,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
// Length of assigned value in bytes
|
||||
size: Token.UINT32_LE.get(buf, off),
|
||||
// reserved for later use (must be zero),
|
||||
flags: parseTagFlags(Token.UINT32_LE.get(buf, off + 4))
|
||||
};
|
||||
}
|
||||
};
|
||||
const TagField = footer => {
|
||||
return new Token.Uint8ArrayType(footer.size - exports.TagFooter.len);
|
||||
};
|
||||
exports.TagField = TagField;
|
||||
function parseTagFlags(flags) {
|
||||
return {
|
||||
containsHeader: isBitSet(flags, 31),
|
||||
containsFooter: isBitSet(flags, 30),
|
||||
isHeader: isBitSet(flags, 31),
|
||||
readOnly: isBitSet(flags, 0),
|
||||
dataType: (flags & 6) >> 1
|
||||
};
|
||||
}
|
||||
exports.parseTagFlags = parseTagFlags;
|
||||
/**
|
||||
* @param num {number}
|
||||
* @param bit 0 is least significant bit (LSB)
|
||||
* @return {boolean} true if bit is 1; otherwise false
|
||||
*/
|
||||
function isBitSet(num, bit) {
|
||||
return (num & 1 << bit) !== 0;
|
||||
}
|
||||
exports.isBitSet = isBitSet;
|
||||
Reference in New Issue
Block a user