diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index d9f43df..bce804e 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
- node-version: [16.x, 18.x, 20.x]
+ node-version: [16.x, 18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v2
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index bece10e..1142ee2 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -4,23 +4,23 @@ on:
release:
types: [published]
+permissions:
+ id-token: write
+ contents: read
+
jobs:
- build:
+ publish:
runs-on: ubuntu-latest
- permissions:
- contents: read
- id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: '20.x'
+ node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
+ - run: npm install -g npm@latest
- run: npm ci
- run: npm run build
env:
NODE_ENV: production
- - run: npm publish --provenance --access public
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - run: npm publish --access public
diff --git a/README.md b/README.md
index 5a9d6a6..c9eb069 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ The website is also open-sourced and can be viewed at the
* Read and write ID3v2 tags in MP4/M4A/M4V/MOV containers (via ID32 box)
* Read and write ID3v2 tags in AIFF/AIFC files (via ID3 chunk)
* Read and write ID3v2 tags in AAC/ADTS streams (prepended ID3v2)
+* Partial reading from Blob/File without loading entire file into memory
* Supports unsynchronisation
* Standards compliant. See ~~[id3.org](http://id3.org)~~
[mutagen-specs.readthedocs.io](https://mutagen-specs.readthedocs.io/en/latest/id3/index.html)
@@ -130,6 +131,50 @@ async function main () {
main()
```
+### Partial reading with `readBlob`
+
+For read-only scenarios, you can read the metadata without loading the entire
+file into memory. `MP3Tag.readBlob()` accepts a
+[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) (or `File`) and
+uses `blob.slice()` to read only the bytes it needs.
+
+In the browser:
+```html
+
+
+```
+
+In Node.js (requires Node 19.8+):
+```javascript
+import { openAsBlob } from 'node:fs'
+import MP3Tag from 'mp3tag.js'
+
+const blob = await openAsBlob('/path/to/large-file.mp3')
+const tags = await MP3Tag.readBlob(blob)
+console.log(tags.title, tags.artist)
+```
+
+`readBlob` accepts the same options as `read`:
+```javascript
+const tags = await MP3Tag.readBlob(blob, {
+ id3v1: true,
+ id3v2: true,
+ mp4: true,
+ aiff: true,
+ aac: true,
+ unsupported: false
+})
+```
+
### MP4/M4A Support
mp3tag.js supports reading and writing ID3v2 tags embedded in MP4/M4A containers
diff --git a/package-lock.json b/package-lock.json
index 22d7cb3..1e15c62 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "mp3tag.js",
- "version": "3.14.1",
+ "version": "3.16.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mp3tag.js",
- "version": "3.14.1",
+ "version": "3.16.0",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.12.3",
@@ -2505,9 +2505,9 @@
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/minimatch": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
- "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -5407,9 +5407,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -5527,11 +5527,10 @@
}
},
"node_modules/mocha/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
- "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -6356,9 +6355,9 @@
}
},
"node_modules/rollup": {
- "version": "2.79.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
- "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
+ "version": "2.80.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
+ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -9125,9 +9124,9 @@
}
},
"minimatch": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
- "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
@@ -11242,9 +11241,9 @@
"dev": true
},
"minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
@@ -11330,9 +11329,9 @@
"dev": true
},
"minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
@@ -11946,9 +11945,9 @@
}
},
"rollup": {
- "version": "2.79.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
- "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
+ "version": "2.80.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
+ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
diff --git a/package.json b/package.json
index e7fad86..8c66702 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mp3tag.js",
- "version": "3.14.1",
+ "version": "3.16.0",
"description": "MP3 tagging library written in pure JavaScript",
"keywords": [
"node",
diff --git a/src/id3v2/parse.mjs b/src/id3v2/parse.mjs
index d90f1a6..fb5763e 100644
--- a/src/id3v2/parse.mjs
+++ b/src/id3v2/parse.mjs
@@ -325,12 +325,28 @@ export function etcoFrame (buffer, version) {
const view = new BufferView(buffer)
const format = view.getUint8(0)
const raw = view.getUint8(1, view.byteLength - 1)
- const rview = new BufferView(Array.isArray(raw) ? raw : [raw])
+ const bytes = Array.isArray(raw) ? raw : [raw]
+ const rview = new BufferView(bytes)
const data = []
- for (let i = 0; i < raw.length; i += 5) {
- const event = rview.getUint8(i)
- const time = rview.getUint32(i + 1)
+ let i = 0
+ while (i + 5 <= bytes.length) {
+ // Per ID3v2.4 §4.5: $FF means "one more byte of events follows"
+ // and "all the following bytes with the value $FF have the same
+ // function". So an event type may span multiple bytes: any number
+ // of leading $FF bytes followed by a single terminating byte
+ // (0x00–0xFE), then a 4-byte timestamp.
+ let typeEnd = i
+ while (typeEnd < bytes.length && bytes[typeEnd] === 0xff) typeEnd++
+ if (typeEnd >= bytes.length || typeEnd + 4 >= bytes.length) break
+ let event
+ if (typeEnd === i) {
+ event = rview.getUint8(i)
+ } else {
+ event = bytes.slice(i, typeEnd + 1)
+ }
+ const time = rview.getUint32(typeEnd + 1)
data.push({ event, time })
+ i = typeEnd + 5
}
return {
format,
diff --git a/src/id3v2/validate.mjs b/src/id3v2/validate.mjs
index ba236ea..1dcb6e5 100644
--- a/src/id3v2/validate.mjs
+++ b/src/id3v2/validate.mjs
@@ -542,10 +542,41 @@ export function etcoFrame (value, version, strict) {
throw new Error('Invalid timestamp format (should be 1 or 2)')
}
for (const { event, time } of value.data) {
- if (typeof event !== 'number') {
- throw new Error('Event is not a number')
- } else if (event > 255 || event < 0) {
- throw new Error('Event should be in range of 0 - 255')
+ // Per ID3v2.4 §4.5: events are either a single byte (0x00–0xFE)
+ // or, when 0xFF is used as escape prefix, a sequence of one or
+ // more 0xFF bytes followed by a single terminating byte. Accept
+ // either a number or an Array of bytes.
+ // Structural checks (always on, including non-strict mode) — these
+ // rules are what make the byte stream parseable per §4.5. Emitting a
+ // bare 0xFF as a single-byte event, or an extended-event array whose
+ // prefix bytes aren't 0xFF, produces a malformed frame that no
+ // compliant reader can decode.
+ if (typeof event === 'number') {
+ if (event > 255 || event < 0) {
+ throw new Error('Event should be in range of 0 - 255')
+ }
+ if (event === 0xff) {
+ throw new Error('Event 0xFF is the escape byte (§4.5); use an array of bytes for extended events')
+ }
+ } else if (Array.isArray(event)) {
+ if (event.length === 0) {
+ throw new Error('Event array must not be empty')
+ }
+ for (const byte of event) {
+ if (typeof byte !== 'number' || byte > 255 || byte < 0) {
+ throw new Error('Event byte should be a number in range of 0 - 255')
+ }
+ }
+ for (let i = 0; i < event.length - 1; i++) {
+ if (event[i] !== 0xff) {
+ throw new Error('Extended event prefix bytes must all be 0xFF (§4.5)')
+ }
+ }
+ if (event[event.length - 1] === 0xff) {
+ throw new Error('Extended event terminator byte must not be 0xFF (§4.5)')
+ }
+ } else {
+ throw new Error('Event is not a number or an array of bytes')
}
if (typeof time !== 'number') {
diff --git a/src/id3v2/write.mjs b/src/id3v2/write.mjs
index 578cdb7..7f410dc 100644
--- a/src/id3v2/write.mjs
+++ b/src/id3v2/write.mjs
@@ -548,7 +548,13 @@ export function sytcFrame (value, options) {
export function etcoFrame (value, options) {
const { id, version, unsynch } = options
- const array = value.data.flatMap(({ event, time }) => [event, ...timeBytes(time)])
+ // Per ID3v2.4 §4.5, event types may be multi-byte: any number of
+ // leading $FF bytes followed by a single terminating byte. Accept
+ // either a number (single byte) or an array of bytes.
+ const array = value.data.flatMap(({ event, time }) => {
+ const bytes = Array.isArray(event) ? event : [event]
+ return [...bytes, ...timeBytes(time)]
+ })
let data = mergeBytes(value.format, array)
if (unsynch) data = unsynchData(data, version)
diff --git a/src/mp3tag.mjs b/src/mp3tag.mjs
index 424e7de..1e5de5b 100644
--- a/src/mp3tag.mjs
+++ b/src/mp3tag.mjs
@@ -7,16 +7,17 @@ import * as MP4 from './mp4/index.mjs'
import * as AIFF from './aiff/index.mjs'
import * as AAC from './aac/index.mjs'
-import { mergeBytes } from './utils/bytes.mjs'
+import { mergeBytes, decodeSynch } from './utils/bytes.mjs'
import { overwriteDefault } from './utils/objects.mjs'
import { isBuffer } from './utils/types.mjs'
+import { findBox, decodeLanguage } from './mp4/boxes.mjs'
import { encoding2Index } from './utils/strings.mjs'
export default class MP3Tag {
get name () { return 'MP3Tag' }
set name (value) { throw new Error('Unable to set this property') }
- get version () { return '3.14.1' }
+ get version () { return '3.16.0' }
set version (value) { throw new Error('Unable to set this property') }
constructor (buffer, verbose = false) {
@@ -505,6 +506,391 @@ export default class MP3Tag {
return tags
}
+ static async readBlob (blob, options = {}) {
+ options = overwriteDefault(options, {
+ id3v1: true,
+ id3v2: true,
+ mp4: true,
+ aiff: true,
+ aac: true,
+ unsupported: false,
+ encoding: 'utf-8'
+ })
+
+ const size = blob.size
+ const tags = {}
+ async function read (offset, length) {
+ return blob.slice(offset, offset + length).arrayBuffer()
+ }
+
+ const containerProps = {
+ title: {
+ get: function () {
+ return (this.v2 && (this.v2.TIT2 || this.v2.TT2)) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TT2' : 'TIT2'] = value
+ }
+ }
+ },
+ artist: {
+ get: function () {
+ return (this.v2 && (this.v2.TPE1 || this.v2.TP1)) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TP1' : 'TPE1'] = value
+ }
+ }
+ },
+ album: {
+ get: function () {
+ return (this.v2 && (this.v2.TALB || this.v2.TAL)) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TAL' : 'TALB'] = value
+ }
+ }
+ },
+ year: {
+ get: function () {
+ return (this.v2 && (this.v2.TYER || this.v2.TDRC || this.v2.TYE)) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ if (version === 2) this.v2.TYE = value
+ else if (version === 3) this.v2.TYER = value
+ else if (version === 4) this.v2.TDRC = value
+ }
+ }
+ },
+ comment: {
+ get: function () {
+ let text = ''
+ if (this.v2 && (this.v2.COMM || this.v2.COM)) {
+ const comm = this.v2.COMM || this.v2.COM
+ if (Array.isArray(comm) && comm.length > 0) text = comm[0].text
+ }
+ return text
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'COM' : 'COMM'] = [{
+ language: 'eng',
+ descriptor: '',
+ text: value
+ }]
+ }
+ }
+ },
+ track: {
+ get: function () {
+ return (this.v2 && (
+ (this.v2.TRCK && this.v2.TRCK.split('/')[0]) ||
+ (this.v2.TRK && this.v2.TRK.split('/')[0])
+ )) || ''
+ },
+ set: function (value) {
+ if (this.v2 && value !== '') {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TRK' : 'TRCK'] = value
+ }
+ }
+ },
+ genre: {
+ get: function () {
+ return (this.v2 && (this.v2.TCON || this.v2.TCO)) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TCO' : 'TCON'] = value
+ }
+ }
+ }
+ }
+
+ const mp3Props = {
+ title: {
+ get: function () {
+ return (this.v2 && (this.v2.TIT2 || this.v2.TT2)) ||
+ (this.v1 && this.v1.title) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TT2' : 'TIT2'] = value
+ }
+ if (this.v1) this.v1.title = value
+ }
+ },
+ artist: {
+ get: function () {
+ return (this.v2 && (this.v2.TPE1 || this.v2.TP1)) ||
+ (this.v1 && this.v1.artist) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TP1' : 'TPE1'] = value
+ }
+ if (this.v1) this.v1.artist = value
+ }
+ },
+ album: {
+ get: function () {
+ return (this.v2 && (this.v2.TALB || this.v2.TAL)) ||
+ (this.v1 && this.v1.album) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TAL' : 'TALB'] = value
+ }
+ if (this.v1) this.v1.album = value
+ }
+ },
+ year: {
+ get: function () {
+ return (this.v2 && (this.v2.TYER || this.v2.TDRC || this.v2.TYE)) ||
+ (this.v1 && this.v1.year) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ if (version === 2) this.v2.TYE = value
+ else if (version === 3) this.v2.TYER = value
+ else if (version === 4) this.v2.TDRC = value
+ }
+ if (this.v1) this.v1.year = value
+ }
+ },
+ comment: {
+ get: function () {
+ let text = ''
+ if (this.v2 && (this.v2.COMM || this.v2.COM)) {
+ const comm = this.v2.COMM || this.v2.COM
+ if (Array.isArray(comm) && comm.length > 0) text = comm[0].text
+ } else if (this.v1 && this.v1.comment) text = this.v1.comment
+ return text
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'COM' : 'COMM'] = [{
+ language: 'eng',
+ descriptor: '',
+ text: value
+ }]
+ }
+ if (this.v1) this.v1.comment = value
+ }
+ },
+ track: {
+ get: function () {
+ return (this.v2 && (
+ (this.v2.TRCK && this.v2.TRCK.split('/')[0]) ||
+ (this.v2.TRK && this.v2.TRK.split('/')[0])
+ )) || (this.v1 && this.v1.track) || ''
+ },
+ set: function (value) {
+ if (this.v2 && value !== '') {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TRK' : 'TRCK'] = value
+ }
+ if (this.v1) this.v1.track = value
+ }
+ },
+ genre: {
+ get: function () {
+ return (this.v2 && (this.v2.TCON || this.v2.TCO)) ||
+ (this.v1 && this.v1.genre) || ''
+ },
+ set: function (value) {
+ if (this.v2) {
+ const version = this.v2Details.version[0]
+ this.v2[version === 2 ? 'TCO' : 'TCON'] = value
+ }
+ if (this.v1) this.v1.genre = value
+ }
+ }
+ }
+
+ const headerLen = Math.min(12, size)
+ if (headerLen < 4) {
+ Object.defineProperties(tags, mp3Props)
+ return tags
+ }
+
+ const header = await read(0, headerLen)
+ const hv = new BufferView(header)
+
+ // AIFF
+ if (options.aiff && headerLen >= 12 && hv.getUint8String(0, 4) === 'FORM') {
+ const formType = hv.getUint8String(8, 4)
+
+ if (formType === 'AIFF' || formType === 'AIFC') {
+ // Walk AIFF chunks to find ID3
+ let chunkOffset = 12
+ let id3ChunkOffset = -1
+ let id3ChunkSize = 0
+
+ while (chunkOffset + 8 <= size) {
+ const chunkHeader = await read(chunkOffset, 8)
+ const cv = new BufferView(chunkHeader)
+ const chunkId = cv.getUint8String(0, 4)
+ const chunkSize = cv.getUint32(4)
+
+ if (chunkId === 'ID3 ') {
+ id3ChunkOffset = chunkOffset
+ id3ChunkSize = chunkSize
+ break
+ }
+
+ chunkOffset += 8 + chunkSize
+ if (chunkSize % 2 !== 0) chunkOffset += 1
+ }
+
+ if (id3ChunkOffset >= 0) {
+ const id3Buffer = await read(id3ChunkOffset + 8, id3ChunkSize)
+
+ if (ID3v2.hasID3v2(id3Buffer)) {
+ const { tags: v2Tags, details } = ID3v2.decode(id3Buffer, 0, options.unsupported)
+ details.aiff = {
+ chunkOffset: id3ChunkOffset,
+ chunkSize: id3ChunkSize
+ }
+ tags.v2 = { ...v2Tags }
+ tags.v2Details = details
+ }
+ }
+
+ Object.defineProperties(tags, containerProps)
+ return tags
+ }
+ }
+
+ // MP4
+ if (options.mp4 && headerLen >= 8 && hv.getUint8String(4, 4) === 'ftyp') {
+ // Walk top-level boxes to find moov
+ let boxOffset = 0
+ let moovOffset = -1
+ let moovSize = 0
+
+ while (boxOffset + 8 <= size) {
+ const boxHeader = await read(boxOffset, 8)
+ const bv = new BufferView(boxHeader)
+ let bSize = bv.getUint32(0)
+ const bType = bv.getUint8String(4, 4)
+
+ if (bSize === 0) bSize = size - boxOffset
+ if (bSize < 8) break
+
+ if (bType === 'moov') {
+ moovOffset = boxOffset
+ moovSize = bSize
+ break
+ }
+
+ boxOffset += bSize
+ }
+
+ if (moovOffset >= 0) {
+ const moovBuffer = await read(moovOffset, moovSize)
+ const mv = new BufferView(moovBuffer)
+
+ // Navigate moov > udta > meta > ID32
+ const udta = findBox(mv, 8, moovSize, 'udta')
+ const meta = udta ? findBox(mv, udta.dataStart, udta.end, 'meta') : null
+ const id32 = meta ? findBox(mv, meta.dataStart + 4, meta.end, 'ID32') : null
+
+ if (id32) {
+ const languageBytes = mv.getUint8(id32.dataStart + 4, 2)
+ const langValue = (languageBytes[0] << 8) | languageBytes[1]
+ const language = decodeLanguage(langValue)
+
+ const id3Start = id32.dataStart + 4 + 2
+ const id3Size = id32.end - id3Start
+ const id3Buffer = moovBuffer.slice(id3Start, id3Start + id3Size)
+
+ if (ID3v2.hasID3v2(id3Buffer)) {
+ const { tags: v2Tags, details } = ID3v2.decode(id3Buffer, 0, options.unsupported)
+ details.mp4 = {
+ language,
+ id32Offset: moovOffset + id32.offset,
+ id32Size: id32.size
+ }
+ tags.v2 = { ...v2Tags }
+ tags.v2Details = details
+ }
+ }
+ }
+
+ Object.defineProperties(tags, containerProps)
+ return tags
+ }
+
+ // ID3v2 at byte 0 (MP3 or AAC)
+ if (options.id3v2 && headerLen >= 10 && hv.getUint8String(0, 3) === 'ID3') {
+ const tagSize = decodeSynch(hv.getUint32(6))
+ const fullSize = 10 + tagSize
+
+ const readLen = Math.min(fullSize + 4, size)
+ const tagBuffer = await read(0, readLen)
+
+ const { tags: v2Tags, details } = ID3v2.decode(tagBuffer, 0, options.unsupported)
+ tags.v2 = { ...v2Tags }
+ tags.v2Details = details
+
+ let isAAC = false
+ if (options.aac && readLen >= fullSize + 2) {
+ const tv = new BufferView(tagBuffer)
+ const b0 = tv.getUint8(fullSize)
+ const b1 = tv.getUint8(fullSize + 1)
+ const syncWord = (b0 << 4) | (b1 >> 4)
+ if (syncWord === 0xFFF) {
+ const layer = (b1 >> 1) & 0x03
+ if (layer === 0) {
+ isAAC = true
+ details.aac = { format: 'ADTS' }
+ }
+ }
+ }
+
+ if (!isAAC && options.id3v1 && size >= 128) {
+ const tail = await read(size - 128, 128)
+ if (ID3v1.hasID3v1(tail)) {
+ const { tags: v1Tags, details: v1Details } = ID3v1.decode(tail, options.encoding)
+ tags.v1 = { ...v1Tags }
+ tags.v1Details = v1Details
+ }
+ }
+
+ Object.defineProperties(tags, isAAC ? containerProps : mp3Props)
+ return tags
+ }
+
+ // No ID3v2: check for ID3v1
+ if (options.id3v1 && size >= 128) {
+ const tail = await read(size - 128, 128)
+ if (ID3v1.hasID3v1(tail)) {
+ const { tags: v1Tags, details } = ID3v1.decode(tail, options.encoding)
+ tags.v1 = { ...v1Tags }
+ tags.v1Details = details
+ }
+ }
+
+ Object.defineProperties(tags, mp3Props)
+ return tags
+ }
+
read (options = {}) {
this.tags = {}
this.error = ''
diff --git a/test/id3v2/index.cjs b/test/id3v2/index.cjs
index d518379..6e13c13 100644
--- a/test/id3v2/index.cjs
+++ b/test/id3v2/index.cjs
@@ -178,6 +178,58 @@ describe('ID3v2', function () {
})
})
+ it('Write ETCO with extended events (0xFF prefix)', function () {
+ // Per ID3v2.4 §4.5, an event type may be a multi-byte sequence:
+ // one or more leading 0xFF escape bytes followed by a single
+ // terminating byte (0x00–0xFE). Verify round-trip for mixed
+ // single-byte and extended events.
+ this.mp3tag.tags.v2.ETCO = {
+ format: 2,
+ data: [
+ { event: 0x02, time: 100 },
+ { event: [0xff, 0x12], time: 5000 },
+ { event: [0xff, 0x20], time: 10000 },
+ { event: 0x10, time: 12000 },
+ { event: [0xff, 0xff, 0x07], time: 20000 }
+ ]
+ }
+ this.mp3tag.save({ strict: true })
+ if (this.mp3tag.error !== '') throw new Error(this.mp3tag.error)
+
+ this.mp3tag.read()
+ if (this.mp3tag.error !== '') throw new Error(this.mp3tag.error)
+
+ assert.deepStrictEqual(this.mp3tag.tags.v2.ETCO, {
+ format: 2,
+ data: [
+ { event: 0x02, time: 100 },
+ { event: [0xff, 0x12], time: 5000 },
+ { event: [0xff, 0x20], time: 10000 },
+ { event: 0x10, time: 12000 },
+ { event: [0xff, 0xff, 0x07], time: 20000 }
+ ]
+ })
+ })
+
+ it('Rejects malformed extended ETCO events (structural, non-strict too)', function () {
+ // Per ID3v2.4 §4.5 these byte layouts produce unparseable frames,
+ // so they are rejected unconditionally — not only under strict
+ // mode. A silent acceptance here would write a tag that no
+ // compliant reader (including this library's own parser) can
+ // decode correctly.
+ const cases = [
+ { event: 0xff, time: 100 }, // bare escape byte
+ { event: [0xff, 0xff], time: 100 }, // terminator is 0xFF
+ { event: [0x12, 0x34], time: 100 } // non-0xFF prefix
+ ]
+ for (const bad of cases) {
+ this.mp3tag.tags.v2.ETCO = { format: 2, data: [bad] }
+ this.mp3tag.save() // NOT strict
+ assert.notStrictEqual(this.mp3tag.error, '',
+ `expected error for event=${JSON.stringify(bad.event)}`)
+ }
+ })
+
it('Write complex multi tag', function () {
this.mp3tag.tags.v2.SYLT = [
{
diff --git a/test/index.cjs b/test/index.cjs
index e5655f2..b73f503 100644
--- a/test/index.cjs
+++ b/test/index.cjs
@@ -34,7 +34,7 @@ describe('MP3Tag', function () {
})
after(function () {
- const extendTests = ['id3v1/index.cjs', 'id3v2/index.cjs', 'id3/index.cjs', 'mp4/index.cjs', 'aiff/index.cjs', 'aac/index.cjs']
+ const extendTests = ['id3v1/index.cjs', 'id3v2/index.cjs', 'id3/index.cjs', 'mp4/index.cjs', 'aiff/index.cjs', 'aac/index.cjs', 'readBlob/index.cjs']
const tests = this.test.parent.tests
let failed = false
diff --git a/test/readBlob/index.cjs b/test/readBlob/index.cjs
new file mode 100644
index 0000000..1858cb8
--- /dev/null
+++ b/test/readBlob/index.cjs
@@ -0,0 +1,347 @@
+/* eslint-env mocha */
+/* global Blob */
+
+const assert = require('assert')
+const MP3Tag = require('../../dist/mp3tag.js')
+const { bytes } = require('../globals.cjs')
+
+/**
+ * Create a Blob from a Uint8Array fixture
+ */
+function createBlob (uint8) {
+ return new Blob([uint8])
+}
+
+/**
+ * Create a minimal ID3v2.3 tag with a TIT2 frame
+ */
+function createID3v2Tag (title) {
+ const titleBytes = Buffer.from(title, 'utf8')
+ const frameSize = 1 + titleBytes.length
+
+ const header = new Uint8Array([
+ 0x49, 0x44, 0x33, 0x03, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ ])
+
+ const frame = new Uint8Array(10 + frameSize)
+ frame[0] = 0x54; frame[1] = 0x49; frame[2] = 0x54; frame[3] = 0x32
+ frame[4] = (frameSize >> 24) & 0xFF
+ frame[5] = (frameSize >> 16) & 0xFF
+ frame[6] = (frameSize >> 8) & 0xFF
+ frame[7] = frameSize & 0xFF
+ frame[10] = 0x00
+ frame.set(titleBytes, 11)
+
+ const tagSize = frame.length
+ header[6] = (tagSize >> 21) & 0x7F
+ header[7] = (tagSize >> 14) & 0x7F
+ header[8] = (tagSize >> 7) & 0x7F
+ header[9] = tagSize & 0x7F
+
+ const result = new Uint8Array(header.length + frame.length)
+ result.set(header, 0)
+ result.set(frame, header.length)
+ return result
+}
+
+/**
+ * Create a minimal AIFF file with optional ID3 chunk
+ */
+function createAIFF (id3Data) {
+ const commChunk = new Uint8Array([
+ 0x43, 0x4F, 0x4D, 0x4D,
+ 0x00, 0x00, 0x00, 0x12,
+ 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x10,
+ 0x00, 0x10,
+ 0x40, 0x0E, 0xAC, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ ])
+
+ const ssndChunk = new Uint8Array([
+ 0x53, 0x53, 0x4E, 0x44,
+ 0x00, 0x00, 0x00, 0x18,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ ])
+
+ let totalSize = 4 + commChunk.length + ssndChunk.length
+ let id3Chunk = null
+
+ if (id3Data) {
+ const id3Size = id3Data.length
+ const needsPadding = id3Size % 2 !== 0
+ id3Chunk = new Uint8Array(8 + id3Size + (needsPadding ? 1 : 0))
+ id3Chunk[0] = 0x49; id3Chunk[1] = 0x44; id3Chunk[2] = 0x33; id3Chunk[3] = 0x20
+ id3Chunk[4] = (id3Size >> 24) & 0xFF
+ id3Chunk[5] = (id3Size >> 16) & 0xFF
+ id3Chunk[6] = (id3Size >> 8) & 0xFF
+ id3Chunk[7] = id3Size & 0xFF
+ id3Chunk.set(id3Data, 8)
+ totalSize += id3Chunk.length
+ }
+
+ const formHeader = new Uint8Array(12)
+ formHeader[0] = 0x46; formHeader[1] = 0x4F; formHeader[2] = 0x52; formHeader[3] = 0x4D
+ formHeader[4] = (totalSize >> 24) & 0xFF
+ formHeader[5] = (totalSize >> 16) & 0xFF
+ formHeader[6] = (totalSize >> 8) & 0xFF
+ formHeader[7] = totalSize & 0xFF
+ formHeader[8] = 0x41; formHeader[9] = 0x49; formHeader[10] = 0x46; formHeader[11] = 0x46
+
+ const result = new Uint8Array(12 + totalSize - 4)
+ let offset = 0
+ result.set(formHeader, offset); offset += formHeader.length
+ result.set(commChunk, offset); offset += commChunk.length
+ result.set(ssndChunk, offset); offset += ssndChunk.length
+ if (id3Chunk) result.set(id3Chunk, offset)
+
+ return result
+}
+
+/**
+ * Create a minimal MP4 file with ID32 box
+ */
+function createMP4WithID32 () {
+ const id3Header = [
+ 0x49, 0x44, 0x33, 0x03, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x15
+ ]
+ const tit2Frame = [
+ 0x54, 0x49, 0x54, 0x32,
+ 0x00, 0x00, 0x00, 0x0B,
+ 0x00, 0x00,
+ 0x00,
+ 0x54, 0x65, 0x73, 0x74, 0x20,
+ 0x54, 0x69, 0x74, 0x6C, 0x65
+ ]
+ const id3Data = [...id3Header, ...tit2Frame]
+
+ const id32Size = 8 + 4 + 2 + id3Data.length
+ const id32Box = [
+ (id32Size >> 24) & 0xFF, (id32Size >> 16) & 0xFF,
+ (id32Size >> 8) & 0xFF, id32Size & 0xFF,
+ 0x49, 0x44, 0x33, 0x32,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x55, 0xC4,
+ ...id3Data
+ ]
+
+ const hdlrBox = [
+ 0x00, 0x00, 0x00, 0x21,
+ 0x68, 0x64, 0x6C, 0x72,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x6D, 0x64, 0x69, 0x72,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00
+ ]
+
+ const metaSize = 8 + 4 + hdlrBox.length + id32Box.length
+ const metaBox = [
+ (metaSize >> 24) & 0xFF, (metaSize >> 16) & 0xFF,
+ (metaSize >> 8) & 0xFF, metaSize & 0xFF,
+ 0x6D, 0x65, 0x74, 0x61,
+ 0x00, 0x00, 0x00, 0x00,
+ ...hdlrBox, ...id32Box
+ ]
+
+ const udtaSize = 8 + metaBox.length
+ const udtaBox = [
+ (udtaSize >> 24) & 0xFF, (udtaSize >> 16) & 0xFF,
+ (udtaSize >> 8) & 0xFF, udtaSize & 0xFF,
+ 0x75, 0x64, 0x74, 0x61,
+ ...metaBox
+ ]
+
+ const mvhdBox = [
+ 0x00, 0x00, 0x00, 0x70,
+ 0x6D, 0x76, 0x68, 0x64,
+ 0x00, 0x00, 0x00, 0x00,
+ ...new Array(100).fill(0)
+ ]
+
+ const moovSize = 8 + mvhdBox.length + udtaBox.length
+ const moovBox = [
+ (moovSize >> 24) & 0xFF, (moovSize >> 16) & 0xFF,
+ (moovSize >> 8) & 0xFF, moovSize & 0xFF,
+ 0x6D, 0x6F, 0x6F, 0x76,
+ ...mvhdBox, ...udtaBox
+ ]
+
+ const ftypBox = [
+ 0x00, 0x00, 0x00, 0x14,
+ 0x66, 0x74, 0x79, 0x70,
+ 0x4D, 0x34, 0x41, 0x20,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x4D, 0x34, 0x41, 0x20
+ ]
+
+ const mdatBox = [
+ 0x00, 0x00, 0x00, 0x6C,
+ 0x6D, 0x64, 0x61, 0x74,
+ ...new Array(100).fill(0xAA)
+ ]
+
+ return new Uint8Array([...ftypBox, ...moovBox, ...mdatBox])
+}
+
+/**
+ * Create a minimal AAC (ADTS) file with ID3v2 prepended
+ */
+function createAACFixture (title) {
+ const id3Tag = createID3v2Tag(title)
+
+ // ADTS frame header: 0xFF 0xF1 = sync(0xFFF) + MPEG-4 + layer=0 + no CRC
+ const adtsFrame = new Uint8Array([
+ 0xFF, 0xF1, 0x50, 0x80, 0x02, 0x00, 0x1C
+ ])
+
+ const result = new Uint8Array(id3Tag.length + adtsFrame.length)
+ result.set(id3Tag, 0)
+ result.set(adtsFrame, id3Tag.length)
+ return result
+}
+
+describe('readBlob', function () {
+ before(function () {
+ if (typeof Blob === 'undefined') this.skip()
+ })
+
+ describe('MP3', function () {
+ it('Reads ID3v2 and ID3v1 tags', async function () {
+ const blob = createBlob(bytes)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.ok(tags.v2)
+ assert.ok(tags.v1)
+ assert.ok(tags.v2.TIT2)
+ assert.strictEqual(tags.v1.title, 'TITLE')
+ })
+
+ it('Matches readBuffer output', async function () {
+ const blob = createBlob(bytes)
+ const tags = await MP3Tag.readBlob(blob)
+ const bufferTags = MP3Tag.readBuffer(bytes.buffer)
+
+ assert.strictEqual(tags.title, bufferTags.title)
+ assert.strictEqual(tags.artist, bufferTags.artist)
+ assert.strictEqual(tags.album, bufferTags.album)
+ assert.strictEqual(tags.year, bufferTags.year)
+ assert.strictEqual(tags.track, bufferTags.track)
+ assert.strictEqual(tags.genre, bufferTags.genre)
+ })
+
+ it('Reads only ID3v1 when no ID3v2 header', async function () {
+ // Build a buffer: MP3 audio + ID3v1 at the end (no ID3v2 prepended)
+ const audio = new Uint8Array([
+ 0xFF, 0xFB, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00
+ ])
+ const id3v1 = new Uint8Array(128)
+ id3v1[0] = 0x54; id3v1[1] = 0x41; id3v1[2] = 0x47 // "TAG"
+ id3v1[3] = 0x48; id3v1[4] = 0x65; id3v1[5] = 0x6C
+ id3v1[6] = 0x6C; id3v1[7] = 0x6F // "Hello" as title
+
+ const combined = new Uint8Array(audio.length + id3v1.length)
+ combined.set(audio, 0)
+ combined.set(id3v1, audio.length)
+
+ const blob = createBlob(combined)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.strictEqual(typeof tags.v2, 'undefined')
+ assert.ok(tags.v1)
+ assert.strictEqual(tags.v1.title, 'Hello')
+ assert.strictEqual(tags.title, 'Hello')
+ })
+ })
+
+ describe('AIFF', function () {
+ it('Reads ID3 chunk from AIFF', async function () {
+ const id3Data = createID3v2Tag('AIFF Song')
+ const aiff = createAIFF(id3Data)
+
+ const blob = createBlob(aiff)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.ok(tags.v2)
+ assert.strictEqual(tags.v2.TIT2, 'AIFF Song')
+ assert.strictEqual(tags.title, 'AIFF Song')
+ })
+
+ it('Contains AIFF-specific details', async function () {
+ const id3Data = createID3v2Tag('Test')
+ const aiff = createAIFF(id3Data)
+
+ const blob = createBlob(aiff)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.ok(tags.v2Details)
+ assert.ok(tags.v2Details.aiff)
+ assert.strictEqual(typeof tags.v2Details.aiff.chunkOffset, 'number')
+ assert.strictEqual(typeof tags.v2Details.aiff.chunkSize, 'number')
+ })
+
+ it('Returns empty tags for AIFF without ID3 chunk', async function () {
+ const aiff = createAIFF(null)
+
+ const blob = createBlob(aiff)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.strictEqual(typeof tags.v2, 'undefined')
+ assert.strictEqual(tags.title, '')
+ })
+ })
+
+ describe('MP4', function () {
+ it('Reads ID32 box from MP4', async function () {
+ const mp4 = createMP4WithID32()
+
+ const blob = createBlob(mp4)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.ok(tags.v2)
+ assert.strictEqual(tags.v2.TIT2, 'Test Title')
+ assert.strictEqual(tags.title, 'Test Title')
+ })
+
+ it('Contains MP4-specific details', async function () {
+ const mp4 = createMP4WithID32()
+
+ const blob = createBlob(mp4)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.ok(tags.v2Details)
+ assert.ok(tags.v2Details.mp4)
+ assert.strictEqual(tags.v2Details.mp4.language, 'und')
+ })
+ })
+
+ describe('AAC', function () {
+ it('Reads ID3v2 from AAC and detects ADTS', async function () {
+ const aac = createAACFixture('AAC Song')
+
+ const blob = createBlob(aac)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.ok(tags.v2)
+ assert.strictEqual(tags.v2.TIT2, 'AAC Song')
+ assert.strictEqual(tags.title, 'AAC Song')
+ assert.ok(tags.v2Details.aac)
+ assert.strictEqual(tags.v2Details.aac.format, 'ADTS')
+ })
+
+ it('Does not read ID3v1 for AAC', async function () {
+ const aac = createAACFixture('AAC Only')
+
+ const blob = createBlob(aac)
+ const tags = await MP3Tag.readBlob(blob)
+
+ assert.strictEqual(typeof tags.v1, 'undefined')
+ })
+ })
+})
diff --git a/types/id3v2/frames.d.ts b/types/id3v2/frames.d.ts
index 69c7b47..7d9d979 100644
--- a/types/id3v2/frames.d.ts
+++ b/types/id3v2/frames.d.ts
@@ -17,7 +17,13 @@ export interface MP3TagAPICFrame {
export interface MP3TagETCOFrame {
format: number;
data: Array<{
- event: number;
+ /**
+ * Event type. A single byte (0x00–0xFE) for standard events, or an
+ * array of bytes for extended events as defined by ID3v2.4 §4.5:
+ * one or more leading 0xFF escape bytes followed by a single
+ * terminating byte (0x00–0xFE).
+ */
+ event: number | number[];
time: number;
}>;
}
diff --git a/types/index.d.ts b/types/index.d.ts
index 5002568..8990598 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -21,6 +21,9 @@ export type MP3TagEncodings =
export interface MP3TagDefaultReadOptions {
id3v1: boolean;
id3v2: boolean;
+ mp4: boolean;
+ aiff: boolean;
+ aac: boolean;
unsupported: boolean;
encoding: MP3TagEncodings;
}
@@ -41,6 +44,9 @@ export interface MP3TagDefaultWriteOptions {
unsupported: boolean;
encoding: MP3TagEncodings;
};
+ mp4: {
+ language: 'und'
+ };
}
export type MP3TagReadOptions = RecursivePartial;
@@ -48,7 +54,7 @@ export type MP3TagWriteOptions = RecursivePartial;
export class MP3Tag {
readonly name = 'MP3Tag';
- readonly version = '3.14.1';
+ readonly version = '3.16.0';
verbose: boolean;
buffer: MP3Buffer;
@@ -56,6 +62,7 @@ export class MP3Tag {
error: string;
static readBuffer (buffer: MP3Buffer, options?: MP3TagReadOptions, verbose?: boolean): MP3TagTags;
+ static readBlob (blob: Blob, options?: MP3TagReadOptions): Promise;
static writeBuffer (buffer: MP3Buffer, tags: MP3TagTags, options?: MP3TagWriteOptions, verbose?: boolean): MP3Buffer;
static getAudioBuffer (buffer: MP3Buffer, emptyNone?: boolean): MP3Buffer;