From f3494c187735845c4ce6593cff47d8942c63a814 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 29 May 2016 18:11:02 +0200 Subject: [PATCH] sync BASTP with upstream b2ace816e9f53702f4cf4e199582430705664bb7 --- src/ch/blinkenlights/bastp/Bastp.java | 10 +- src/ch/blinkenlights/bastp/Common.java | 27 +++-- src/ch/blinkenlights/bastp/OggFile.java | 4 +- src/ch/blinkenlights/bastp/OpusFile.java | 127 +++++++++++++++++++++++ 4 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 src/ch/blinkenlights/bastp/OpusFile.java diff --git a/src/ch/blinkenlights/bastp/Bastp.java b/src/ch/blinkenlights/bastp/Bastp.java index 4e336e00..5036e807 100644 --- a/src/ch/blinkenlights/bastp/Bastp.java +++ b/src/ch/blinkenlights/bastp/Bastp.java @@ -56,8 +56,14 @@ public class Bastp { tags.put("type", "FLAC"); } else if(magic.equals("OggS")) { - tags = (new OggFile()).getTags(s); - tags.put("type", "OGG"); + // This may be an Opus OR an Ogg Vorbis file + tags = (new OpusFile()).getTags(s); + if (tags.size() > 0) { + tags.put("type", "OPUS"); + } else { + tags = (new OggFile()).getTags(s); + tags.put("type", "OGG"); + } } else if(file_ff[0] == -1 && file_ff[1] == -5) { /* aka 0xfffb in real languages */ tags = (new LameHeader()).getTags(s); diff --git a/src/ch/blinkenlights/bastp/Common.java b/src/ch/blinkenlights/bastp/Common.java index 00b557da..d879da9d 100644 --- a/src/ch/blinkenlights/bastp/Common.java +++ b/src/ch/blinkenlights/bastp/Common.java @@ -25,11 +25,11 @@ import java.util.Vector; public class Common { private static final long MAX_PKT_SIZE = 524288; - + public void xdie(String reason) throws IOException { throw new IOException(reason); } - + /* ** Returns a 32bit int from given byte offset in LE */ @@ -40,7 +40,7 @@ public class Common { } return r; } - + public int b2be32(byte[] b, int off) { return swap32(b2le32(b, off)); } @@ -48,7 +48,14 @@ public class Common { public int swap32(int i) { return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff); } - + + /* + ** Returns a 16bit int from given byte offset in LE + */ + public int b2le16(byte[] b, int off) { + return ( b2u(b[off]) | b2u(b[off+1]) << 8 ); + } + /* ** convert 'byte' value into unsigned int */ @@ -62,24 +69,22 @@ public class Common { public void debug(String s) { System.out.println("DBUG "+s); } - + public HashMap parse_vorbis_comment(RandomAccessFile s, long offset, long payload_len) throws IOException { HashMap tags = new HashMap(); int comments = 0; // number of found comments int xoff = 0; // offset within 'scratch' int can_read = (int)(payload_len > MAX_PKT_SIZE ? MAX_PKT_SIZE : payload_len); byte[] scratch = new byte[can_read]; - + // seek to given position and slurp in the payload s.seek(offset); s.read(scratch); - // skip vendor string in format: [LEN][VENDOR_STRING] xoff += 4 + b2le32(scratch, xoff); // 4 = LEN = 32bit int comments = b2le32(scratch, xoff); xoff += 4; - - // debug("comments count = "+comments); + for(int i=0; i + * + * 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 . + */ +package ch.blinkenlights.bastp; + + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; + + +public class OpusFile extends OggFile { + // A list of tags we are going to ignore in the OpusTags section + public static final String[] FORBIDDEN_TAGS = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_TRACK_PEAK", "REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_ALBUM_PEAK"}; + + public OpusFile() { + } + + public HashMap getTags(RandomAccessFile s) throws IOException { + + // The opus specification is very strict: The first packet MUST + // contain the OpusHeader while the 2nd MUST contain the + // OggHeader payload: https://wiki.xiph.org/OggOpus + long pos = 0; + long offsets[] = parse_ogg_page(s, pos); + + HashMap tags = new HashMap(); + HashMap opus_head = parse_opus_head(s, pos+offsets[0], offsets[1]); + pos += offsets[0]+offsets[1]; + + // Check if we parsed a version number and ensure it doesn't have any + // of the upper 4 bits set (eg: <= 15) + if(opus_head.containsKey("version") && (Integer)opus_head.get("version") <= 0xF) { + // Get next page: The spec requires this to be an OpusTags head + offsets = parse_ogg_page(s, pos); + tags = parse_opus_vorbis_comment(s, pos+offsets[0], offsets[1]); + // ...and merge replay gain intos into the tags map + calculate_gain(opus_head, tags); + } + + return tags; + } + + /** + * Adds replay gain information to the tags hash map + */ + private void calculate_gain(HashMap header, HashMap tags) { + // Remove any unacceptable tags (Opus files must not have + // their own REPLAYGAIN_* fields) + for(String k : FORBIDDEN_TAGS) { + tags.remove(k); + } + // Include the gain value found in the opus header + int header_gain = (Integer)header.get("header_gain"); + addTagEntry(tags, "R128_BASTP_BASE_GAIN", ""+header_gain); + } + + + /** + * Attempts to parse an OpusHead block at given offset. + * Returns an hash-map, will be empty on failure + */ + private HashMap parse_opus_head(RandomAccessFile s, long offset, long pl_len) throws IOException { + /* Structure: + * 8 bytes of 'OpusHead' + * 1 byte version + * 1 byte channel count + * 2 bytes pre skip + * 4 bytes input-sample-rate + * 2 bytes outputGain as Q7.8 + * 1 byte channel map + * --> 19 bytes + */ + + HashMap id_hash = new HashMap(); + byte[] buff = new byte[19]; + if(pl_len >= buff.length) { + s.seek(offset); + s.read(buff); + if((new String(buff, 0, 8)).equals("OpusHead")) { + id_hash.put("version" , b2u(buff[8])); + id_hash.put("channels" , b2u(buff[9])); + id_hash.put("pre_skip" , b2le16(buff, 10)); + id_hash.put("sampling_rate", b2le32(buff, 12)); + id_hash.put("header_gain" , (int)((short)b2le16(buff, 16))); + id_hash.put("channel_map" , b2u(buff[18])); + } + } + + return id_hash; + } + + /** + * Parses an OpusTags section + * Returns a hash map of the found tags + */ + private HashMap parse_opus_vorbis_comment(RandomAccessFile s, long offset, long pl_len) throws IOException { + final int magic_len = 8; // OpusTags + byte[] magic = new byte[magic_len]; + + if(pl_len < magic_len) + xdie("opus comment field is too short!"); + + // Read and check magic signature + s.seek(offset); + s.read(magic); + + if((new String(magic, 0, magic_len)).equals("OpusTags") == false) + xdie("Damaged packet found!"); + + return parse_vorbis_comment(s, offset+magic_len, pl_len-magic_len); + } + +}