• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benrr101 / node-taglib-sharp / 46462135

pending completion
46462135

push

appveyor

Benjamin Russell
Merge branch 'release/v5.1.0'

3096 of 3788 branches covered (81.73%)

Branch coverage included in aggregate %.

2171 of 2171 new or added lines in 47 files covered. (100.0%)

25320 of 26463 relevant lines covered (95.68%)

437.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.17
/src/mpeg/mpegAudioHeader.ts
1
import XingHeader from "./xingHeader";
1✔
2
import VbriHeader from "./vbriHeader";
1✔
3
import {ByteVector} from "../byteVector";
4
import {CorruptFileError} from "../errors";
1✔
5
import {File} from "../file";
1✔
6
import {IAudioCodec, MediaTypes} from "../properties";
1✔
7
import {ChannelMode, MpegVersion} from "./mpegEnums";
1✔
8
import {Guards, NumberUtils} from "../utils";
1✔
9

10
/**
11
 * Provides information about an MPEG audio stream. For more information and definition of the
12
 * header, see http://www.mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
13
 */
14
export default class MpegAudioHeader implements IAudioCodec {
1✔
15
    // @TODO: make an enum for header flags
16

17
    /**
18
     * Static instance of an audio header that has unknown information.
19
     */
20
    public static readonly UNKNOWN: MpegAudioHeader = MpegAudioHeader.fromInfo(
1✔
21
        0,
22
        0,
23
        XingHeader.UNKNOWN,
24
        VbriHeader.UNKNOWN
25
    );
26

27
    private static readonly BITRATES: number[][][] = [
1✔
28
        [ // Version 1
29
            [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1], // layer 1
30
            [0, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384, -1], // layer 2
31
            [0, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, -1]  // layer 3
32
        ],
33
        [ // Version 2 or 2.5
34
            [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1], // layer 1
35
            [0,  8, 16, 24, 32, 40, 48,  56,  64,  80,  96, 112, 128, 144, 160, -1], // layer 2
36
            [0,  8, 16, 24, 32, 40, 48,  56,  64,  80,  96, 112, 128, 144, 160, -1]  // layer 3
37
        ]
38
    ];
39

40
    private static readonly BLOCK_SIZES: number[][] = [
1✔
41
        [0, 384, 1152, 1152], // Version 1
42
        [0, 384, 1152,  576], // Version 2
43
        [0, 384, 1152,  576]  // Version 2.5
44
    ];
45

46
    private static readonly SAMPLE_RATES: number[][] = [
1✔
47
        [44100, 48000, 32000, 0], // Version 1
48
        [22050, 24000, 16000, 0], // Version 2
49
        [11025, 12000,  8000, 0]  // Version 2.5
50
    ];
51

52
    private _durationMilliseconds: number;
53
    private _flags: number;
54
    private _streamLength: number;
55
    private _vbriHeader: VbriHeader;
56
    private _xingHeader: XingHeader;
57

58
    // #region Constructors
59

60
    private constructor() { /* private to enforce construction via static methods */ }
61

62
    /**
63
     * Constructs and initializes a new instance by reading its contents from a data
64
     * {@link ByteVector} and its Xing header from the appropriate location in the
65
     * specified file.
66
     * @param data The header data to read
67
     * @param file File to read the Xing/VBRI header from
68
     * @param position Position into `file` where the header begins, must be a positive
69
     *     8-bit integer.
70
     */
71
    public static fromData(data: ByteVector, file: File, position: number): MpegAudioHeader {
72
        Guards.truthy(data, "data");
19✔
73
        Guards.truthy(file, "file");
17✔
74
        Guards.safeUint(position, "position");
15✔
75

76
        const header = new MpegAudioHeader();
11✔
77
        header._durationMilliseconds = 0;
11✔
78
        header._streamLength = 0;
11✔
79

80
        const error = this.getHeaderError(data);
11✔
81
        if (error) {
11✔
82
            throw new CorruptFileError(error);
7✔
83
        }
84

85
        header._flags = data.toUint();
4✔
86
        header._xingHeader = XingHeader.UNKNOWN;
4✔
87
        header._vbriHeader = VbriHeader.UNKNOWN;
4✔
88

89
        // Check for a Xing header that will help us in gathering info about a VBR stream
90
        file.seek(position + XingHeader.xingHeaderOffset(header.version, header.channelMode));
4✔
91

92
        const xingData = file.readBlock(16);
4✔
93
        if (xingData.length === 16 && xingData.startsWith(XingHeader.FILE_IDENTIFIER)) {
4✔
94
            header._xingHeader = XingHeader.fromData(xingData);
1✔
95
        }
96

97
        if (header._xingHeader.isPresent) {
4✔
98
            return header;
1✔
99
        }
100

101
        // A Xing header could not be found, next check for a Fraunhofer VBRI header
102
        file.seek(position + VbriHeader.VBRI_HEADER_OFFSET);
3✔
103

104
        // Only get the first 24 bytes of the header. We're not interested in the TOC entries.
105
        const vbriData = file.readBlock(24);
3✔
106
        if (vbriData.length === 24 && vbriData.startsWith(VbriHeader.FILE_IDENTIFIER)) {
3✔
107
            header._vbriHeader = VbriHeader.fromData(vbriData);
1✔
108
        }
109

110
        return header;
3✔
111
    }
112

113
    /**
114
     * Constructs and initializes a new instance by populating it with specified values.
115
     * @param flags Flags for the new instance
116
     * @param streamLength Stream length of the new instance
117
     * @param xingHeader Xing header associated with the new instance
118
     * @param vbriHeader VBRI header associated with the new instance
119
     */
120
    public static fromInfo(
121
        flags: number,
122
        streamLength: number,
123
        xingHeader: XingHeader,
124
        vbriHeader: VbriHeader
125
    ): MpegAudioHeader {
126
        Guards.uint(flags, "flags");
59✔
127
        Guards.safeUint(streamLength, "streamLength");
54✔
128
        Guards.truthy(xingHeader, "xingHeader");
49✔
129
        Guards.truthy(vbriHeader, "vbriHeader");
47✔
130

131
        const header = new MpegAudioHeader();
45✔
132
        header._flags = flags;
45✔
133
        header._streamLength = streamLength;
45✔
134
        header._xingHeader = xingHeader;
45✔
135
        header._vbriHeader = vbriHeader;
45✔
136
        header._durationMilliseconds = 0;
45✔
137

138
        return header;
45✔
139
    }
140

141
    // #endregion
142

143
    // #region Properties
144

145
    /** @inheritDoc */
146
    public get audioBitrate(): number {
147
        // NOTE: Although it would be *amazing* to store `this.durationMilliseconds / 1000` in a
148
        //    variable, we can't b/c it causes a stack overflow. Oh well.
149
        if (
77✔
150
            this._xingHeader.totalSize > 0 &&
90✔
151
            this._xingHeader.totalFrames > 0 &&
152
            this.durationMilliseconds / 1000 > 0
153
        ) {
154
            return Math.round(this._xingHeader.totalSize * 8 / (this.durationMilliseconds / 1000) / 1000);
3✔
155
        }
156
        if (
74✔
157
            this._vbriHeader.totalSize > 0 &&
87✔
158
            this._vbriHeader.totalFrames > 0 &&
159
            this.durationMilliseconds / 1000 > 0
160
        ) {
161
            return Math.round(this._vbriHeader.totalSize * 8 / (this.durationMilliseconds / 1000) / 1000);
3✔
162
        }
163

164
        const index1 = this.version === MpegVersion.Version1 ? 0 : 1;
71✔
165
        const index2 = this.audioLayer - 1;
71✔
166
        const index3 = NumberUtils.uintAnd(NumberUtils.uintRShift(this._flags, 12), 0x0f);
71✔
167
        return MpegAudioHeader.BITRATES[index1][index2][index3];
71✔
168
    }
169

170
    /** @inheritDoc */
171
    public get audioChannels(): number { return this.channelMode === ChannelMode.SingleChannel ? 1 : 2; }
9✔
172

173
    /**
174
     * Gets the length of the frames in the audio represented by the current instance.
175
     */
176
    public get audioFrameLength(): number {
177
        const audioLayer = this.audioLayer;
47✔
178
        if (audioLayer === 1) {
47✔
179
            return Math.floor(48000 * this.audioBitrate / this.audioSampleRate) + (this.isPadded ? 4 : 0);
20✔
180
        }
181
        if (audioLayer === 2 || this.version === MpegVersion.Version1) {
27✔
182
            return Math.floor(144000 * this.audioBitrate / this.audioSampleRate) + (this.isPadded ? 1 : 0);
21✔
183
        }
184
        if (audioLayer === 3) {
6!
185
            return Math.floor(72000 * this.audioBitrate / this.audioSampleRate) + (this.isPadded ? 1 : 0);
6✔
186
        }
187
        return 0;
×
188
    }
189

190
    /**
191
     * Gets the MPEG audio layer used to encode the audio represented by the current instance.
192
     */
193
    public get audioLayer(): number {
194
        switch (NumberUtils.uintAnd(NumberUtils.uintRShift(this._flags, 17), 0x03)) {
150✔
195
            case 1:
150✔
196
                return 3;
21✔
197
            case 2:
198
                return 2;
57✔
199
            default:
200
                return 1;
72✔
201
        }
202
    }
203

204
    /** @inheritDoc */
205
    public get audioSampleRate(): number {
206
        const index1 = this.version;
63✔
207
        const index2 = NumberUtils.uintAnd(NumberUtils.uintRShift(this._flags, 10), 0x03);
63✔
208
        return MpegAudioHeader.SAMPLE_RATES[index1][index2];
63✔
209
    }
210

211
    /**
212
     * Gets the MPEG audio channel mode of the audio represented by the current instance.
213
     */
214
    public get channelMode(): ChannelMode { return NumberUtils.uintAnd(NumberUtils.uintRShift(this._flags, 6), 0x03); }
22✔
215

216
    /** @inheritDoc */
217
    public get description(): string {
218
        let builder = "MPEG Version ";
5✔
219
        switch (this.version) {
5✔
220
            case MpegVersion.Version1:
5✔
221
                builder += "1";
2✔
222
                break;
2✔
223
            case MpegVersion.Version2:
224
                builder += "2";
1✔
225
                break;
1✔
226
            case MpegVersion.Version25:
227
                builder += "2.5";
2✔
228
                break;
2✔
229
        }
230
        builder += ` Audio, Layer ${this.audioLayer}`;
5✔
231

232
        if (this._xingHeader.isPresent || this._vbriHeader.isPresent) {
5✔
233
            builder += " VBR";
2✔
234
        }
235

236
        return builder;
5✔
237
    }
238

239
    /** @inheritDoc */
240
    public get durationMilliseconds(): number {
241
        if (this._durationMilliseconds > 0) { return this._durationMilliseconds; }
32✔
242

243
        const blockSizeForVersionAndLayer = MpegAudioHeader.BLOCK_SIZES[this.version][this.audioLayer];
18✔
244
        if (this._xingHeader.totalFrames > 0) {
18✔
245
            // Read the length and the bitrate from the Xing header
246
            const timePerFrameSeconds = blockSizeForVersionAndLayer / this.audioSampleRate;
4✔
247
            const durationSeconds = timePerFrameSeconds * this._xingHeader.totalFrames;
4✔
248
            this._durationMilliseconds = durationSeconds * 1000;
4✔
249
        } else if (this._vbriHeader.totalFrames > 0) {
14✔
250
            // Read the length and the bitrate from the VBRI header
251
            const timePerFrameSeconds = blockSizeForVersionAndLayer / this.audioSampleRate;
4✔
252
            const durationSeconds = Math.round(timePerFrameSeconds * this._vbriHeader.totalFrames);
4✔
253
            this._durationMilliseconds = durationSeconds * 1000;
4✔
254
        } else if (this.audioFrameLength > 0 && this.audioBitrate > 0) {
10✔
255
            // Since there was no valid Xing or VBRI header found, we hope that we're in a constant
256
            // bitrate file
257

258
            // Round off to upper integer value
259
            const frames = Math.floor((this._streamLength + this.audioFrameLength - 1) / this.audioFrameLength);
8✔
260
            const durationSeconds = (this.audioFrameLength * frames) / (this.audioBitrate * 125);
8✔
261
            this._durationMilliseconds = durationSeconds * 1000;
8✔
262
        }
263

264
        return this._durationMilliseconds;
18✔
265
    }
266

267
    // TODO: Introduce an MPEG flags enum
268

269
    /**
270
     * Whether or not the current audio is copyrighted.
271
     */
272
    public get isCopyrighted(): boolean { return ((this._flags >> 3) & 1) === 1; }
7✔
273

274
    /**
275
     * Whether or not the current audio is original.
276
     */
277
    public get isOriginal(): boolean { return ((this._flags >> 2) & 1) === 1; }
9✔
278

279
    /**
280
     * Whether or not the audio represented by the current instance is padded.
281
     */
282
    public get isPadded(): boolean { return ((this._flags >> 9) & 1) === 1; }
52✔
283

284
    /**
285
     * Gets whether the audio represented by the current instance is protected by CRC.
286
     */
287
    public get isProtected(): boolean { return ((this._flags >> 16) & 1) === 0; }
7✔
288

289
    /** @inheritDoc */
290
    public get mediaTypes(): MediaTypes { return MediaTypes.Audio; }
5✔
291

292
    /**
293
     * Sets the length of the audio stream represented by the current instance.
294
     * If this value has not been set, {@link durationMilliseconds} will return an incorrect value.
295
     * @internal This is intended to be set when the file is read.
296
     */
297
    public set streamLength(value: number) {
298
        Guards.safeUint(value, "value");
3✔
299
        this._streamLength = value;
3✔
300

301
        // Force the recalculation of duration if it depends on the stream length.
302
        if (this._xingHeader.totalFrames === 0 && this._vbriHeader.totalFrames === 0) {
3✔
303
            this._durationMilliseconds = 0;
1✔
304
        }
305
    }
306

307
    /**
308
     * Gets the VBRI header found in the audio. {@link VbriHeader.UNKNOWN} is returned if no header
309
     * was found.
310
     */
311
    public get vbriHeader(): VbriHeader { return this._vbriHeader; }
9✔
312

313
    /**
314
     * Gets the MPEG version used to encode the audio represented by the current instance.
315
     */
316
    public get version(): MpegVersion {
317
        switch ((this._flags >> 19) & 0x03) {
178✔
318
            case 0:
178✔
319
                return MpegVersion.Version25;
42✔
320
            case 2:
321
                return MpegVersion.Version2;
63✔
322
            default:
323
                return MpegVersion.Version1;
73✔
324
        }
325
    }
326

327
    /**
328
     * Gets the Xing header found in the audio. {@link XingHeader.UNKNOWN} is returned if no header
329
     * was found.
330
     */
331
    public get xingHeader(): XingHeader { return this._xingHeader; }
9✔
332

333
    // #endregion
334

335
    /**
336
     * Searches for an audio header in a file starting at a specified position and searching
337
     * through a specified number of bytes.
338
     * @param file File to search
339
     * @param position Position in `file` at which to start searching
340
     * @param length Maximum number of bytes to search before giving up. Defaults to `-1` to
341
     *     have no maximum
342
     * @returns The header that was found or `undefined` if a header was not found
343
     */
344
    public static find(file: File, position: number, length?: number): MpegAudioHeader {
345
        Guards.truthy(file, "file");
15✔
346
        Guards.safeUint(position, "position");
13✔
347
        if (length !== undefined) {
8✔
348
            Guards.uint(length, "length");
5✔
349
        }
350

351
        const end = position + (length || 0);
4✔
352

353
        file.seek(position);
4✔
354
        let buffer = file.readBlock(3);
4✔
355

356
        if (buffer.length < 3) {
4✔
357
            return undefined;
1✔
358
        }
359

360
        do {
3✔
361
            // @TODO: ugh, this has that bizarre 3 byteoffset into each read, remove it
362
            file.seek(position + 3);
5✔
363
            buffer = buffer.subarray(buffer.length - 3);
5✔
364
            buffer.addByteVector(file.readBlock(File.bufferSize));
5✔
365

366
            for (let i = 0; i < buffer.length - 3 && (length === undefined || position + i < end); i++) {
5✔
367
                if (buffer.get(i) === 0xFF && buffer.get(i + 1) > 0xE0) {
4,092✔
368
                    const data = buffer.subarray(i, 4);
1✔
369
                    if (!this.getHeaderError(data)) {
1!
370
                        try {
1✔
371
                            return MpegAudioHeader.fromData(data, file, position + i);
1✔
372
                        } catch (e) {
373
                            if (!(e instanceof CorruptFileError)) {
×
374
                                throw e;
×
375
                            }
376
                        }
377
                    }
378
                }
379
            }
380

381
            position += File.bufferSize;
4✔
382
        } while (buffer.length > 3 && (length === undefined || position < end));
8✔
383

384
        return undefined;
2✔
385
    }
386

387
    private static getHeaderError(data: ByteVector): string {
388
        if (data.length < 4) {
12✔
389
            return "Insufficient header length";
2✔
390
        }
391
        if (data.get(0) !== 0xFF) {
10✔
392
            return "First byte did not match MPEG sync";
1✔
393
        }
394

395
        // Checking bits from high to low:
396
        // - First 3 bytes MUST be set
397
        // - Bits 4 and 5 can be 00, 10, or 11 but not 01
398
        // - One or more of bits 6 and 7 must be set
399
        // - Bit 8 can be anything
400
        if (NumberUtils.uintAnd(data.get(1), 0xE6) <= 0xE0 || NumberUtils.uintAnd(data.get(1), 0x18) === 0x08) {
9✔
401
            return "Second byte did not match MPEG sync";
2✔
402
        }
403

404
        const flags = data.toUint();
7✔
405
        if (NumberUtils.hasFlag(NumberUtils.uintRShift(flags, 12), 0x0F, true)) {
7✔
406
            return "Header uses invalid bitrate index";
1✔
407
        }
408
        if (NumberUtils.hasFlag(NumberUtils.uintRShift(flags, 10), 0x03, true)) {
6✔
409
            return "Invalid sample rate";
1✔
410
        }
411

412
        return undefined;
5✔
413
    }
414
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc