diff options
author | Scott Jackson <daneren2005@users.noreply.github.com> | 2022-04-20 21:22:48 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-20 21:22:48 -0700 |
commit | 6a513834180a343773424200299d4973b6bbec52 (patch) | |
tree | b276e7ce259e30043bd82aed26e2b697f44ee958 /app/src/main/java/github/daneren2005/dsub/util/tags/Mp4File.java | |
parent | 47cd98ef2b6c9cbe17b64da67221f3238818a384 (diff) | |
parent | d0221587e9d332c522f7ac429781d4ccaa7f5ce9 (diff) | |
download | dsub-6a513834180a343773424200299d4973b6bbec52.tar.gz dsub-6a513834180a343773424200299d4973b6bbec52.tar.bz2 dsub-6a513834180a343773424200299d4973b6bbec52.zip |
Merge pull request #1094 from paroj/edge
sync bastp to upstream/ vanilla-music
Diffstat (limited to 'app/src/main/java/github/daneren2005/dsub/util/tags/Mp4File.java')
-rw-r--r-- | app/src/main/java/github/daneren2005/dsub/util/tags/Mp4File.java | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/Mp4File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/Mp4File.java new file mode 100644 index 00000000..db00bf6a --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/tags/Mp4File.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2016 Ian Harmon + * Copyright (C) 2017 Google Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package github.daneren2005.dsub.util.tags; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Stack; + +/* +* Helper class for tracking the traversal of the atom tree +*/ +class Atom { + String name; + long start; + int length; + + public Atom(String name, long start, int length) { + this.name = name; + this.start = start; + this.length = length; + } +} + +/* +* MP4 tag parser +*/ +public class Mp4File extends Common { + + // only these tags are returned. others may be parsed, but discarded. + final static List<String> ALLOWED_TAGS = Arrays.asList( + "replaygain_track_gain", + "replaygain_album_gain", + "title", + "album", + "artist", + "albumartist", + "composer", + "genre", + "year", + "tracknumber", + "discnumber" + ); + + // mapping between atom <-> vorbis tags + final static HashMap<String, String> ATOM_TAGS; + static { + ATOM_TAGS = new HashMap<String, String>(); + ATOM_TAGS.put("�nam", "title"); + ATOM_TAGS.put("�alb", "album"); + ATOM_TAGS.put("�ART", "artist"); + ATOM_TAGS.put("aART", "albumartist"); + ATOM_TAGS.put("�wrt", "composer"); + ATOM_TAGS.put("�gen", "genre"); + ATOM_TAGS.put("�day", "year"); + ATOM_TAGS.put("trkn", "tracknumber"); + ATOM_TAGS.put("disk", "discnumber"); + } + + // These tags are 32bit integers, not strings. + final static List<String> BINARY_TAGS = Arrays.asList( + "tracknumber", + "discnumber" + ); + + // maximum size for tag names or values + final static int MAX_BUFFER_SIZE = 512; + // only used when developing + final static boolean PRINT_DEBUG = false; + + // When processing atoms, we first read the atom length (4 bytes), + // and then the atom name (also 4 bytes). This value should not be changed. + final static int ATOM_HEADER_SIZE = 8; + + /* + * Traverses the atom structure of an MP4 file and returns as soon as tags + * are parsed + */ + public HashMap getTags(RandomAccessFile s) throws IOException { + HashMap tags = new HashMap(); + if (PRINT_DEBUG) { System.out.println(); } + try { + + // maintain a trail of breadcrumbs to know what part of the file we're in, + // so e.g. that we only parse [name] atoms that are part of a tag + Stack<Atom> path = new Stack<Atom>(); + + s.seek(0); + int atomSize; + byte[] atomNameRaw = new byte[4]; + String atomName; + String tagName = null; + + // begin traversing the file + // file structure info from http://atomicparsley.sourceforge.net/mpeg-4files.html + while (s.getFilePointer() < s.length()) { + + // if we've read/skipped past the end of atoms, remove them from the path stack + while (!path.empty() && s.getFilePointer() >= (path.peek().start + path.peek().length)) { + // if we've finished the tag atom [ilst], we can stop parsing. + // when tags are read successfully, this should be the exit point for the parser. + if (path.peek().name.equals("ilst")) { + if (PRINT_DEBUG) { System.out.println(); } + return tags; + } + path.pop(); + } + + // read a new atom's details + atomSize = s.readInt(); + + // return if we're unable to parse an atom size + // (e.g. previous atoms were parsed incorrectly and the + // file pointer is misaligned) + if (atomSize <= 0) { return tags; } + + s.read(atomNameRaw); + atomName = new String(atomNameRaw); + + // determine if we're currently decending through the hierarchy + // to a tag atom + boolean approachingTagAtom = false; + boolean onMetaAtom = false; + boolean onTagAtom = false; + String fourAtom = null; + // compare everything in the current path hierarchy and the new atom as well + // this is a bit repetitive as-is, but shouldn't be noticeable + for (int i = 0; i <= path.size(); i++) { + String thisAtomName = (i < path.size()) ? path.get(i).name : atomName; + if ((i == 0 && thisAtomName.equals("moov")) || + (i == 1 && thisAtomName.equals("udta")) || + (i == 2 && thisAtomName.equals("meta")) || + (i == 3 && thisAtomName.equals("ilst")) || + (i == 4 && thisAtomName.equals("----")) || + (i == 4 && ATOM_TAGS.containsKey(thisAtomName)) || + (i == 5 && (thisAtomName.equals("name") || thisAtomName.equals("data"))) + ) { + approachingTagAtom = true; + // if we're at the end of the current hierarchy, mark if it's the [meta] or a tag atom. + if (i == path.size()) { + onMetaAtom = thisAtomName.equals("meta"); + onTagAtom = (thisAtomName.equals("name") || thisAtomName.equals("data")); + } + // depth is 4 and this is a known atom: rembemer this! + if (i == 4 && ATOM_TAGS.containsKey(thisAtomName)) { + fourAtom = ATOM_TAGS.get(thisAtomName); + } + } + // quit as soon as we know we're not on the road to a tag atom + else { + approachingTagAtom = false; + break; + } + } + + // add the new atom to the path hierarchy + path.push(new Atom(atomName, s.getFilePointer()-ATOM_HEADER_SIZE, atomSize)); + if (PRINT_DEBUG) { printDebugAtomPath(s, path, atomName, atomSize); } + + // skip all non-pertinent atoms + if (!approachingTagAtom) { s.skipBytes(atomSize-ATOM_HEADER_SIZE); } + // dive into tag-related ones + else { + // the meta atom has an extra 4 bytes that need to be skipped + if (onMetaAtom) { s.skipBytes(4); } + + // read tag contents when there + if (onTagAtom) { + // get a tag name + if (atomName.equals("name")) { + // skip null bytes + s.skipBytes(4); + tagName = new String(readIntoBuffer(s, atomSize-(ATOM_HEADER_SIZE+4))); + } + + // get a tag value + else if (atomName.equals("data")) { + // skip flags/null bytes + s.skipBytes(8); + + // use the 'fourAtom' value if we did not have a tag name + tagName = (tagName == null ? fourAtom : tagName); + // read the tag + byte[] tagBuffer = readIntoBuffer(s, atomSize-(ATOM_HEADER_SIZE+8)); + + if (ALLOWED_TAGS.contains(tagName)) + { + String tagValue = (BINARY_TAGS.contains(tagName) ? String.format("%d", b2be32(tagBuffer, 0)) : new String(tagBuffer, "UTF-8")); + if (PRINT_DEBUG) { + System.out.println(String.format("parsed tag '%s': '%s'\n", tagName, tagValue)); + } + addTagEntry(tags, tagName.toUpperCase(), tagValue); + } + // This is the end of this tree, make sure that we don't re-use tagName in any other tree + tagName = null; + } + } + } + } + // End of while loop, the file has been completely read through. + // The parser should only return here if the tags atom [ilst] was missing. + return tags; + } + // if anything goes wrong, just return whatever we already have + catch (Exception e) { + return tags; + } + } + + /* + * Reads bytes from an atom up to the buffer size limit, currently 512B + */ + private byte[] readIntoBuffer(RandomAccessFile s, int dataSize) throws IOException { + // read tag up to buffer limit + int bufferSize = Math.min(dataSize, MAX_BUFFER_SIZE); + byte[] buffer = new byte[bufferSize]; + s.read(buffer, 0, buffer.length); + if (dataSize > bufferSize) { + s.skipBytes(dataSize - bufferSize); + } + return buffer; + } + + /* + * Can be used when traversing the atom hierarchy to print the tree of atoms + */ + private void printDebugAtomPath(RandomAccessFile s, Stack<Atom> path, + String atomName, int atomSize) throws IOException + { + String treeLines = ""; + for (int i = 0; i < path.size(); i++) { treeLines += ". "; } + long atomStart = s.getFilePointer()-ATOM_HEADER_SIZE; + System.out.println(String.format("%-22s %8d to %8d, length %8d", + (treeLines + "[" + atomName + "]"), atomStart, (atomStart+atomSize), atomSize)); + } +} |