/* * 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.mp4; import com.tulskiy.musique.playlist.Track; import com.tulskiy.musique.util.AudioMath; import net.sourceforge.jaad.aac.Decoder; import net.sourceforge.jaad.aac.SampleBuffer; import net.sourceforge.jaad.mp4.MP4Container; import net.sourceforge.jaad.mp4.api.AudioTrack; import net.sourceforge.jaad.mp4.api.Frame; import net.sourceforge.jaad.mp4.api.MetaData; import net.sourceforge.jaad.mp4.api.Movie; import javax.sound.sampled.AudioFormat; import java.io.IOException; import java.io.RandomAccessFile; import java.util.List; import java.util.logging.Level; /** * Author: Denis Tulskiy * Date: 4/5/11 */ public class MP4Decoder implements com.tulskiy.musique.audio.Decoder { private Decoder decoder; private SampleBuffer sampleBuffer; private AudioFormat audioFormat; private int currentSample; private int totalSamples; private int gaplessDelay; private int gaplessPadding; private int offset; private int bps = 2; private Frame frame; private RandomAccessFile in; private AudioTrack track; @Override public boolean open(Track track) { try { in = new RandomAccessFile(track.getTrackData().getFile(), "r"); sampleBuffer = new SampleBuffer(); sampleBuffer.setBigEndian(false); initDecoder(0); return true; } catch (IOException e) { logger.log(Level.WARNING, "Error opening file " + track.getTrackData().getFile().getAbsolutePath(), e); } return false; } private void initDecoder(long sample) throws IOException { in.seek(0); MP4Container cont = new MP4Container(in); Movie movie = cont.getMovie(); List<net.sourceforge.jaad.mp4.api.Track> tracks = movie.getTracks(AudioTrack.AudioCodec.AAC); if (tracks == null || tracks.isEmpty()) { throw new IOException("Could not find AAC track"); } track = (AudioTrack) tracks.get(0); decoder = new Decoder(track.getDecoderSpecificInfo()); parseGaplessInfo(movie); sample += gaplessDelay; totalSamples = track.getFrameCount(); bps = track.getSampleSize() / 8; int target = 0; currentSample = -1; for (int i = 0; i < totalSamples; i++) { target += track.getSampleDuration(i); if (sample < target) { currentSample = i; break; } } if (currentSample == -1) currentSample = totalSamples; int preheat = 2; int s = currentSample - preheat; if (s < 0) s = 0; track.setCurrentFrame(s); for (int i = s; i < currentSample; i++) { frame = track.readNextFrame(); decoder.decodeFrame(frame.getData(), sampleBuffer); } offset = (int) (sample - (target - track.getSampleDuration(currentSample))); if (audioFormat == null) audioFormat = new AudioFormat((float) track.getSampleRate(), bps * 8, track.getChannelCount(), true, false); } private void parseGaplessInfo(Movie movie) { gaplessPadding = track.getLastFramePadding(); MetaData metaData = movie.getMetaData(); String iTunSMPB = metaData.get(MetaData.Field.GAPLESS_PLAYBACK); if (iTunSMPB != null && iTunSMPB.length() > 0) { String[] data = iTunSMPB.trim().split(" "); gaplessDelay = Integer.parseInt(data[1], 16); gaplessPadding = (int) (track.getSampleDuration(0) - Integer.parseInt(data[2], 16)); } else { //now estimate gapless delay based on the tool String tool = metaData.get(MetaData.Field.ENCODER_TOOL); if (tool != null && !tool.isEmpty()) { if (tool.startsWith("Nero")) { gaplessDelay = (int) (track.getSampleDuration(0) * 2 + 576); } else if (tool.startsWith("FAAC")) { gaplessDelay = (int) track.getSampleDuration(0); } } } } @Override public AudioFormat getAudioFormat() { return audioFormat; } @Override public void seekSample(long sample) { try { initDecoder(sample); } catch (IOException e) { logger.log(Level.WARNING, "Error while trying to see to sample " + sample, e); } } @Override public int decode(byte[] buf) { try { frame = track.readNextFrame(); currentSample++; if (frame == null) { return -1; } decoder.decodeFrame(frame.getData(), sampleBuffer); int len; if (currentSample == totalSamples && gaplessPadding != 0) { len = gaplessPadding * sampleBuffer.getChannels(); } else { len = (int) (sampleBuffer.getLength() * sampleBuffer.getSampleRate()) * bps; } len *= bps; int off = offset * bps * sampleBuffer.getChannels(); System.arraycopy(sampleBuffer.getData(), off, buf, 0, len - off); offset = 0; return len - off; } catch (IOException e) { logger.log(Level.WARNING, "Error decoding mp4 file", e); } return -1; } @Override public void close() { try { in.close(); in = null; decoder = null; } catch (IOException e) { logger.log(Level.WARNING, "Error closing file input stream", e); } } }