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

benrr101 / node-taglib-sharp / 51048452

24 Nov 2024 10:04PM UTC coverage: 92.545% (-0.02%) from 92.565%
51048452

Pull #113

appveyor

benrr101
Fixing a thing after merging develop
Pull Request #113: Fix MP3 VBR Headers

3250 of 4131 branches covered (78.67%)

Branch coverage included in aggregate %.

492 of 502 new or added lines in 11 files covered. (98.01%)

1 existing line in 1 file now uncovered.

26753 of 28289 relevant lines covered (94.57%)

471.58 hits per line

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

98.73
/src/mpeg/mpegAudioHeader.ts
1
import XingHeader from "./xingHeader";
1✔
2
import VbrHeader from "./vbrHeader";
3
import VbriHeader from "./vbriHeader";
1✔
4
import {ByteVector} from "../byteVector";
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
    private static readonly BITRATES: number[][][] = [
1✔
16
        [ // Version 1
17
            [0 /*free*/, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, /*reserved*/], // layer 1
18
            [0 /*free*/, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384, /*reserved*/], // layer 2
19
            [0 /*free*/, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, /*reserved*/]  // layer 3
20
        ],
21
        [ // Version 2 or 2.5
22
            [0 /*free*/, 32, 48, 56,  64,  80,  96, 112, 128, 144, 160, 176, 192, 224, 256, /*reserved*/], // layer 1
23
            [0 /*free*/,  8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160, /*reserved*/], // layer 2
24
            [0 /*free*/,  8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160, /*reserved*/]  // layer 3
25
        ]
26
    ];
27

28
    private static readonly BLOCK_SIZES: number[][] = [
1✔
29
        [0, 384, 1152, 1152], // Version 1
30
        [0, 384, 1152,  576], // Version 2
31
        [0, 384, 1152,  576]  // Version 2.5
32
    ];
33

34
    private static readonly SAMPLE_RATES: number[][] = [
1✔
35
        [44100, 48000, 32000, /*reserved*/], // Version 1
36
        [22050, 24000, 16000, /*reserved*/], // Version 2
37
        [11025, 12000,  8000, /*reserved*/]  // Version 2.5
38
    ];
39

40
    private readonly _bitrate: number;
41
    private readonly _channelMode: ChannelMode;
42
    private readonly _layer: number;
43
    private readonly _isCopyrighted: boolean;
44
    private readonly _isOriginal: boolean;
45
    private readonly _isProtected: boolean;
46
    private readonly _sampleRate: number;
47
    private readonly _samplesPerFrame: number;
48
    private readonly _streamLength: number;
49
    private readonly _version: MpegVersion;
50
    private readonly _versionString: string;
51

52
    private _vbrHeader: VbrHeader;
53

54
    // #region Constructors
55

56
    private constructor(flags: number, streamLength: number) {
57
        this._streamLength = streamLength;
172✔
58

59
        // TODO: These could be moved to C#-like Lazy instances
60
        // TODO: Introduce an MPEG flags enum
61

62
        // Bit 19: Version
63
        switch (NumberUtils.uintAnd(NumberUtils.uintRShift(flags, 19), 0x03)) {
172✔
64
            case 0:
172✔
65
                this._version = MpegVersion.Version25;
66✔
66
                this._versionString = "2.5";
66✔
67
                break;
66✔
68
            case 2:
69
                this._version = MpegVersion.Version2;
49✔
70
                this._versionString = "2";
49✔
71
                break;
49✔
72
            default:
73
                // 1: Protected against by IsValidHeader
74
                this._version = MpegVersion.Version1;
57✔
75
                this._versionString = "1";
57✔
76
                break;
57✔
77
        }
78

79
        // Bits 18-17: Audio layer
80
        switch (NumberUtils.uintAnd(NumberUtils.uintRShift(flags, 17), 0x03)) {
172✔
81
            case 1:
172✔
82
                this._layer = 3;
62✔
83
                break;
62✔
84
            case 2:
85
                this._layer = 2;
46✔
86
                break;
46✔
87
            default:
88
                // 3: Protected against by IsValidHeader
89
                this._layer = 1;
64✔
90
                break;
64✔
91
        }
92

93
        // Bit 16: Error protection
94
        this._isProtected = !NumberUtils.hasFlag(flags, 0x10000);
172✔
95

96
        // Bits 15-12: Bit rate (as per header)
97
        // NOTE: IsValidHeader protects against reserved bitrate index
98
        const bitrateIndex1 = this._version === MpegVersion.Version1 ? 0 : 1;
172✔
99
        const bitrateIndex2 = this._layer - 1;
172✔
100
        const bitrateIndex3 = NumberUtils.uintAnd(NumberUtils.uintRShift(flags, 12), 0x0F);
172✔
101
        this._bitrate = MpegAudioHeader.BITRATES[bitrateIndex1][bitrateIndex2][bitrateIndex3];
172✔
102

103
        // Bits 11-10: Sample rate
104
        // NOTE: IsValidHeader protects against reserved sample rate index
105
        const sampleRateIndex2 = NumberUtils.uintAnd(NumberUtils.uintRShift(flags, 10), 0x03);
172✔
106
        this._sampleRate = MpegAudioHeader.SAMPLE_RATES[this._version][sampleRateIndex2];
172✔
107

108
        // Bit 9: Frame padding - not needed today (maybe if we support accurate reading someday we will)
109

110
        // Bit 8: Private bit - unknown meaning, skipping
111

112
        // Bit 7-6: Channel mode
113
        this._channelMode = NumberUtils.uintAnd(NumberUtils.uintRShift(flags, 6), 0x03);
172✔
114

115
        // Bit 5-4: Join stereo mode extension - not useful here, skipping
116

117
        // Bit 3: Copyright
118
        this._isCopyrighted = NumberUtils.hasFlag(flags, 0x08);
172✔
119

120
        // Bit 2: Original
121
        this._isOriginal = NumberUtils.hasFlag(flags, 0x04);
172✔
122

123
        // Bit 1-0: Emphasis - not useful here, skipping
124

125
        // Lookup samples per frame
126
        this._samplesPerFrame = MpegAudioHeader.BLOCK_SIZES[this._version][this._layer];
172✔
127
    }
