/* * Copyright (c) 2008, 2009, 2010, 2011 Denis Tulskiy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * version 3 along with this work. If not, see <http://www.gnu.org/licenses/>. */ package com.tulskiy.musique.audio.formats.flac; import com.tulskiy.musique.audio.Decoder; import com.tulskiy.musique.playlist.Track; import org.kc7bfi.jflac.frame.Frame; import org.kc7bfi.jflac.io.RandomFileInputStream; import org.kc7bfi.jflac.metadata.Metadata; import org.kc7bfi.jflac.metadata.SeekTable; import org.kc7bfi.jflac.metadata.StreamInfo; import org.kc7bfi.jflac.util.ByteData; import javax.sound.sampled.AudioFormat; import java.io.IOException; import java.io.RandomAccessFile; /** * @Author: Denis Tulskiy * @Date: 12.06.2009 */ public class FLACDecoder implements Decoder { private RandomAccessFile inputFile; private StreamInfo streamInfo; private SeekTable seekTable; private org.kc7bfi.jflac.FLACDecoder decoder; private ByteData byteData = new ByteData(0); private int offset = -1; public synchronized boolean open(Track track) { try { logger.fine("Opening file: " + track.getTrackData().getFile()); inputFile = new RandomAccessFile(track.getTrackData().getFile(), "r"); // ogg = iFile.getAudioHeader().getCodec().equals("Ogg FLAC"); // if (ogg) { // oggDecoder = new OggFlacDecoder(); // oggDecoder.open(inputFile); // streamInfo = oggDecoder.getStreamInfo(); // decoder = oggDecoder.getDecoder(); // } else { decoder = new org.kc7bfi.jflac.FLACDecoder(new RandomFileInputStream(inputFile)); parseMetadata(); // } return true; } catch (IOException e) { e.printStackTrace(); } return false; } public AudioFormat getAudioFormat() { return streamInfo.getAudioFormat(); } private void parseMetadata() { streamInfo = null; try { Metadata[] metadata = decoder.readMetadata(); for (Metadata m : metadata) { if (m instanceof StreamInfo) streamInfo = (StreamInfo) m; else if (m instanceof SeekTable) seekTable = (SeekTable) m; } } catch (IOException e) { e.printStackTrace(); } } public void seekSample(long sample) { decoder.flush(); // if (ogg) { // seekOgg(sample); // } else { seekFlac(sample); // } decoder.flush(); } public int decode(byte[] buf) { try { if (offset != -1) { int len = byteData.getLen() - offset; System.arraycopy(byteData.getData(), offset, buf, 0, len); offset = -1; return len; } Frame readFrame = decoder.readNextFrame(); if (readFrame == null) { return -1; } byteData.setData(buf); decoder.decodeFrame(readFrame, byteData); return byteData.getLen(); } catch (IOException e) { e.printStackTrace(); } return -1; } public void close() { try { if (inputFile != null) inputFile.close(); } catch (IOException e) { e.printStackTrace(); } } private void seekOgg(long target_sample) { // // long left_pos = 0; // long right_pos = 0; // try { // right_pos = inputFile.length(); // } catch (IOException e) { // e.printStackTrace(); // } // long left_sample = 0, right_sample = streamInfo.getTotalSamples(); // long this_frame_sample = 0; // long pos = 0; // boolean did_a_seek; // int iteration = 0; // // /* In the first iterations, we will calculate the target byte position // * by the distance from the target sample to left_sample and // * right_sample (let's call it "proportional search"). After that, we // * will switch to binary search. // */ // int BINARY_SEARCH_AFTER_ITERATION = 2; // // /* We will switch to a linear search once our current sample is less // * than this number of samples ahead of the target sample // */ // long LINEAR_SEARCH_WITHIN_SAMPLES = streamInfo.getMaxBlockSize() * 2; // // /* If the total number of samples is unknown, use a large value, and // * force binary search immediately. // */ // if (right_sample == 0) { // right_sample = Long.MAX_VALUE; // BINARY_SEARCH_AFTER_ITERATION = 0; // } // // for (; ; iteration++) { // if (iteration == 0 || this_frame_sample > target_sample || target_sample - this_frame_sample > LINEAR_SEARCH_WITHIN_SAMPLES) { // if (iteration >= BINARY_SEARCH_AFTER_ITERATION) { // pos = (right_pos + left_pos) / 2; // } else { // pos = (long) ((double) (target_sample - left_sample) / (double) (right_sample - left_sample) * (double) (right_pos - left_pos)); // // /* @@@ // * before EOF, to make sure we land before the last frame, // * thereby getting a this_frame_sample and so having a better // * estimate. @@@@@@DELETE:this would also mostly (or totally if we could // * be sure to land before the last frame) avoid the // * end-of-stream case we have to check later. // */ // } // // /* physical seek */ // oggDecoder.seekHelper(pos); // oggDecoder.flush(); // oggDecoder.getNextPage(right_pos - pos); // did_a_seek = true; // } else // did_a_seek = false; // // decoder.getBitInputStream().reset(); // Frame frame; // try { // frame = decoder.readNextFrame(); // } catch (IOException e) { // e.printStackTrace(); // return; // } // if (frame == null) { // if (did_a_seek) { // /* this can happen if we seek to a point after the last frame; we drop // * to binary search right away in this case to avoid any wasted // * iterations of proportional search. // */ // right_pos = pos; // BINARY_SEARCH_AFTER_ITERATION = 0; // } else { // /* this can probably only happen if total_samples is unknown and the // * target_sample is past the end of the stream // */ // return; // } // } else if (frame.header.sampleNumber <= target_sample && // target_sample <= frame.header.sampleNumber + frame.header.blockSize) { //// System.out.println("Done seeking"); // int offset = (int) (target_sample - frame.header.sampleNumber) * frame.header.channels * frame.header.bitsPerSample / 8; // ByteData bd = decoder.decodeFrame(frame, null); // outputStream.write(bd.getData(), offset, bd.getLen() - offset); // break; // // } else { // this_frame_sample = frame.header.sampleNumber; // // if (did_a_seek) { // if (this_frame_sample <= target_sample) { // /* The 'equal' case should not happen, since // * FLAC__stream_decoder_process_single() // * should recognize that it has hit the // * target sample and we would exit through // * the 'break' above. // */ // left_sample = this_frame_sample; // /* sanity check to avoid infinite loop */ // if (left_pos == pos) { // return; // } // left_pos = pos; // } else if (this_frame_sample > target_sample) { // right_sample = this_frame_sample; // /* sanity check to avoid infinite loop */ // if (right_pos == pos) { // return; // } // right_pos = pos; // } // } // } // } } private void seekFlac(long target_sample) { long lower_bound, upper_bound = 0, lower_bound_sample, upper_bound_sample, this_frame_sample; long pos; int i; int approx_bytes_per_frame; boolean first_seek = true; long total_samples = streamInfo.getTotalSamples(); int min_blocksize = streamInfo.getMinBlockSize(); int max_blocksize = streamInfo.getMaxBlockSize(); int max_framesize = streamInfo.getMaxFrameSize(); int min_framesize = streamInfo.getMinFrameSize(); int channels = streamInfo.getChannels(); int bps = streamInfo.getBitsPerSample(); /* we are just guessing here */ if (max_framesize > 0) approx_bytes_per_frame = (max_framesize + min_framesize) / 2 + 1; else if (min_blocksize == max_blocksize && min_blocksize > 0) { approx_bytes_per_frame = min_blocksize * channels * bps / 8 + 64; } else approx_bytes_per_frame = 4096 * channels * bps / 8 + 64; lower_bound = 0; lower_bound_sample = 0; try { upper_bound = inputFile.length(); } catch (IOException e) { e.printStackTrace(); } upper_bound_sample = total_samples > 0 ? total_samples : target_sample /*estimate it*/; if (seekTable != null) { long new_lower_bound = lower_bound; long new_upper_bound = upper_bound; long new_lower_bound_sample = lower_bound_sample; long new_upper_bound_sample = upper_bound_sample; /* find the closest seekPosition point <= target_sample, if it exists */ for (i = seekTable.numberOfPoints() - 1; i >= 0; i--) { if (seekTable.getSeekPoint(i).getFrameSamples() > 0 && /* defense against bad seekpoints */ (total_samples <= 0 || seekTable.getSeekPoint(i).getSampleNumber() < total_samples) && /* defense against bad seekpoints */ seekTable.getSeekPoint(i).getSampleNumber() <= target_sample) break; } if (i >= 0) { /* i.e. we found a suitable seekPosition point... */ new_lower_bound = seekTable.getSeekPoint(i).getStreamOffset(); new_lower_bound_sample = seekTable.getSeekPoint(i).getSampleNumber(); } /* find the closest seekPosition point > target_sample, if it exists */ for (i = 0; i < seekTable.numberOfPoints(); i++) { if (seekTable.getSeekPoint(i).getFrameSamples() > 0 && /* defense against bad seekpoints */ (total_samples <= 0 || seekTable.getSeekPoint(i).getSampleNumber() < total_samples) && /* defense against bad seekpoints */ seekTable.getSeekPoint(i).getSampleNumber() > target_sample) break; } if (i < seekTable.numberOfPoints()) { /* i.e. we found a suitable seekPosition point... */ new_upper_bound = seekTable.getSeekPoint(i).getStreamOffset(); new_upper_bound_sample = seekTable.getSeekPoint(i).getSampleNumber(); } /* final protection against unsorted seekPosition tables; keep original values if bogus */ if (new_upper_bound >= new_lower_bound) { lower_bound = new_lower_bound; upper_bound = new_upper_bound; lower_bound_sample = new_lower_bound_sample; upper_bound_sample = new_upper_bound_sample; } } if (upper_bound_sample == lower_bound_sample) upper_bound_sample++; while (true) { try { /* check if the bounds are still ok */ if (lower_bound_sample >= upper_bound_sample || lower_bound > upper_bound) { return; } pos = (long) (lower_bound + ((double) (target_sample - lower_bound_sample) / (double) (upper_bound_sample - lower_bound_sample) * (double) (upper_bound - lower_bound)) - approx_bytes_per_frame); if (pos >= upper_bound) pos = upper_bound - 1; if (pos < lower_bound) pos = lower_bound; // System.out.println("Seek to: " + pos); inputFile.seek(pos); // decoder.getBitInputStream().skipBitsNoCRC(1); decoder.getBitInputStream().reset(); Frame frame = decoder.readNextFrame(); // System.out.println("Found: " + frame.header.sampleNumber); if (frame.header.sampleNumber <= target_sample && target_sample <= frame.header.sampleNumber + frame.header.blockSize) { // System.out.println("Done seeking"); offset = (int) (target_sample - frame.header.sampleNumber) * frame.header.channels * frame.header.bitsPerSample / 8; byteData = decoder.decodeFrame(frame, byteData); break; } /* our write callback will change the state when it gets to the target frame */ /* actually, we could have got_a_frame if our decoder is at FLAC__STREAM_DECODER_END_OF_STREAM so we need to check for that also */ this_frame_sample = frame.header.sampleNumber; if (decoder.getSamplesDecoded() == 0 || (this_frame_sample + frame.header.blockSize >= upper_bound_sample && !first_seek)) { if (pos == lower_bound) { /* can't move back any more than the first frame, something is fatally wrong */ System.err.printf("FLAC Decoder: Seek to %d error. %d samples overrun, sorry\n", target_sample, this_frame_sample - target_sample); return; } /* our last move backwards wasn't big enough, try again */ approx_bytes_per_frame = approx_bytes_per_frame != 0 ? approx_bytes_per_frame * 2 : 16; continue; } /* allow one seekPosition over upper bound, so we can get a correct upper_bound_sample for streams with unknown total_samples */ first_seek = false; /* make sure we are not seeking in corrupted stream */ if (this_frame_sample < lower_bound_sample) { System.err.println("FLAC Decoder: Seek error. This frame sample is lower than lower bound sample"); return; } /* we need to narrow the search */ if (target_sample < this_frame_sample) { upper_bound_sample = this_frame_sample + frame.header.blockSize; /*@@@@@@ what will decode position be if at end of stream? */ upper_bound = inputFile.getFilePointer() - decoder.getBitInputStream().getInputBytesUnconsumed(); approx_bytes_per_frame = (int) (2 * (upper_bound - pos) / 3 + 16); } else { /* target_sample >= this_frame_sample + this frame's blocksize */ lower_bound_sample = this_frame_sample + frame.header.blockSize; lower_bound = inputFile.getFilePointer() - decoder.getBitInputStream().getInputBytesUnconsumed(); approx_bytes_per_frame = (int) (2 * (lower_bound - pos) / 3 + 16); } } catch (IOException e) { e.printStackTrace(); } } } }