From d4c4498068109f6438dd7a38f54eb97657902216 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 11 Nov 2012 20:29:30 +0100 Subject: [PATCH] Adding BASTP TagParser This is required for ReplayGain - a POC version of the code is already implemented (no album gain, no config option yet) --- .../android/vanilla/PlaybackService.java | 36 +++++- src/ch/blinkenlights/bastp/Bastp.java | 50 ++++++++ src/ch/blinkenlights/bastp/Common.java | 92 +++++++++++++++ src/ch/blinkenlights/bastp/FlacFile.java | 78 +++++++++++++ src/ch/blinkenlights/bastp/OggFile.java | 107 ++++++++++++++++++ 5 files changed, 358 insertions(+), 5 deletions(-) create mode 100644 src/ch/blinkenlights/bastp/Bastp.java create mode 100644 src/ch/blinkenlights/bastp/Common.java create mode 100644 src/ch/blinkenlights/bastp/FlacFile.java create mode 100644 src/ch/blinkenlights/bastp/OggFile.java diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index 89e6ce73..3fae0e3b 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -66,6 +66,10 @@ import java.io.EOFException; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Vector; +import ch.blinkenlights.bastp.Bastp; + /** * Handles music playback and pretty much all the other work. @@ -555,6 +559,31 @@ public final class PlaybackService extends Service return mp; } + public void prepareMediaPlayer(MediaPlayer mp, String path) throws IOException{ + + mp.setDataSource(path); + + Hashtable tags = (new Bastp()).getTags(path); + float adjust = 1.0f; + + if(tags.containsKey("REPLAYGAIN_TRACK_GAIN")) { + String rg_raw = (String)((Vector)tags.get("REPLAYGAIN_TRACK_GAIN")).get(0); + String rg_numonly = ""; + float rg_float = 0f; + try { + String nums = rg_raw.replaceAll("[^0-9.-]",""); + rg_float = Float.parseFloat(nums); + } catch(Exception e) {} + + adjust = (float)Math.pow(10, (rg_float/20) ); + Toast.makeText(this, path+"\n"+" PX "+rg_raw+" adj = "+adjust, Toast.LENGTH_LONG).show(); + + } + + mp.setVolume(adjust, adjust); + mp.prepare(); + } + /** * Destroys any currently prepared MediaPlayer and * re-creates a newone if needed. @@ -586,8 +615,7 @@ public final class PlaybackService extends Service && !mTimeline.isEndOfQueue() ) { try { mPreparedMediaPlayer = getNewMediaPlayer(); - mPreparedMediaPlayer.setDataSource(nextSong.path); - mPreparedMediaPlayer.prepare(); + prepareMediaPlayer(mPreparedMediaPlayer, nextSong.path); mMediaPlayer.setNextMediaPlayer(mPreparedMediaPlayer); Log.d("VanillaMusic", "New media player prepared as "+mPreparedMediaPlayer+" with path "+nextSong.path); } catch (IOException e) { @@ -1051,14 +1079,12 @@ public final class PlaybackService extends Service if(mPreparedMediaPlayer != null && mPreparedMediaPlayer.isPlaying()) { - Log.d("VanillaMusic", "Replacing existing mediaplayer object with prepared version"); mMediaPlayer.release(); mMediaPlayer = mPreparedMediaPlayer; mPreparedMediaPlayer = null; } else { - mMediaPlayer.setDataSource(song.path); - mMediaPlayer.prepare(); + prepareMediaPlayer(mMediaPlayer, song.path); } mMediaPlayerInitialized = true; diff --git a/src/ch/blinkenlights/bastp/Bastp.java b/src/ch/blinkenlights/bastp/Bastp.java new file mode 100644 index 00000000..d3afe0a2 --- /dev/null +++ b/src/ch/blinkenlights/bastp/Bastp.java @@ -0,0 +1,50 @@ +package ch.blinkenlights.bastp; + +import ch.blinkenlights.bastp.OggFile; +import ch.blinkenlights.bastp.FlacFile; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.util.Hashtable; + + +public class Bastp { + + public Bastp() { + } + + public Hashtable getTags(String fname) { + Hashtable tags = new Hashtable(); + try { + RandomAccessFile ra = new RandomAccessFile(fname, "r"); + tags = getTags(ra); + ra.close(); + } + catch(Exception e) { + /* we dont' care much: SOMETHING went wrong. d'oh! */ + } + + return tags; + } + + public Hashtable getTags(RandomAccessFile s) { + Hashtable tags = new Hashtable(); + byte[] file_ff = new byte[4]; + + try { + s.read(file_ff); + String magic = new String(file_ff); + if(magic.equals("fLaC")) { + tags = (new FlacFile()).getTags(s); + } + else if(magic.equals("OggS")) { + tags = (new OggFile()).getTags(s); + } + tags.put("_MAGIC", magic); + } + catch (IOException e) { + } + return tags; + } + +} + diff --git a/src/ch/blinkenlights/bastp/Common.java b/src/ch/blinkenlights/bastp/Common.java new file mode 100644 index 00000000..99bccc8a --- /dev/null +++ b/src/ch/blinkenlights/bastp/Common.java @@ -0,0 +1,92 @@ +package ch.blinkenlights.bastp; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Hashtable; +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 + */ + public int b2le32(byte[] b, int off) { + int r = 0; + for(int i=0; i<4; i++) { + r |= ( b2u(b[off+i]) << (8*i) ); + } + return r; + } + + public int b2be32(byte[] b, int off) { + return swap32(b2le32(b, off)); + } + + public int swap32(int i) { + return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff); + } + + /* + ** convert 'byte' value into unsigned int + */ + public int b2u(byte x) { + return (x & 0xFF); + } + + /* + ** Printout debug message to STDOUT + */ + public void debug(String s) { + System.out.println("DBUG "+s); + } + + public Hashtable parse_vorbis_comment(RandomAccessFile s, long offset, long payload_len) throws IOException { + Hashtable tags = new Hashtable(); + 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 scratch.length) + xdie("string out of bounds"); + + String tag_raw = new String(scratch, xoff-clen, clen); + String[] tag_vec = tag_raw.split("=",2); + String tag_key = tag_vec[0].toUpperCase(); + + /* A key can have multiple values, so we need + ** to store all child data in an array */ + if(tags.containsKey(tag_key)) { + ((Vector)tags.get(tag_key)).add(tag_vec[1]); // just add to existing vecotr + } + else { + Vector vx = new Vector(); + vx.add(tag_vec[1]); + tags.put(tag_key, vx); + } + + } + return tags; + } + +} diff --git a/src/ch/blinkenlights/bastp/FlacFile.java b/src/ch/blinkenlights/bastp/FlacFile.java new file mode 100644 index 00000000..e98bfd77 --- /dev/null +++ b/src/ch/blinkenlights/bastp/FlacFile.java @@ -0,0 +1,78 @@ +/***************************************************************** + * This file is part of 'bastp!' - the BuggyAndSloppyTagParser! * + * * + * (C) 2012 Adrian Ulrich * + * * + * Released as 'Public Domain' software * + * * + * * + *****************************************************************/ + +package ch.blinkenlights.bastp; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Hashtable; +import java.util.Enumeration; + + +public class FlacFile extends Common { + private static final int FLAC_TYPE_COMMENT = 4; // ID of 'VorbisComment's + + public FlacFile() { + } + + public Hashtable getTags(RandomAccessFile s) throws IOException { + int xoff = 4; // skip file magic + int retry = 64; + int r[]; + Hashtable tags = new Hashtable(); + + for(; retry > 0; retry--) { + r = parse_metadata_block(s, xoff); + + if(r[2] == FLAC_TYPE_COMMENT) { + tags = parse_vorbis_comment(s, xoff+r[0], r[1]); + break; + } + + if(r[3] != 0) + break; // eof reached + + // else: calculate next offset + xoff += r[0] + r[1]; + } + return tags; + } + + /* Parses the metadata block at 'offset' and returns + ** [header_size, payload_size, type, stop_after] + */ + private int[] parse_metadata_block(RandomAccessFile s, long offset) throws IOException { + int[] result = new int[4]; + byte[] mb_head = new byte[4]; + int stop_after = 0; + int block_type = 0; + int block_size = 0; + + s.seek(offset); + + if( s.read(mb_head) != 4 ) + xdie("failed to read metadata block header"); + + block_size = b2be32(mb_head,0); // read whole header as 32 big endian + block_type = (block_size >> 24) & 127; // BIT 1-7 are the type + stop_after = (((block_size >> 24) & 128) > 0 ? 1 : 0 ); // BIT 0 indicates the last-block flag + block_size = (block_size & 0x00FFFFFF); // byte 1-7 are the size + + // debug("size="+block_size+", type="+block_type+", is_last="+stop_after); + + result[0] = 4; // hardcoded - only returned to be consistent with OGG parser + result[1] = block_size; + result[2] = block_type; + result[3] = stop_after; + + return result; + } + +} diff --git a/src/ch/blinkenlights/bastp/OggFile.java b/src/ch/blinkenlights/bastp/OggFile.java new file mode 100644 index 00000000..30d21a49 --- /dev/null +++ b/src/ch/blinkenlights/bastp/OggFile.java @@ -0,0 +1,107 @@ +/***************************************************************** + * This file is part of 'bastp!' - the BuggyAndSloppyTagParser! * + * * + * (C) 2012 Adrian Ulrich * + * * + * Released as 'Public Domain' software * + * * + * * + *****************************************************************/ + +package ch.blinkenlights.bastp; + + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Hashtable; + + +public class OggFile extends Common { + + private static final int OGG_PAGE_SIZE = 27; // Static size of an OGG Page + private static final int OGG_TYPE_COMMENT = 3; // ID of 'VorbisComment's + + public OggFile() { + } + + public Hashtable getTags(RandomAccessFile s) throws IOException { + long offset = 0; + int retry = 64; + Hashtable tags = new Hashtable(); + + for( ; retry > 0 ; retry-- ) { + long res[] = parse_ogg_page(s, offset); + if(res[2] == OGG_TYPE_COMMENT) { + tags = parse_ogg_vorbis_comment(s, offset+res[0], res[1]); + break; + } + offset += res[0] + res[1]; + } + return tags; + } + + + /* Parses the ogg page at offset 'offset' and returns + ** [header_size, payload_size, type] + */ + private long[] parse_ogg_page(RandomAccessFile s, long offset) throws IOException { + long[] result = new long[3]; // [header_size, payload_size] + byte[] p_header = new byte[OGG_PAGE_SIZE]; // buffer for the page header + byte[] scratch; + int bread = 0; // number of bytes read + int psize = 0; // payload-size + int nsegs = 0; // Number of segments + + s.seek(offset); + bread = s.read(p_header); + if(bread != OGG_PAGE_SIZE) + xdie("Unable to read() OGG_PAGE_HEADER"); + if((new String(p_header, 0, 5)).equals("OggS\0") != true) + xdie("Invalid magic - not an ogg file?"); + + nsegs = b2u(p_header[26]); + // debug("> file seg: "+nsegs); + if(nsegs > 0) { + scratch = new byte[nsegs]; + bread = s.read(scratch); + if(bread != nsegs) + xdie("Failed to read segtable"); + + for(int i=0; i pre-read */ + if(psize >= 1 && s.read(p_header, 0, 1) == 1) { + result[2] = b2u(p_header[0]); + } + + return result; + } + + /* In 'vorbiscomment' field is prefixed with \3vorbis in OGG files + ** we check that this marker is present and call the generic comment + ** parset with the correct offset (+7) */ + private Hashtable parse_ogg_vorbis_comment(RandomAccessFile s, long offset, long pl_len) throws IOException { + final int pfx_len = 7; + byte[] pfx = new byte[pfx_len]; + + if(pl_len < pfx_len) + xdie("ogg vorbis comment field is too short!"); + + s.seek(offset); + s.read(pfx); + + if( (new String(pfx, 0, pfx_len)).equals("\3vorbis") == false ) + xdie("Damaged packet found!"); + + return parse_vorbis_comment(s, offset+pfx_len, pl_len-pfx_len); + } + +};