128

129
    /**
130
     * Constructs an MPEG audio header by searching the provided file for an MPEG sync signature
131
     * and reading the header that immediately follows.
132
     * @param file File from which to read the audio header
133
     * @param searchStart Offset into the file to begin searching
134
     * @param searchEnd Offset into the file to stop searching
135
     * @param streamBytes Total number of bytes in the audio stream. Used to calculate duration if
136
     *     a VBR header does not additionally specify it. If VBR header is not present and
137
     *     {@link streamBytes} is `undefined`, then duration will be 0.
138
     * @returns MpegAudioHeader Header as read from the file, `undefined` if not found.
139
     */
140
    public static fromFile(file: File, searchStart: number, searchEnd: number, streamBytes?: number): MpegAudioHeader {
141
        Guards.truthy(file, "file");
196✔
142
        Guards.safeUint(searchStart, "searchStart");
194✔
143
        Guards.safeUint(searchEnd, "searchEnd");
189✔
144
        Guards.greaterThanInclusive(searchEnd, searchStart, "searchEnd");
184✔
145
        if (streamBytes) {
184✔
146
            Guards.safeUint(streamBytes, "totalBytes");
7✔
147
        }
148

149
        // Scan the file to find the header
150
        let flags: number;
151
        let filePosition = searchStart;
181✔
152
        while (filePosition < searchEnd) {
181✔
153
            // Read a buffer worth of bytes, at least 4, from the file
154
            file.seek(filePosition);
186✔
155
            const bufferSize = Math.min(File.bufferSize, searchEnd - filePosition);
186✔
156
            const buffer = file.readBlock(bufferSize);
186✔
157
            if (buffer.length < 4) {
186✔
158
                break;
8✔
159
            }
160

161
            // Scan the buffer for the header signature
162
            // Note: We need at least 4 bytes to check for a header, so once we get less than 4
163
            //     bytes left in the buffer just skip it to avoid the subarray allocation and
164
            //     function call to check it.
165
            for (let i = 0; i <= buffer.length - 4; i++) {
177✔
166
                const headerBytes = buffer.subarray(i, 4);
179✔
167
                if (this.isHeaderValid(headerBytes)) {
179✔
168
                    flags = headerBytes.toUint();
172✔
169
                    filePosition += i;
172✔
170
                    break;
172✔
171
                }
172
            }
173

174
            if (flags) {
177✔
175
                break;
172✔
176
            }
177

178
            // Advance to the end of the buffer, minus the 3 bytes we didn't try to check
179
            filePosition += buffer.length - 3;
5✔
180
        }
181

182
        if (!flags) {
180✔
183
            return undefined;
8✔
184
        }
185

186
        // Create the header from the flags
187
        const header = new MpegAudioHeader(flags, streamBytes);
172✔
188

189
        header._vbrHeader =
172✔
190
            XingHeader.fromFile(
338✔
191
                file,
192
                filePosition,
193
                header._version,
194
                header._channelMode,
195
                header._samplesPerFrame,
196
                header._sampleRate,
197
                header._streamLength
198
            ) || VbriHeader.fromFile(
199
                file,
200
                filePosition,
201
                header._samplesPerFrame,
202
                header._sampleRate
203
            );
204

205
        return header;
172✔
206
    }
