package freenet.client.filter; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import freenet.l10n.NodeL10n; import freenet.support.Logger; public class MP3Filter implements ContentDataFilter { // Various sources on the Internet. // The most comprehensive one appears to be: // http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm // Others: // http://www.mp3-tech.org/programmer/frame_header.html // http://www.codeproject.com/KB/audio-video/mpegaudioinfo.aspx // http://www.id3.org/mp3Frame // http://www.mp3-converter.com/mp3codec/ static final short[] [] [] bitRateIndices = { //Version 2.5 { {}, {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256} }, //Reserved { }, //Version 2.0 { {}, {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256} }, //Version 1 { {}, {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}, {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}, {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448} } }; static final int[] [] sampleRateIndices = { //Version 2.5 {11025, 12000, 8000}, //Reserved {}, //Version 2.0 {22050, 24000, 16000}, //Version 1 {44100, 48000, 32000} }; // Samples per frame for each [version][layer] static final int[][] samplesPerFrame = { // Version 2.5 { 0, 576, 1152, 384 }, // Reserved {}, // Version 2 { 0, 576, 1152, 384 }, // Version 1 { 0, 1152, 1152, 384 } }; // Bits per slot for each layer static final int[] bitsPerSlot = { // Reserved 0, // Layer III 8, // Layer II 8, // Layer I 32 }; @Override public void readFilter(InputStream input, OutputStream output, String charset, HashMap<String, String> otherParams, FilterCallback cb) throws DataFilterException, IOException { filter(input, output); } public void filter(InputStream input, OutputStream output) throws DataFilterException, IOException { //FIXME: Add support for free formatted files(highly uncommon) DataInputStream in = new DataInputStream(input); DataOutputStream out = new DataOutputStream(output); boolean foundStream = true; int totalFrames = 0; int totalCRCs = 0; int foundFrames = 0; int maxFoundFrames = 0; long countLostSyncBytes = 0; int countFreeBitrate = 0; try { int frameHeader = in.readInt(); foundStream = (frameHeader & 0xffe00000) == 0xffe00000; // Seek ahead until we find the Frame sync while (true) { if (foundStream && (frameHeader & 0xffe00000) == 0xffe00000) { // Populate header details final byte version = (byte) ((frameHeader & 0x00180000) >>> 19); //2 bits if (version == 1) { foundStream = false; continue; // Not valid } final byte layer = (byte) ((frameHeader & 0x00060000) >>> 17); //2 bits if (layer == 0) { foundStream = false; continue; // Not valid } // WARNING: layer is encoded! 1 = layer 3, 2 = layer 2, 3 = layer 1! final boolean hasCRC = ((frameHeader & 0x00010000) >>> 16) != 1; //1 bit, but inverted final byte bitrateIndex = (byte) ((frameHeader & 0x0000f000) >>> 12); //4 bits if (bitrateIndex == 0) { // FIXME It looks like it would be very hard to support free bitrate. // Unfortunately, this is used occasionally e.g. on the chaosradio mp3's. foundStream = false; countFreeBitrate++; continue; // Not valid } if (bitrateIndex == 15) { foundStream = false; continue; // Not valid } final byte samplerateIndex = (byte) ((frameHeader & 0x00000c00) >>> 10); //2 bits if (samplerateIndex == 3) { foundStream = false; continue; // Not valid } final boolean paddingBit = ((frameHeader & 0x00000200) >>> 9) == 1; // We skip the following bits here (listed for future reference): // Private 0x00000100 (1 bit) // Channel mode 0x000000c0 (2 bits) // Mode extension 0x00000030 (2 bits) // Copyright 0x00000008 (1 bit) // Original 0x00000004 (1 bit) // FIXME A small boost in security might be gained by clearing the latter two. byte emphasis = (byte) ((frameHeader & 0x00000003)); if (emphasis == 2) { foundStream = false; continue; // Not valid } // Generate other values from tables final int bitrate = bitRateIndices[version][layer][bitrateIndex] * 1000; final int samplerate = sampleRateIndices[version][samplerateIndex]; final int samples = samplesPerFrame[version][layer]; final int granularity = bitsPerSlot[layer]; int frameLength = samples / granularity * bitrate / samplerate; frameLength += paddingBit ? 1 : 0; frameLength *= granularity / 8; short crc = 0; if (hasCRC) { totalCRCs++; crc = in.readShort(); Logger.normal(this, "Found a CRC"); // FIXME calculate the CRC. It applies to a large number of frames, dependant on the format. } // Write out the frame byte[] frame = null; frame = new byte[frameLength-4]; in.readFully(frame); out.writeInt(frameHeader); // FIXME CRCs may or may not work. I have not been able to find an mp3 file with CRCs but without free bitrate. if (hasCRC) out.writeShort(crc); out.write(frame); totalFrames++; foundFrames++; if (countLostSyncBytes != 0) Logger.normal(this, "Lost sync for "+countLostSyncBytes+" bytes"); countLostSyncBytes = 0; frameHeader = in.readInt(); } else if (!foundStream && (frameHeader & 0xffffff00) == 0x49443300) { // This is an ID3v2 header, see http://id3.org/id3v2.3.0#ID3v2_header // Skip minor version, flags in.skip(2); // ID3 tag size byte[] encodedSize = new byte[4]; in.readFully(encodedSize); int size = 0; size |= (encodedSize[0] & 0x7F) << 21; size |= (encodedSize[1] & 0x7F) << 14; size |= (encodedSize[2] & 0x7F) << 7; size |= (encodedSize[3] & 0x7F); in.skip(size); Logger.normal(this, "Skipped " + size + " bytes of ID3v2 data"); frameHeader = in.readInt(); foundStream = (frameHeader & 0xffe00000) == 0xffe00000; } else if (!foundStream && (frameHeader & 0xffffff00) == 0x54414700) { // This is an ID3v1 header // ID3v1 is of fixed length (128 bytes), from which we have already read the first 4 in.skip(124); Logger.normal(this, "Skipped an ID3v1 TAG"); frameHeader = in.readInt(); foundStream = (frameHeader & 0xffe00000) == 0xffe00000; } else { if(foundFrames != 0) Logger.normal(this, "Series of frames: "+foundFrames); if(foundFrames > maxFoundFrames) maxFoundFrames = foundFrames; foundFrames = 0; frameHeader = frameHeader << 8; frameHeader |= (in.readUnsignedByte()); if((frameHeader & 0xffe00000) == 0xffe00000) { foundStream = true; } else { countLostSyncBytes++; } } } } catch (EOFException e) { if(foundFrames != 0) Logger.normal(this, "Series of frames: "+foundFrames); if(countLostSyncBytes != 0) Logger.normal(this, "Lost sync for "+countLostSyncBytes+" bytes"); if(totalFrames == 0 || maxFoundFrames < 10) { if(countFreeBitrate > 100) throw new DataFilterException(l10n("freeBitrateNotSupported"), l10n("freeBitrateNotSupported"), l10n("freeBitrateNotSupportedExplanation")); if(totalFrames == 0) throw new DataFilterException(l10n("bogusMP3NoFrames"), l10n("bogusMP3NoFrames"), l10n("bogusMP3NoFramesExplanation")); } out.flush(); Logger.normal(this, totalFrames+" frames, of which "+totalCRCs+" had a CRC"); return; } } private String l10n(String key) { return NodeL10n.getBase().getString("MP3Filter."+key); } @Override public void writeFilter(InputStream input, OutputStream output, String charset, HashMap<String, String> otherParams, FilterCallback cb) throws DataFilterException, IOException { filter(input, output); } public static void main(String[] args) throws DataFilterException, IOException { File f = new File(args[0]); FileInputStream fis = new FileInputStream(f); File out = new File(args[0]+".filtered.mp3"); FileOutputStream fos = new FileOutputStream(out); MP3Filter filter = new MP3Filter(); // // Skip some bytes for testing resyncing. // byte[] buf = new byte[4096]; // fis.read(buf); // fis.read(buf); // fis.read(buf); // fis.read(buf); filter.readFilter(fis, fos, null, null, null); fis.close(); fos.close(); } }