Adding BASTP TagParser

This is required for ReplayGain - a POC version of the code is already implemented (no album gain, no config option yet)
This commit is contained in:
Adrian Ulrich 2012-11-11 20:29:30 +01:00
parent 75976e298a
commit d4c4498068
5 changed files with 358 additions and 5 deletions

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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<comments; i++) {
int clen = (int)b2le32(scratch, xoff);
xoff += 4+clen;
if(xoff > 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;
}
}

View File

@ -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;
}
}

View File

@ -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<nsegs; i++) {
psize += b2u(scratch[i]);
}
}
// populate result array
result[0] = (s.getFilePointer() - offset);
result[1] = psize;
result[2] = -1;
/* next byte is most likely the type -> 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);
}
};