207

208
    // #endregion
209

210
    // #region Properties
211

212
    /** @inheritDoc */
213
    public get audioBitrate(): number { return this._vbrHeader?.bitrateKilobytes || this._bitrate; }
277✔
214

215
    /** @inheritDoc */
216
    public get audioChannels(): number { return this.channelMode === ChannelMode.SingleChannel ? 1 : 2; }
4✔
217

218
    /** @inheritDoc */
219
    public get audioSampleRate(): number { return this._sampleRate; }
9✔
220

221
    /**
222
     * Gets the MPEG audio channel mode of the audio represented by the current instance.
223
     */
224
    public get channelMode(): ChannelMode { return this._channelMode; }
8✔
225

226
    /** @inheritDoc */
227
    public get description(): string {
228
        let result = `MPEG Version ${this._versionString} Audio, Layer ${this._layer}`;
8✔
229
        if (this._vbrHeader?.bitrateKilobytes) {
8✔
230
            result += " VBR";
1✔
231
        }
232

233
        return result;
8✔
234
    }
235

236
    /** @inheritDoc */
237
    public get durationMilliseconds(): number {
238
        if (this._vbrHeader?.durationMilliseconds) {
5✔
239
            return this._vbrHeader.durationMilliseconds;
1✔
240
        }
241

242
        // @TODO: Consider returning undefined if duration cannot be determined
243
        if (this._streamLength && this.audioBitrate) {
4✔
244
            return (this._streamLength * 8) / this.audioBitrate
2✔
245
        }
246

247
        return 0;
2✔
248
    }
249

250
    /**
251
     * Whether the current audio is copyrighted.
252
     */
253
    public get isCopyrighted(): boolean { return this._isCopyrighted }
2✔
254

255
    /**
256
     * Whether the current audio is original.
257
     */
258
    public get isOriginal(): boolean { return this._isOriginal; }
2✔
259

260
    /**
261
     * Gets whether the audio represented by the current instance is protected by CRC.
262
     */
263
    public get isProtected(): boolean { return this._isProtected; }
2✔
264

265
    /**
266
     * Gets the MPEG audio layer used to encode the audio represented by the current instance.
267
     */
268
    public get layer(): number { return this._layer; }
138✔
269

270
    /** @inheritDoc */
271
    public get mediaTypes(): MediaTypes { return MediaTypes.Audio; }
1✔
272

273
    /**
274
     * Gets the variable bitrate header (VBR) if the MPEG audio frame contains one.
275
     */
276
    public get vbrHeader(): VbrHeader { return this._vbrHeader; }
2✔
277

278
    /**
279
     * Gets the MPEG version used to encode the audio represented by the current instance.
280
     */
281
    public get version(): MpegVersion { return this._version }
138✔
282

283
    // #endregion
284

285
    private static isHeaderValid(data: ByteVector): boolean {
286
        // We assume that data is at least 4 bytes long.
287
        if (data.get(0) != 0xFF) {
179✔
288
            // First byte must be FF
289
            return false;
3✔
290
        }
291

292
        // Check for the second byte for reserved values
293
        const byte1 = data.get(1);
176✔
294
        if (NumberUtils.uintAnd(byte1, 0xE0) != 0xE0) {
176✔
295
            // Highest 3 bits must be 0b111 for second byte of sync sequence
296
            return false;
1✔
297
        }
298
        if (NumberUtils.uintAnd(byte1, 0x18) === 0x08) {
175✔
299
            // MPEG version cannot be 0b01 (reserved)
300
            return false;
1✔
301
        }
302
        if (NumberUtils.uintAnd(byte1, 0x06) === 0x00) {
174✔
303
            // Layer cannot be 0b00 (reserved)
304
            return false;
1✔
305
        }
306

307
        // Check the third byte for reserved values
308
        const byte2 = data.get(2);
173✔
309
        if (NumberUtils.uintAnd(byte2, 0xF0) === 0xF0) {
173✔
310
            // Bitrate index cannot be 0b1111 (reserved)
311
            return false;
1✔
312
        }
313
        if (NumberUtils.uintAnd(byte2, 0x0C) === 0x0C) {
172!
314
            // Sample rate index cannot be 0b11 (reserved)
NEW
315
            return false;
×
316
        }
317

318
        return true;
172✔
319
    }
320
}
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

© 2026 Coveralls, Inc