package at.cbh.jtagger.musicfile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import at.cbh.jtagger.exceptions.NoFrameHeaderFoundException;
import at.cbh.jtagger.exceptions.NotAMp3FileExcpetion;
import at.cbh.jtagger.util.JTaggerUtils;
/*
* Diese Klasse erweiter die Klasse File um methoden die
* relevant sind um Informationen aus einem MP3 File
* auszulesen. Ein Mp3 File besteht aus vielen Frames die
* alle einen 4 Byte grossen Header besitzen. In diesem
* Header ist Information zu dem Mp3 gespeicher wie Bitrate
* Samplingrate usw. Zusätzlich wurde noch eine Methode hinzugefügt
* mit der sich die Dauer eines SOngs berechnen lässt. Die
* Grösse des Files bekommt man über die Methoden der File Klasse.
*
* Eine genaue Spezifikation des MP3 Frame headers ist unter
* folgendem Link zu finden:
*
* [url]http://www.mp3-tech.org/programmer/frame_header.html[/url]
*
* author: ******
* version: 0.7
*
*/
/*
* TODO: - Methode implementieren die prüft ob das Mp3 eine variable
* Bitrate hat.
* - Einen besseren Weg finden den Frame Header eindeutig zu
* validieren.
* - Exceptions werfen
*/
@SuppressWarnings("serial")
public class Mp3File extends MusicFile {
/*
* In diesem String ist der Frameheader gespeichert
*
* TODO: In einer späteren Verison sollte der Header in
* einem Byte Array gespeichert werden.
*/
private String firstFrameHeader;
private int bitrate;
private long duration;
private int samplingRate;
private String MPEGVersion;
private String layer;
private boolean hasProtectionBit = false;
private boolean isPadded = false;
private String channelMode;
private boolean hasCopyright = false;
private boolean isCopyOfOriginal = false;
private String emphasis;
/*
* Diese Variablen speicerh layer version und mpeg
* versiojn und dienen als Tabellenindex zur Brechnung
* der Bitrate und der Samplingrate
*/
private int l;
private int v;
@SuppressWarnings("unused")
private long startPosOfFirstFrameHeader;
/*
* Der Konstruktor erzeugt ein neues Mp3 Objekt. Es wird
* geprüft ob es sich um ein Mp3 file hadelt in dem die
* dateiendung mit allen variationen von .mp3 verglichen
* wird. Danach wird der Header des ersten frames gesucht
* und falls gefunden ausgelesen.
*
* Handelt es sich um ein anderes Fileformat oder wird
* kein FrameHeader gefunden so wird eine Exception geworfen.
*/
public Mp3File(String pathname) throws NotAMp3FileExcpetion, NoFrameHeaderFoundException {
super(pathname);
if (!this.isMp3File())
throw new NotAMp3FileExcpetion(pathname
+ " is not a valid Mp3 File");
/*
* Dies ist ein Zeiger. Er startet bei Position null
* im File und dient dem auffinden des Frameheaders
*/
startPosOfFirstFrameHeader = 0L;
init();
}
/*
* Diese Methode prüft ob die Dateiendung MP3 ist und das file
* tatsächlich eine Datei ist und man lesend darauf zugreifen
* kann.
*/
private boolean isMp3File() {
return (this.getName().endsWith(".mp3")
|| this.getName().endsWith(".MP3")
|| this.getName().endsWith(".Mp3") || this.getName().endsWith(
".mP3"))
&& this.canRead() && this.isFile();
}
/*
* Diese Methode sucht den ersten MP3 Frame Headers
* im File. Es wird zuerst das Syncwort gesucht. Das
* Synwortist 0xff dem 3 weiter Bytes folgen die gewisse
* Kriteren erfüllen müssen um als frame Header durchzugehen.
* Die Methode sucht einfach das auftreten des ersten Syncwortes
* und vergleicht die 4 Bytes inkl. Synwort mit einem Muster.
* Ist es ein gültiger Frameheader so wird er als String in
* die Variable firstFrameHeader geschrieben. Wenn nicht
* so wir weiter im File gesucht.
*/
private void getFirstFrameHeader() throws NoFrameHeaderFoundException {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(this,"r");
} catch (FileNotFoundException e) {
System.out.println("File not found");
}
/*
* Im Speicher werden 65536 Bytes für blöcke aus
* dem File reserviert die dann mit dem muster
* byteweise verglichen werden.
*/
byte byteBlock[] = new byte[1000];
/*
* BlockCounter zählt wieviel blöcke schon gelesen
* werden und dient grundsätzlich zur positionsberechnung
* des Zeigers.
*/
long blockCounter = 0;
/*
* Wenn der header gefunden wurde wird die schleife verlassen
*/
boolean headerFound = false;
try {
/*
* Hier wird der erste 65K Block in den
* Hauptspeicher gelesen.
*/
raf.read(byteBlock);
} catch (IOException e1) {
e1.printStackTrace();
}
/*
* Solange kein Header gefunden wurde wird der ByteBlock byteweise
* durchlaufen und geprüft ob es sich bei byte[i] + die nächsten 3
* Bytes um einen gültiogen Header Handeln kann. Ist dies der Fall
* wird die Schleife verlassen. Wenn nicht wird bis zum ende des
* Blockes weitergesucht. Da die letzten 3 Bytes nicht geprüft werden
* können (da ja noch kein neuer Block von der HD gelesen wurde werden
* die letzten 3 Bytes einfach beim neuen Block dazugelesen weshalb
* das RAF auch auch blockCounter * 65536 - 3 * blockCounter gesetzt
* wird.
*/
while (!headerFound) {
for (int i = 0; i < byteBlock.length - 3; i++) {
if (byteBlock[i] == (byte) 0xff) {
if (isHeader(byteBlock[i + 1], byteBlock[i + 2],
byteBlock[i + 3])) {
firstFrameHeader = "11111111"
+ JTaggerUtils.intArrayToString(JTaggerUtils
.getBits(byteBlock[i + 1]))
+ JTaggerUtils.intArrayToString(JTaggerUtils
.getBits(byteBlock[i + 2]))
+ JTaggerUtils.intArrayToString(JTaggerUtils
.getBits(byteBlock[i + 3]));
headerFound = true;
/*
* Da der Header gefunden und in den SPeicher geschrieben
* wurde kann die Schleife verlassen werden.
*/
System.out.println(i);
break;
}
}
}
blockCounter++;
/*
* Da im ersten Block nix gefunden wurde wird ein neuer
* Block aus dem File in den Speicher gelesen.
*/
try {
raf.seek(blockCounter * 1000 - 3 * blockCounter);
raf.read(byteBlock);
} catch (IOException e) {
e.printStackTrace();
}
/*
* Wenn kein Header gefunden wurde wird das RAF geschlossen
* und eine Exception geworfen.
*/
if((blockCounter * 1000 - 3 * blockCounter) > this.length()) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
throw new NoFrameHeaderFoundException("No frameHeaderFound");
}
}
/*
* Wenn alles gepasst hat wird das RAF geschlossen
*/
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/*
* Diese Methode prüft ob die 3 Bytes die dem Syncwort folgen
* einen gültigen Mp3 Frame Header formen. Welche kriterien
* das sind kann unter folgendem Link nachgelesen werden:
*
* [url]http://www.mp3-tech.org/programmer/frame_header.html[/url]
*
* Die Methode prüft die kriteren Byteweise und macht
* AND verknüpfungen mit dem vorherigen Bytewerten.
*
*/
private boolean isHeader(int byteTwo, int byteThree, int byteFour) {
int two[] = JTaggerUtils.getBits(byteTwo);
int three[] = JTaggerUtils.getBits(byteThree);
int four[] = JTaggerUtils.getBits(byteFour);
/*
* Hier wird geprüft ob die ersten 4 Bits des
* zweiten Bytes gesetzt sind.
*/
if (two[0] == 0 || two[1] == 0 || two[2] == 0 || two[3] == 0)
return false;
/*
* Hier wird geprüft ob Bit 4 und Bit 5
* nicht 01 lauten und Bit 6 und Bit 7 des zweiten
* Bytes nicht 00 lauten da diese Werte reserviert sind
* und auf ein knock out kriterium ist
*/
//if (two[3] == 0 && two[4] == 1)
//return false;
if(two[5] == 0 && two[6] == 0)
return false;
/*
* Beim dritten Byte dürfen die ersten 4 Bits nicht
* gesetzt sein.
*/
if (three[0] == 1 && three[1] == 1 && three[2] == 1 && three[3] == 1)
return false; // marks a bad header
if (three[0] == 0 && three[1] == 0 && three[2] == 0 && three[3] == 0)
return false; // marks a bad header
if (three[4] == 1 && three[5] == 1)
return false; // marks a bad header
/*
* Beim vierten Byte dürfen die letzten 2 Bits nicht
* 10 sein.
*/
if (four[6] == 1 && four[7] == 0)
return false;
return true;
}
/*
* Diese Methode ermittelt aus dem übergebenen Code
* die MPEG Version. Der Code ist der Index für
* folgende Wertetabelle:
*
* 00 - MPEG Version 2.5 (later extension of MPEG 2)
* 01 - reserved
* 10 - MPEG Version 2 (ISO/IEC 13818-3)
* 11 - MPEG Version 1 (ISO/IEC 11172-3)
*
*/
private void detectMPEGVersion(String code) {
final String MPEG_VERSION25 = "00";
final String MPEG_VERSION2 = "10";
final String MPEG_VERSION1 = "11";
if (code.equals(MPEG_VERSION25)) {
MPEGVersion = "MPEG Version 2.5";
/*
* v = 3 ist der Wert für 2.5
*/
v = 3;
} else if (code.equals(MPEG_VERSION2)) {
MPEGVersion = "MPEG Version 2";
v = 2;
} else if (code.equals(MPEG_VERSION1)) {
MPEGVersion = "MPEG Version 1";
v = 1;
}
}
/*
* Diese Methode ermittelt aus dem übergebenen Code
* die MPEG Version. Der Code ist der Index für
* folgende Wertetabelle:
*
* 00 - reserved
* 01 - Layer III
* 10 - Layer II
* 11 - Layer I
*
*/
private void detectLayerDescription(String code) {
final String LAYER3 = "01";
final String LAYER2 = "10";
final String LAYER1 = "11";
if (code.equals(LAYER3)) {
layer = "Layer III";
l = 3;
} else if (code.equals(LAYER2)) {
layer = "Layer II";
l = 2;
} else if (code.equals(LAYER1)) {
layer = "Layer I";
l = 1;
}
}
/*
* Diese Methode prüft ob das Protection
* Bit gesetzt ist und setzt bei Bedarf den
* Marker auf true.
*/
private void detectProtectionBit(String code) {
if (code.equals("1"))
hasProtectionBit = true;
}
/*
* Diese Methode schlägt den Wert der Bitrat in einer
* Tabell nach. Benötigt werden dafür die Indizes code
* version und layer. Die Werte werden aus folgender
* Tabelle entnommen:
*
* bits V1,L1 V1,L2 V1,L3 V2,L1 V2, L2 & L3
* 0000 free free free free free
* 0001 32 32 32 32 8
* 0010 64 48 40 48 16
* 0011 96 56 48 56 24
* 0100 128 64 56 64 32
* 0101 160 80 64 80 40
* 0110 192 96 80 96 48
* 0111 224 112 96 112 56
* 1000 256 128 112 128 64
* 1001 288 160 128 144 80
* 1010 320 192 160 160 96
* 1011 352 224 192 176 112
* 1100 384 256 224 192 128
* 1101 416 320 256 224 144
* 1110 448 384 320 256 160
* 1111 bad bad bad bad bad
*/
private void detectBitRate(String code, int vIn, int lIn) {
/*
* Hier wird ein 2 Dimensionales Array initialisiert
* dass die Werte der BitRate Tabelle aufnimmt.
*/
System.out.println(code + " vIn " + vIn + " lIn " + lIn);
final int BT[][] = new int[14][5];
/*
* Hier wird die BitRate Tabelle mit Werten gefüllt
*/
BT[0][0]= 32; BT[0][1]= 32; BT[0][2]= 32; BT[0][3]= 32; BT[0][4]= 8;
BT[1][0]= 64; BT[1][1]= 48; BT[1][2]= 40; BT[1][3]= 48; BT[1][4]= 16;
BT[2][0]= 96; BT[2][1]= 56; BT[2][2]= 48; BT[2][3]= 56; BT[2][4]= 24;
BT[3][0]= 128; BT[3][1]= 64; BT[3][2]= 56; BT[3][3]= 64; BT[3][4]= 32;
BT[4][0]= 160; BT[4][1]= 80; BT[4][2]= 64; BT[4][3]= 80; BT[4][4]= 40;
BT[5][0]= 192; BT[5][1]= 96; BT[5][2]= 80; BT[5][3]= 96; BT[5][4]= 48;
BT[6][0]= 224; BT[6][1]= 112; BT[6][2]= 96; BT[6][3]= 112; BT[6][4]= 56;
BT[7][0]= 256; BT[7][1]= 128; BT[7][2]= 112; BT[7][3]= 128; BT[7][4]= 64;
BT[8][0]= 288; BT[8][1]= 160; BT[8][2]= 128; BT[8][3]= 144; BT[8][4]= 80;
BT[9][0]= 320; BT[9][1]= 192; BT[9][2]= 160; BT[9][3]= 160; BT[9][4]= 96;
BT[10][0]= 352; BT[10][1]= 224; BT[10][2]= 192; BT[10][3]= 176; BT[10][4]= 112;
BT[11][0]= 384; BT[11][1]= 256; BT[11][2]= 224; BT[11][3]= 192; BT[11][4]= 128;
BT[12][0]= 416; BT[12][1]= 320; BT[12][2]= 256; BT[12][3]= 224; BT[12][4]= 144;
BT[13][0]= 488; BT[13][1]= 384; BT[13][2]= 320; BT[13][3]= 256; BT[13][4]= 160;
int n = 0;
int m = 0;
/*
* Der Version des Layers und des Mpeg Codes entsprechen werden
* die Werte aus der Tabelle gelesen
*/
if (vIn == 1 && lIn == 1)
n = 0;
if (vIn == 1 && lIn == 2)
n = 1;
if (vIn == 1 && lIn == 3)
n = 2;
if ((vIn == 2 || vIn == 3) && lIn == 2)
n = 3;
if ((vIn == 2 || vIn == 3) && (lIn == 2 || lIn == 3))
n = 4;
m = JTaggerUtils.getDecFromBinary(code) - 1;
/*
* TODO: Exception werfen
*/
if(m > 13 || m < 0 || n < 0) {
bitrate = 0;
return;
}
bitrate = BT[m][n];
}
/*
* Diese Methode schlägt die Sampling Rate des Files mittels
* der Indizes code und der version in folgender Tabelle nach:
*
* 00 44100 Hz 22050 Hz 11025 Hz
* 01 48000 Hz 24000 Hz 12000 Hz
* 10 32000 Hz 16000 Hz 8000 Hz
* 11 reserv. reserv. reserv.
*
*/
private void detectSampleRate(String code,int vIn) {
final int SRT[][] = new int[3][3];
SRT[0][0] = 44100; SRT[0][1] = 22050; SRT[0][2] = 11025;
SRT[1][0] = 48000; SRT[1][1] = 24000; SRT[1][2] = 12000;
SRT[2][0] = 32000; SRT[2][1] = 16000; SRT[2][2] = 8000;
int index = JTaggerUtils.getDecFromBinary(code);
/*
* TODO: Exception werfen
*/
if(index < 0) {
samplingRate = 0;
return;
}
samplingRate = SRT[index][vIn - 1];
}
/*
* Prüft ob das Padding bit gesetzt ist
*/
private void detectPaddingBit(String code) {
if (code.equals("1")) isPadded = true;
}
/*
* Liest den Channel Mode aus dem header
*/
private void detectChannelMode(String code) {
final String CM[] = { "Stereo", "Joint Stereo (Stereo)",
"Dual Channel (Stereo)", "Single Channel (Mono)" };
int index = JTaggerUtils.getDecFromBinary(code);
/*
* TODO: Exception werfen
*/
if(index < 0 || index > 3) {
channelMode = "";
return;
}
channelMode = CM[index];
}
/*
* Prüft ob das Copyrigth bit gesetzt ist.
*/
private void detectCopyRight(String code) {
if (code.equals("1"))
hasCopyright = true;
}
/*
* Prüft ob das File ein original ist.
*/
private void detectOriginal(String code) {
if (code.equals("1"))
isCopyOfOriginal = true;
}
/*
* Setzt das Emphasis des files
*/
private void detectEmphasis(String code) {
int index = JTaggerUtils.getDecFromBinary(code);
/*
* TODO: Exception werfen
*/
if(index == 0) emphasis = "none";
else if(index == 1) emphasis = "50/15 ms";
else if( index == 3) emphasis = "CCIT J.17";
else emphasis = "Error";
}
public int getBitrate() {
return bitrate;
}
public String getChannelMode() {
return channelMode;
}
public long getDuration() {
return duration;
}
public String getEmphasis() {
return emphasis;
}
public boolean isHasCopyright() {
return hasCopyright;
}
public boolean isHasProtectionBit() {
return hasProtectionBit;
}
public boolean isCopyOfOriginal() {
return isCopyOfOriginal;
}
public boolean isPadded() {
return isPadded;
}
public String getLayer() {
return layer;
}
public String getMPEGVersion() {
return MPEGVersion;
}
public int getSamplingRate() {
return samplingRate;
}
public String getHeader() {
return firstFrameHeader;
}
/*
* Sobald der Frame Header im File gefunden wurde können
* die Werte initialisiert werden. Die Strings sind gleich
* benannt wie die Positionsvariablen im Header:
*
* AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
*
* Diese Strings werden an die einzelnen Methoden übergeben
* die dann die Information für die Klasse bereitstellen.
*/
private void init() throws NoFrameHeaderFoundException {
getFirstFrameHeader();
/*
* TODO Exception werfen
*/
String B = firstFrameHeader.substring(11, 13);
String C = firstFrameHeader.substring(13, 15);
String D = firstFrameHeader.substring(15, 16);
String E = firstFrameHeader.substring(16, 20);
String F = firstFrameHeader.substring(20, 22);
String G = firstFrameHeader.substring(22, 23);
String I = firstFrameHeader.substring(24, 26);
String K = firstFrameHeader.substring(28, 29);
String L = firstFrameHeader.substring(29, 30);
String M = firstFrameHeader.substring(30, 32);
detectMPEGVersion(B);
detectLayerDescription(C);
detectProtectionBit(D);
detectBitRate(E, v, l);
duration = JTaggerUtils.calcDuration(this.length(), bitrate);
detectSampleRate(F, v);
detectPaddingBit(G);
detectChannelMode(I);
detectCopyRight(K);
detectOriginal(L);
detectEmphasis(M);
}
@Override
public String getMusicFileDescription() {
return "MP3";
}
}