This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 87ab54be43 TIKA-4784 -- improve utf16 handling in id3 (#2943)
87ab54be43 is described below
commit 87ab54be430c21afda13dfc7321e88c8c0374652
Author: Tim Allison <[email protected]>
AuthorDate: Mon Jul 13 13:09:03 2026 -0400
TIKA-4784 -- improve utf16 handling in id3 (#2943)
---
.../org/apache/tika/parser/mp3/ID3v22Handler.java | 8 +-
.../org/apache/tika/parser/mp3/ID3v23Handler.java | 8 +-
.../org/apache/tika/parser/mp3/ID3v24Handler.java | 8 +-
.../org/apache/tika/parser/mp3/ID3v2Frame.java | 74 +++++++--
.../org/apache/tika/parser/mp3/ID3v2FrameTest.java | 171 +++++++++++++++++++++
.../org/apache/tika/parser/mp3/Mp3ParserTest.java | 55 +++++++
6 files changed, 306 insertions(+), 18 deletions(-)
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
index 5f0102cd03..25bd4afc34 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
@@ -71,9 +71,13 @@ public class ID3v22Handler implements ID3Tags {
case "TCR":
copyright = getTagString(tag.data, 0, tag.data.length);
break;
- case "COM":
- comments.add(getComment(tag.data, 0, tag.data.length));
+ case "COM": {
+ ID3Comment comment = getComment(tag.data, 0,
tag.data.length);
+ if (comment != null) {
+ comments.add(comment);
+ }
break;
+ }
case "TRK":
trackNumber = getTagString(tag.data, 0, tag.data.length);
break;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
index 9d10a5ac29..be3fd4fb11 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
@@ -72,9 +72,13 @@ public class ID3v23Handler implements ID3Tags {
case "TCOP":
copyright = getTagString(tag.data, 0, tag.data.length);
break;
- case "COMM":
- comments.add(getComment(tag.data, 0, tag.data.length));
+ case "COMM": {
+ ID3Comment comment = getComment(tag.data, 0,
tag.data.length);
+ if (comment != null) {
+ comments.add(comment);
+ }
break;
+ }
case "TRCK":
trackNumber = getTagString(tag.data, 0, tag.data.length);
break;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
index f9d7b106a8..cb7fc38165 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
@@ -78,9 +78,13 @@ public class ID3v24Handler implements ID3Tags {
case "TCOP":
copyright = getTagString(tag.data, 0, tag.data.length);
break;
- case "COMM":
- comments.add(getComment(tag.data, 0, tag.data.length));
+ case "COMM": {
+ ID3Comment comment = getComment(tag.data, 0,
tag.data.length);
+ if (comment != null) {
+ comments.add(comment);
+ }
break;
+ }
case "TRCK":
trackNumber = getTagString(tag.data, 0, tag.data.length);
break;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
index 1dddd1410b..3b40a5344b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
@@ -230,14 +230,13 @@ public class ID3v2Frame implements MP3Frame {
// (return empty string), because new String(..)
// gives different results on different JVMs
if (encoding.encoding.equals("UTF-16") && actualLength == 2 &&
- ((data[offset] == (byte) 0xff && data[offset + 1] == (byte)
0xfe) ||
- (data[offset] == (byte) 0xfe && data[offset + 1] ==
(byte) 0xff))) {
+ hasBOM(data, offset, actualLength)) {
return "";
}
try {
// Build the base string
- return new String(data, offset, actualLength, encoding.encoding);
+ return decodeText(data, offset, actualLength, encoding);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Core encoding " + encoding.encoding +
" is not available",
e);
@@ -245,10 +244,59 @@ public class ID3v2Frame implements MP3Frame {
}
/**
- * Builds up the ID3 comment, by parsing and extracting
- * the comment string parts from the given data.
+ * Decodes text in the frame's declared encoding. $01 is UTF-16 with a
BOM; if a tagger omits
+ * it, Java assumes big-endian and turns little-endian text into mojibake
('T' 0x54 0x00 ->
+ * U+5400), so recover the byte order from the bytes.
+ */
+ private static String decodeText(byte[] data, int offset, int length,
TextEncoding encoding)
+ throws UnsupportedEncodingException {
+ String charset = encoding.encoding;
+ if ("UTF-16".equals(charset) && !hasBOM(data, offset, length)) {
+ charset = guessUTF16ByteOrder(data, offset, length);
+ }
+ return new String(data, offset, length, charset);
+ }
+
+ private static boolean hasBOM(byte[] data, int offset, int length) {
+ if (length < 2) {
+ return false;
+ }
+ int first = data[offset] & 0xff;
+ int second = data[offset + 1] & 0xff;
+ return (first == 0xff && second == 0xfe) || (first == 0xfe && second
== 0xff);
+ }
+
+ /**
+ * Recovers the byte order of BOM-less UTF-16, only reached when a $01
frame omits its
+ * mandatory BOM (a malformed tagger). Chars below U+0100, which dominate
ID3 tags, carry one
+ * NUL per code unit whose column reveals the order. Chars above (eg CJK)
carry no NUL and no
+ * signal; pure-CJK text then falls back to big-endian - Java's own
BOM-less default, so this
+ * is never worse than the prior unconditional decode.
+ */
+ private static String guessUTF16ByteOrder(byte[] data, int offset, int
length) {
+ int bigEndian = 0;
+ int littleEndian = 0;
+ for (int i = 0; i + 1 < length; i += 2) {
+ if (data[offset + i] == 0) {
+ bigEndian++;
+ }
+ if (data[offset + i + 1] == 0) {
+ littleEndian++;
+ }
+ }
+ return littleEndian > bigEndian ? "UTF-16LE" : "UTF-16BE";
+ }
+
+ /**
+ * Parses the comment parts from the given data, or null if the frame is
too short or
+ * malformed to hold a comment.
*/
protected static ID3Comment getComment(byte[] data, int offset, int
length) {
+ // encoding flag + 3-byte language
+ if (length < 4) {
+ return null;
+ }
+
// Comments must have an encoding
int encodingFlag = data[offset];
if (encodingFlag >= 0 && encodingFlag < encodings.length) {
@@ -264,6 +312,7 @@ public class ID3v2Frame implements MP3Frame {
String lang = getString(data, offset + 1, 3);
// After that we have [Desc]\0(\0)[Text]
+ int end = offset + length;
int descStart = offset + 4;
int textStart = -1;
String description = null;
@@ -271,29 +320,30 @@ public class ID3v2Frame implements MP3Frame {
// Find where the description ends
try {
- for (int i = descStart; i < offset + length; i++) {
- if (encoding.doubleByte && data[i] == 0 && data[i + 1] == 0) {
+ for (int i = descStart; i < end; i++) {
+ // a double byte terminator needs both bytes present
+ if (encoding.doubleByte && i + 1 < end && data[i] == 0 &&
data[i + 1] == 0) {
// Handle LE vs BE on low byte text
- if (i + 2 < offset + length && data[i + 1] == 0 && data[i
+ 2] == 0) {
+ if (i + 2 < end && data[i + 2] == 0) {
i++;
}
textStart = i + 2;
- description = new String(data, descStart, i - descStart,
encoding.encoding);
+ description = decodeText(data, descStart, i - descStart,
encoding);
break;
}
if (!encoding.doubleByte && data[i] == 0) {
textStart = i + 1;
- description = new String(data, descStart, i - descStart,
encoding.encoding);
+ description = decodeText(data, descStart, i - descStart,
encoding);
break;
}
}
// Did we find the end?
if (textStart > -1) {
- text = new String(data, textStart, offset + length -
textStart, encoding.encoding);
+ text = decodeText(data, textStart, end - textStart, encoding);
} else {
// Assume everything is the text
- text = new String(data, descStart, offset + length -
descStart, encoding.encoding);
+ text = decodeText(data, descStart, end - descStart, encoding);
}
// Return
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
new file mode 100644
index 0000000000..8fee5d672c
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.mp3;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_16BE;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.parser.mp3.ID3Tags.ID3Comment;
+
+/**
+ * Tests the shared ID3v2 text decoding used by every ID3v2.2/2.3/2.4 text
frame.
+ */
+public class ID3v2FrameTest {
+
+ private static final byte ISO_8859_1_FLAG = 0;
+ private static final byte UTF_16_BOM_FLAG = 1;
+ private static final byte UTF_16BE_FLAG = 2;
+ private static final byte UTF_8_FLAG = 3;
+
+ private static final byte[] BOM_LE = {(byte) 0xff, (byte) 0xfe};
+ private static final byte[] BOM_BE = {(byte) 0xfe, (byte) 0xff};
+
+ // LATIN has a NUL per char in UTF-16 (a byte-order signal); CJK does not
+ private static final String LATIN = "Test Copyright";
+ private static final String CJK = "日本語";
+
+ private static byte[] frame(byte encodingFlag, byte[]... parts) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write(encodingFlag);
+ for (byte[] part : parts) {
+ out.write(part, 0, part.length);
+ }
+ return out.toByteArray();
+ }
+
+ private static byte[] bytes(String text, Charset charset) {
+ return text.getBytes(charset);
+ }
+
+ private static String tagString(byte[] data) {
+ return ID3v2Frame.getTagString(data, 0, data.length);
+ }
+
+ @Test
+ public void testSingleByteEncodings() {
+ assertEquals(LATIN, tagString(frame(ISO_8859_1_FLAG, bytes(LATIN,
ISO_8859_1))));
+ assertEquals(CJK, tagString(frame(UTF_8_FLAG, bytes(CJK, UTF_8))));
+ }
+
+ @Test
+ public void testUTF16WithBOM() {
+ assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, BOM_LE,
bytes(LATIN, UTF_16LE))));
+ assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, BOM_BE,
bytes(LATIN, UTF_16BE))));
+ assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, BOM_LE, bytes(CJK,
UTF_16LE))));
+ assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, BOM_BE, bytes(CJK,
UTF_16BE))));
+ }
+
+ @Test
+ public void testUTF16BEWithoutBOMFlag() {
+ assertEquals(LATIN, tagString(frame(UTF_16BE_FLAG, bytes(LATIN,
UTF_16BE))));
+ assertEquals(CJK, tagString(frame(UTF_16BE_FLAG, bytes(CJK,
UTF_16BE))));
+ }
+
+ // $01 promises a BOM; omitting it used to decode LE as BE mojibake ('T'
0x54 0x00 -> U+5400)
+ @Test
+ public void testUTF16WithoutBOMRecoversByteOrder() {
+ assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN,
UTF_16LE))));
+ assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN,
UTF_16BE))));
+ }
+
+ // no NUL bytes (all chars above U+00FF) means no signal, so keep the
big-endian default
+ @Test
+ public void testUTF16WithoutBOMKeepsBigEndianDefaultWhenNoSignal() {
+ assertEquals(CJK, tagString(frame(UTF_16_BOM_FLAG, bytes(CJK,
UTF_16BE))));
+ }
+
+ @Test
+ public void testNullTerminationIsTrimmed() {
+ byte[] doubleNul = {0, 0};
+ byte[] singleNul = {0};
+ assertEquals(LATIN, tagString(frame(UTF_16_BOM_FLAG, bytes(LATIN,
UTF_16LE), doubleNul)));
+ assertEquals(LATIN,
+ tagString(frame(UTF_16_BOM_FLAG, BOM_LE, bytes(LATIN,
UTF_16LE), doubleNul)));
+ assertEquals(LATIN, tagString(frame(ISO_8859_1_FLAG, bytes(LATIN,
ISO_8859_1), singleNul)));
+ }
+
+ // TIKA-1024: a frame holding nothing but a BOM decodes to the empty string
+ @Test
+ public void testNakedBOM() {
+ assertEquals("", tagString(frame(UTF_16_BOM_FLAG, BOM_LE)));
+ assertEquals("", tagString(frame(UTF_16_BOM_FLAG, BOM_BE)));
+ }
+
+ // COMM decodes description and text separately, so each recovers byte
order on its own
+ @Test
+ public void testCommentWithoutBOMRecoversByteOrder() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write(UTF_16_BOM_FLAG);
+ out.write(bytes("eng", ISO_8859_1));
+ out.write(bytes("Desc", UTF_16LE));
+ out.write(new byte[]{0, 0});
+ out.write(bytes(LATIN, UTF_16LE));
+ byte[] data = out.toByteArray();
+
+ ID3Comment comment = ID3v2Frame.getComment(data, 0, data.length);
+ assertEquals("eng", comment.getLanguage());
+ assertEquals("Desc", comment.getDescription());
+ assertEquals(LATIN, comment.getText());
+ }
+
+ @Test
+ public void testCommentWithBOM() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write(UTF_16_BOM_FLAG);
+ out.write(bytes("eng", ISO_8859_1));
+ out.write(BOM_LE);
+ out.write(bytes("Desc", UTF_16LE));
+ out.write(new byte[]{0, 0});
+ out.write(BOM_LE);
+ out.write(bytes(LATIN, UTF_16LE));
+ byte[] data = out.toByteArray();
+
+ ID3Comment comment = ID3v2Frame.getComment(data, 0, data.length);
+ assertEquals("eng", comment.getLanguage());
+ assertEquals("Desc", comment.getDescription());
+ assertEquals(LATIN, comment.getText());
+ }
+
+ // too short for flag + 3 byte language, or an unknown flag, decodes to
null (no overrun)
+ @Test
+ public void testMalformedCommentsReturnNull() {
+ assertNull(ID3v2Frame.getComment(new byte[0], 0, 0));
+ assertNull(ID3v2Frame.getComment(new byte[]{UTF_16_BOM_FLAG}, 0, 1));
+ assertNull(ID3v2Frame.getComment(new byte[]{ISO_8859_1_FLAG, 'e',
'n'}, 0, 3));
+ // 0x05 is not a defined ID3v2 text encoding
+ byte[] badFlag = {5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'};
+ assertNull(ID3v2Frame.getComment(badFlag, 0, badFlag.length));
+ }
+
+ // a double byte comment ending on a lone NUL must not overrun looking for
a terminator
+ @Test
+ public void testCommentTruncatedOnTerminatorDoesNotOverrun() {
+ byte[] data = {UTF_16_BOM_FLAG, 'e', 'n', 'g', 0};
+ ID3Comment comment = ID3v2Frame.getComment(data, 0, data.length);
+ assertEquals("eng", comment.getLanguage());
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
index 935b4be893..bbe9936e2e 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
@@ -19,6 +19,9 @@ package org.apache.tika.parser.mp3;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+
import org.junit.jupiter.api.Test;
import org.apache.tika.TikaTest;
@@ -26,6 +29,8 @@ import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.metadata.XMPDM;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.BodyContentHandler;
/**
* Test case for parsing mp3 files.
@@ -362,4 +367,54 @@ public class Mp3ParserTest extends TikaTest {
assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
assertEquals("", metadata.get(XMPDM.GENRE));
}
+
+ // each body used to abort the parse of readable audio (AIOOBE / SIOOBE /
NPE)
+ @Test
+ public void testMalformedCommentFrameIsSkipped() throws Exception {
+ byte[][] bodies = {
+ new byte[0], // empty body
+ new byte[]{1}, // no language
+ new byte[]{0, 'e', 'n'}, // truncated
language
+ new byte[]{5, 'e', 'n', 'g', 'D', 'e', 's', 'c', 0, 'T'}, //
0x05 = unknown encoding
+ new byte[]{1, 'e', 'n', 'g', 0} // ends before
the double byte terminator
+ };
+ for (byte[] body : bodies) {
+ String name = "COMM body of length " + body.length;
+ Metadata metadata = new Metadata();
+ try (TikaInputStream tis =
TikaInputStream.get(mp3WithFrame("COMM", body))) {
+ new Mp3Parser().parse(tis, new BodyContentHandler(-1),
metadata, new ParseContext());
+ }
+ assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE),
name);
+ // audio behind the broken tag is still read
+ assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE), name);
+ }
+ }
+
+ // wraps the audio of testMP3noid3.mp3 in an ID3v2.3 tag holding the
single given frame
+ private byte[] mp3WithFrame(String frameId, byte[] body) throws Exception {
+ byte[] audio;
+ try (TikaInputStream tis =
getResourceAsStream("/test-documents/testMP3noid3.mp3")) {
+ audio = tis.readAllBytes();
+ }
+
+ ByteArrayOutputStream frame = new ByteArrayOutputStream();
+ frame.write(frameId.getBytes(StandardCharsets.US_ASCII));
+ // ID3v2.3 frame sizes are plain 32 bit big endian
+ frame.write(new byte[]{(byte) (body.length >>> 24), (byte)
(body.length >>> 16),
+ (byte) (body.length >>> 8), (byte) body.length});
+ frame.write(new byte[]{0, 0});
+ frame.write(body);
+ byte[] frames = frame.toByteArray();
+
+ ByteArrayOutputStream mp3 = new ByteArrayOutputStream();
+ mp3.write("ID3".getBytes(StandardCharsets.US_ASCII));
+ mp3.write(new byte[]{3, 0, 0});
+ // tag size is synchsafe (7 bits/byte), unlike the plain frame size
above
+ int size = frames.length;
+ mp3.write(new byte[]{(byte) ((size >>> 21) & 0x7f), (byte) ((size >>>
14) & 0x7f),
+ (byte) ((size >>> 7) & 0x7f), (byte) (size & 0x7f)});
+ mp3.write(frames);
+ mp3.write(audio);
+ return mp3.toByteArray();
+ }
}