/* * myLib - https://github.com/taktod/myLib * Copyright (c) 2014 ttProject. All rights reserved. * * Licensed under The MIT license. */ package com.ttProject.container.flv.type; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; import com.ttProject.container.flv.FlvCodecType; import com.ttProject.container.flv.FlvTag; import com.ttProject.frame.AudioAnalyzer; import com.ttProject.frame.AudioFrame; import com.ttProject.frame.AudioSelector; import com.ttProject.frame.IAudioFrame; import com.ttProject.frame.aac.AacDsiFrameAnalyzer; import com.ttProject.frame.aac.AacFrame; import com.ttProject.frame.extra.AudioMultiFrame; import com.ttProject.nio.channels.ByteReadChannel; import com.ttProject.nio.channels.IReadChannel; import com.ttProject.unit.extra.BitConnector; import com.ttProject.unit.extra.BitLoader; import com.ttProject.unit.extra.bit.Bit1; import com.ttProject.unit.extra.bit.Bit2; import com.ttProject.unit.extra.bit.Bit4; import com.ttProject.unit.extra.bit.Bit8; import com.ttProject.util.BufferUtil; /** * audiotag * @author taktod * nellymoser 16 or 8, sampleRate 0 bitCount 1 channels 0 * speex 16Khz , sampleRate 1 bitCount 1 channels 0 */ public class AudioTag extends FlvTag { /** logger */ private Logger logger = Logger.getLogger(AudioTag.class); private Bit4 codecId = new Bit4(); private Bit2 sampleRate = new Bit2(); private Bit1 bitCount = new Bit1(); private Bit1 channels = new Bit1(); private Bit8 sequenceHeaderFlag = null; private ByteBuffer frameBuffer = null; private IAudioFrame frame = null; private AudioAnalyzer frameAnalyzer = null; private boolean frameAppendFlag = false; // フレームが追加されたことを検知するフラグ /** * constructor * @param tagType */ public AudioTag(Bit8 tagType) { super(tagType); } /** * constructor */ public AudioTag() { this(new Bit8(0x08)); } /** * hold the analyzer object. * @param analyzer */ public void setFrameAnalyzer(AudioAnalyzer analyzer) { this.frameAnalyzer = analyzer; } /** * ref sampleRate. * @return * @throws Exception */ public int getSampleRate() throws Exception { if(frame == null) { switch(getCodec()) { case NELLY_16: case SPEEX: return 16000; case NELLY_8: case MP3_8: case G711_A: case G711_U: return 8000; default: switch(sampleRate.get()) { case 0: return 5512; case 1: return 11025; case 2: return 22050; case 3: return 44100; default: throw new Exception("unexpected sampleRate val:" + sampleRate.get()); } } } return frame.getSampleRate(); } /** * ref sampleNum(depends on frame.) * @return * @throws Exception */ public int getSampleNum() throws Exception { if(frame == null) { analyzeFrame(); // analyze0 } return frame.getSampleNum(); } /** * ref channels * @return */ public int getChannels() { if(frame == null) { if(channels.get() == 1) { return 2; } else { return 1; } } return frame.getChannel(); } /** * bitCount * @return */ public int getBitCount() { if(frame == null) { if(bitCount.get() == 1) { return 16; } else { return 8; } } return frame.getBit(); } /** * {@inheritDoc} */ @Override public void load(IReadChannel channel) throws Exception { if(codecId != null) { switch(getCodec()) { case AAC: channel.position(getPosition() + 13); frameBuffer = BufferUtil.safeRead(channel, getSize() - 13 - 4); if(sequenceHeaderFlag.get() == 0) { if(frameAnalyzer == null || !(frameAnalyzer instanceof AacDsiFrameAnalyzer)) { throw new Exception("frameAnalyzer is not suitable for aac."); } frameAnalyzer.setPrivateData(new ByteReadChannel(frameBuffer)); } break; default: if(getSize() - 12 - 4 > 0) { channel.position(getPosition() + 12); frameBuffer = BufferUtil.safeRead(channel, getSize() - 12 - 4); } else { channel.position(getPosition() + 11); } break; } } // check prevTagSize if(getPrevTagSize() != BufferUtil.safeRead(channel, 4).getInt()) { throw new Exception("the size of end tag is corrupted."); } } /** * ref Codec * @return */ public FlvCodecType getCodec() { return FlvCodecType.getAudioCodecType(codecId.get()); } /** * {@inheritDoc} */ @Override public void minimumLoad(IReadChannel channel) throws Exception { super.minimumLoad(channel); if(getSize() == 15) { logger.warn("empty data is captured."); return; } BitLoader loader = new BitLoader(channel); loader.load(codecId, sampleRate, bitCount, channels); if(getCodec() == FlvCodecType.AAC) { sequenceHeaderFlag = new Bit8(); loader.load(sequenceHeaderFlag); } } /** * {@inheritDoc} */ @Override protected void requestUpdate() throws Exception { if(frameBuffer == null && frame == null) { throw new Exception("data body is undefined."); } ByteBuffer frameBuffer = null; if(frameAppendFlag) { // TODO this frame analyze is other task, so it could be other func. // codecId, sampleRate, bitCount, channels will be analyzed from frame. IAudioFrame codecCheckFrame = frame; if(frame instanceof AudioMultiFrame) { // for codec check, need to ref first frame. codecCheckFrame = ((AudioMultiFrame) frame).getFrameList().get(0); } sampleRate = null; bitCount = null; channels = null; int sizeEx = 0; switch(codecCheckFrame.getCodecType()) { case AAC: codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.AAC)); sequenceHeaderFlag = new Bit8(1); sizeEx = 1; break; case MP3: if(frame.getSampleRate() == 8000) { // no example for mp3_8, I need a sample. codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.MP3_8)); sampleRate = new Bit2(); } else { codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.MP3)); } break; case NELLYMOSER: if(frame.getSampleRate() == 16000) { // nelly16 0x42 codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.NELLY_16)); sampleRate = new Bit2(); } else if(frame.getSampleRate() == 8000) { // nelly8 0x52 codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.NELLY_8)); sampleRate = new Bit2(); } else { codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.NELLY)); } break; case SPEEX: // 0xB6 codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.SPEEX)); if(frame.getSampleRate() != 16000) { throw new Exception("sampleRate of speex is 16kHz only."); } if(frame.getChannel() != 1) { throw new Exception("channel of speex is monoral only."); } sampleRate = new Bit2(1); break; case ADPCM_SWF: codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.ADPCM)); break; case PCM_ALAW: codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.G711_A)); sampleRate = new Bit2(0); break; case PCM_MULAW: codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.G711_U)); sampleRate = new Bit2(0); break; default: throw new Exception("frame type is invalid for flv.:" + frame); } // channels if(channels == null) { channels = new Bit1(); switch(frame.getChannel()) { case 1: channels.set(0); break; case 2: channels.set(1); break; default: throw new Exception("audio channel is not suitable for flv.:" + frame.getChannel()); } } // bitCount if(bitCount == null) { bitCount = new Bit1(); switch(frame.getBit()) { case 8: bitCount.set(0); break; case 16: bitCount.set(1); break; default: // some frame doesn't have bit depth information. bitCount.set(1); } } // sampleRate if(sampleRate == null) { sampleRate = new Bit2(); switch((int)(frame.getSampleRate() / 100)) { case 55: sampleRate.set(0); break; case 110: sampleRate.set(1); break; case 220: sampleRate.set(2); break; case 441: sampleRate.set(3); break; default: throw new Exception("sampleRate is not suitable for flv."); } } frameAppendFlag = false; // reorganize data, do on getFrameBuffer // need to check size. frameBuffer = getFrameBuffer(); setPts((long)(1.0D * frame.getPts() / frame.getTimebase() * 1000)); setTimebase(1000); setSize(11 + 1 + sizeEx + frameBuffer.remaining() + 4); } else { frameBuffer = getFrameBuffer(); } BitConnector connector = new BitConnector(); ByteBuffer startBuffer = getStartBuffer(); ByteBuffer audioInfoBuffer = connector.connect( codecId, sampleRate, bitCount, channels, sequenceHeaderFlag /* extra data for aac */ ); ByteBuffer tailBuffer = getTailBuffer(); setData(BufferUtil.connect( startBuffer, audioInfoBuffer, frameBuffer, tailBuffer )); } /** * ref frameBuffer * @return */ private ByteBuffer getFrameBuffer() throws Exception { if(frameBuffer == null) { // make frame buffer from frame. if(frame != null) { // TODO for nellymoser this frame can be audioMultiFrame, in this case need to connect. if(frame instanceof AudioMultiFrame) { List<ByteBuffer> buffers = new ArrayList<ByteBuffer>(); for(IAudioFrame aFrame : ((AudioMultiFrame) frame).getFrameList()) { buffers.add(aFrame.getData()); } frameBuffer = BufferUtil.connect(buffers); } else if(frame instanceof AacFrame) { // for aac, get the buffer only. header is from msh. AacFrame aacFrame = (AacFrame)frame; frameBuffer = aacFrame.getBuffer(); } else { frameBuffer = frame.getData(); } } } if(frameBuffer == null) { return null; } else { return frameBuffer.duplicate(); } } /** * analyzeFrame * @throws Exception */ private void analyzeFrame() throws Exception { if(frameBuffer == null) { throw new Exception("target frame buffer is not loaded yet."); } if(getCodec() == FlvCodecType.AAC && sequenceHeaderFlag.get() != 1) { // aacのmshも処理しません。 return; } if(frameAnalyzer == null) { throw new Exception("frameAnalyzer is unknown."); } IReadChannel channel = new ByteReadChannel(frameBuffer); AudioSelector selector = frameAnalyzer.getSelector(); selector.setBit(getBitCount()); selector.setChannel(getChannels()); // selector.setSampleNum(getSampleNum()); // getSampleNum cause infinite loop. selector.setSampleRate(getSampleRate()); double pts = getPts(); do { AudioFrame audioFrame = (AudioFrame)frameAnalyzer.analyze(channel); audioFrame.setPts((long)pts); audioFrame.setTimebase(getTimebase()); // need to get more collect data from sampleNum. pts += 1.0D * audioFrame.getSampleNum() * getTimebase() / audioFrame.getSampleRate(); if(frame != null) { if(!(frame instanceof AudioMultiFrame)) { AudioMultiFrame multiFrame = new AudioMultiFrame(); multiFrame.addFrame(frame); frame = multiFrame; } ((AudioMultiFrame)frame).addFrame((IAudioFrame)audioFrame); } else { frame = (IAudioFrame)audioFrame; } } while(channel.size() != channel.position()); } /** * ref frame. * @return * @throws Exception */ public IAudioFrame getFrame() throws Exception { if(frame == null) { analyzeFrame(); } return frame; } /** * add frame. * @param frame */ public void addFrame(IAudioFrame tmpFrame) throws Exception { if(tmpFrame == null) { // addedFrame is null, nothing. return; } if(!(tmpFrame instanceof IAudioFrame)) { throw new Exception("try to add non-audioFrame for audioTag."); } frameAppendFlag = true; if(frame == null) { frame = tmpFrame; } else if(frame instanceof AudioMultiFrame) { ((AudioMultiFrame)frame).addFrame(tmpFrame); } else { AudioMultiFrame multiFrame = new AudioMultiFrame(); multiFrame.addFrame(frame); if(tmpFrame instanceof AudioMultiFrame) { for(IAudioFrame aFrame : ((AudioMultiFrame) tmpFrame).getFrameList()) { multiFrame.addFrame(aFrame); } } else { multiFrame.addFrame(tmpFrame); } frame = multiFrame; } super.update(); } /** * check msh. * @return */ public boolean isSequenceHeader() { return getCodec() == FlvCodecType.AAC && sequenceHeaderFlag.get() == 0; } /** * initialize as aac msh. * @param dsi */ public void setAacMediaSequenceHeader(AacFrame frame, ByteBuffer data) throws Exception { codecId.set(FlvCodecType.getAudioCodecNum(FlvCodecType.AAC)); switch(frame.getChannel()) { case 1: channels.set(0); break; case 2: channels.set(1); break; default: throw new Exception("channel is not suitable for audioTag.:" + frame.getChannel()); } switch(frame.getBit()) { case 8: bitCount.set(0); break; case 16: default: bitCount.set(1); break; } switch((int)(frame.getSampleRate() / 100)) { case 55: sampleRate.set(0); break; case 110: sampleRate.set(1); break; case 220: sampleRate.set(2); break; case 441: sampleRate.set(3); break; default: throw new Exception("frameRate is not suitable for flv.:" + frame.getSampleRate()); } sequenceHeaderFlag = new Bit8(0); frameBuffer = data; // calcurate pts. setPts((long)(1.0D * frame.getPts() / frame.getTimebase() * 1000)); setTimebase(1000); setSize(11 + 1 + 1 + frameBuffer.remaining() + 4); super.update(); } @Override public void setPts(long pts) { // update frame pts before container pts. if(frame != null && frame instanceof AudioFrame) { AudioFrame aFrame = (AudioFrame)frame; aFrame.setPts(pts * aFrame.getTimebase() / 1000); } super.setPts(pts); } /** * {@inheritDoc} */ @Override public String toString() { StringBuilder data = new StringBuilder(); data.append("AudioTag:"); data.append(" timestamp:").append(getPts()); data.append(" codec:").append(getCodec()); try { int sampleRate = getSampleRate(); data.append(" sampleRate:").append(sampleRate); int sampleNum = getSampleNum(); data.append(" sampleNum:").append(sampleNum); } catch(Exception e) { } return data.toString(); } }