diff --git a/src/AudioBuffer.java b/src/AudioBuffer.java index 5606d67..15bc880 100644 --- a/src/AudioBuffer.java +++ b/src/AudioBuffer.java @@ -1,11 +1,11 @@ -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import com.beatofthedrum.alacdecoder.AlacDecodeUtils; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; - -import com.beatofthedrum.alacdecoder.AlacDecodeUtils; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** * A ring buffer where every frame is decrypted, decoded and stored @@ -14,255 +14,278 @@ * */ public class AudioBuffer { - // Constants - Should be somewhere else - public static final int BUFFER_FRAMES = 512; // Total buffer size (number of frame) - public static final int START_FILL = 282; // Alac will wait till there are START_FILL frames in buffer - public static final int MAX_PACKET = 2048; // Also in UDPListener (possible to merge it in one place?) - - // The lock for writing/reading concurrency - private final Lock lock = new ReentrantLock(); - + // Constants - Should be somewhere else + public static final int BUFFER_FRAMES = 512; // Total buffer size (number of frame) + public static final int START_FILL = 282; // Alac will wait till there are START_FILL frames in buffer + public static final int MAX_PACKET = 2048; // Also in UDPListener (possible to merge it in one place?) + + // The Lock for the next Frame Method + private final Lock nextFrameLock = new ReentrantLock(); + private final Condition nextFrameIsWaiting = nextFrameLock.newCondition(); + + //the lock for the audiobuffer + private final Lock audioBufferLock = new ReentrantLock(); // The array that represents the buffer - private AudioData[] audioBuffer; - - // Can we read in buffer? - private boolean synced = false; - - //Audio infos (rate, etc...) - AudioSession session; - - // The seqnos at which we read and write - private int readIndex; - private int writeIndex; - private int actualBufSize; // The number of packet in buffer - private boolean decoder_isStopped = false; //The decoder stops 'cause the isn't enough packet. Waits till buffer is ok - - // RSA-AES decryption infos - private SecretKeySpec k; - private Cipher c; - - // Needed for asking missing packets - AudioServer server; - - - /** - * Instantiate the buffer - * @param session audio infos - * @param server whre to ask for resending missing packets - */ - public AudioBuffer(AudioSession session, AudioServer server){ - this.session = session; - this.server = server; - - audioBuffer = new AudioData[BUFFER_FRAMES]; - for (int i = 0; i< BUFFER_FRAMES; i++){ - audioBuffer[i] = new AudioData(); - audioBuffer[i].data = new int[session.OUTFRAME_BYTES()]; // = OUTFRAME_BYTES = 4(frameSize+3) - } - } - - /** - * Sets the packets as not ready. Audio thread will only listen to ready packets. - * No audio more. - */ - public void flush(){ - for (int i = 0; i< BUFFER_FRAMES; i++){ - audioBuffer[i].ready = false; - synced = false; - } - } - - - /** - * Returns the next ready frame. If none, waiting for one - * @return - */ - public int[] getNextFrame(){ - synchronized (lock) { - actualBufSize = writeIndex-readIndex; // Packets in buffer - if(actualBufSize<0){ // If loop - actualBufSize = 65536-readIndex+ writeIndex; - } - - if(actualBufSize<1 || !synced){ // If no packets more or Not synced (flush: pause) - if(synced){ // If it' because there is not enough packets - System.err.println("Underrun!!! Not enough frames in buffer!"); - } - - try { - // We say the decoder is stopped and we wait for signal - System.err.println("Waiting"); - decoder_isStopped = true; - lock.wait(); - decoder_isStopped = false; - System.err.println("re-starting"); - readIndex++; // We read next packet - - // Underrun: stream reset - session.resetFilter(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - return null; - } - - // Overrunning. Restart at a sane distance - if (actualBufSize >= BUFFER_FRAMES) { // overrunning! uh-oh. restart at a sane distance - System.err.println("Overrun!!! Too much frames in buffer!"); - readIndex = writeIndex - START_FILL; - } - // we get the value before the unlock ;-) - int read = readIndex; - readIndex++; - - // If loop - actualBufSize = writeIndex-readIndex; - if(actualBufSize<0){ - actualBufSize = 65536-readIndex+ writeIndex; - } - - session.updateFilter(actualBufSize); - - AudioData buf = audioBuffer[read % BUFFER_FRAMES]; - - if(!buf.ready){ - System.err.println("Missing Frame!"); - // Set to zero then - for(int i=0; i writeIndex){ // Too early, did we miss some packet between writeIndex and seqno? - server.request_resend(writeIndex, seqno); - outputSize = this.alac_decode(data, audioBuffer[(seqno % BUFFER_FRAMES)].data); - audioBuffer[(seqno % BUFFER_FRAMES)].ready = true; - writeIndex = seqno + 1; - } else if(seqno > readIndex){ // readIndex < seqno < writeIndex not yet played but too late. Still ok - outputSize = this.alac_decode(data, audioBuffer[(seqno % BUFFER_FRAMES)].data); - audioBuffer[(seqno % BUFFER_FRAMES)].ready = true; - } else { - System.err.println("Late packet with seq. numb.: " + seqno); // Really to late - } - - // The number of packet in buffer - actualBufSize = writeIndex - readIndex; - if(actualBufSize<0){ - actualBufSize = 65536-readIndex+ writeIndex; - } - - if(decoder_isStopped && actualBufSize > START_FILL){ - lock.notify(); - } - - // SEQNO is stored in a short an come back to 0 when equal to 65536 (2 bytes) - if(writeIndex == 65536){ - writeIndex = 0; - } - } - } - - - /** - * Decrypt and decode the packet. - * @param data - * @param outbuffer the result - * @return - */ - private int alac_decode(byte[] data, int[] outbuffer){ - byte[] packet = new byte[MAX_PACKET]; - - // Init AES - initAES(); - - int i; - for (i=0; i+16<=data.length; i += 16){ - // Decrypt - this.decryptAES(data, i, 16, packet, i); - } - - // The rest of the packet is unencrypted - for (int k = 0; k<(data.length % 16); k++){ - packet[i+k] = data[i+k]; - } - - int outputsize = 0; - outputsize = AlacDecodeUtils.decode_frame(session.getAlac(), packet, outbuffer, outputsize); - - assert outputsize==session.getFrameSize()*4; // FRAME_BYTES length - - return outputsize; - } - - - /** - * Initiate the cipher - */ - private void initAES(){ - // Init AES encryption - try { - k = new SecretKeySpec(session.getAESKEY(), "AES"); - c = Cipher.getInstance("AES/CBC/NoPadding"); - c.init(Cipher.DECRYPT_MODE, k, new IvParameterSpec(session.getAESIV())); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Decrypt array from input offset with a length of inputlen and puts it in output at outputoffsest - * @param array - * @param inputOffset - * @param inputLen - * @param output - * @param outputOffset - * @return - */ - private int decryptAES(byte[] array, int inputOffset, int inputLen, byte[] output, int outputOffset){ - try{ - return c.update(array, inputOffset, inputLen, output, outputOffset); - }catch(Exception e){ - e.printStackTrace(); - } - - return -1; - } - - - + private final AudioData[] audioBuffer; + + // Can we read in buffer? + private final boolean synced = false; + + //Audio infos (rate, etc...) + private final AudioSession session; + + // The seqnos at which we read and write + //used to track overrunning the buffer + //index of the buffer + private int readIndex = 0; + //seqno + private int readSeqno = -1; + private int writeIndex = 0; + //seqno + private int writSeqno = -1; + //The decoder stops 'cause the isn't enough packet. Waits till buffer is ok + private boolean decoder_isStopped = false; + + // RSA-AES decryption infos + private SecretKeySpec k; + private Cipher c; + + // Needed for asking missing packets + final AudioServer server; + + + /** + * Instantiate the buffer + * @param session audio infos + * @param server whre to ask for resending missing packets + */ + public AudioBuffer(AudioSession session, AudioServer server){ + this.session = session; + this.server = server; + + audioBuffer = new AudioData[BUFFER_FRAMES]; + } + + /** + * Sets the read index to the write index. All the data will be ignored. + * No audio more. + */ + public void flush(){ + readIndex = writeIndex; + } + + /** + * Returns the next ready frame. If none, waiting for one + * @return the next frame + */ + public int[] getNextFrame() { + try { + AudioData audioData = poll(); + if (audioData == null) { + //missing in combat! + return null; + } else { + readSeqno = audioData.getSequenceNumber(); + return audioData.getData(); + } + } catch (IndexOutOfBoundsException e) { + // If it' because there is not enough packets + if(synced){ + System.err.println("Underrun!!! Not enough frames in buffer!"); + } + // We say the decoder is stopped and we wait for signal + System.err.println("Waiting"); + decoder_isStopped = true; + nextFrameLock.lock(); + try { + nextFrameIsWaiting.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } finally { + nextFrameLock.unlock(); + } + decoder_isStopped = false; + System.err.println("re-starting"); + readIndex++; // We read next packet + + // Underrun: stream reset + session.resetFilter(); + return null; + } + } + + + /** + * Adds packet into the buffer + * @param seqno seqno of the given packet. Used as index + * @param data + */ + public void putPacketInBuffer(int seqno, byte[] data) { + int[] decoded = new int[session.OUTFRAME_BYTES()]; + int outputSize = this.alac_decode(data, decoded); + AudioData audioData = new AudioData(decoded, seqno); + + try { + put(seqno, audioData); + } catch (IndexOutOfBoundsException e) { + // Really to late + System.err.println("Late packet with seq. numb.: " + seqno); + } + + if(decoder_isStopped && calculateActualBufferSize() > START_FILL){ + nextFrameLock.lock(); + nextFrameIsWaiting.signal(); + nextFrameLock.unlock(); + } + } + + /** + * puts an Audio-Data into the array + * @param seqno the Sequence-Number, used as instance + * @param audioData the audio-Data to put + * @throws java.lang.IndexOutOfBoundsException if the package is was already read + */ + private void put(int seqno, AudioData audioData) { + if (seqno < readSeqno) { + throw new IndexOutOfBoundsException(); + } + if (seqno != readSeqno +1) { + server.request_resend(writeIndex, seqno); + } + int index = seqno % BUFFER_FRAMES; + try { + audioBufferLock.lock(); + audioBuffer[index] = audioData; + } finally { + audioBufferLock.unlock(); + writSeqno = seqno; + } + if (index > writeIndex) + writeIndex = index; + } + + /** + * calculates how many packages got buffered + * @return the number of packages buffered + */ + private int calculateActualBufferSize() { + int count = 0; + try { + audioBufferLock.lock(); + int limit = writeIndex; + if (writeIndex < readIndex) { + limit = BUFFER_FRAMES; + for (int i = 0; i < writeIndex; i++) { + AudioData audioData = audioBuffer[i]; + if (audioData.getSequenceNumber() > readSeqno) + count++; + } + } + for (int i = (readIndex + 1) ; i < limit; i++) { + AudioData audioData = audioBuffer[i]; + if (audioData != null && audioData.getSequenceNumber() > readSeqno) + count++; + } + } finally { + audioBufferLock.unlock(); + } + return count; + } + + /** + * returns the next AudioData + * @return null if empty or an instance of AudioData + */ + private synchronized AudioData poll() throws IndexOutOfBoundsException{ + if (readIndex >= writeIndex) + throw new IndexOutOfBoundsException(); + AudioData audioData; + try { + audioBufferLock.lock(); + audioData = audioBuffer[readIndex]; + audioBuffer[readIndex] = null; + } finally { + audioBufferLock.unlock(); + } + readIndex = increment(readIndex); + return audioData; + } + + /** + * increments the index to comply with the ringbuffer (start at 0 if index == BUFFER_FRAMES) + * @return the incremented integer + */ + private int increment(int index) { + if (index < BUFFER_FRAMES) { + return index +1; + } else { + return 0; + } + } + + + /** + * Decrypt and decode the packet. + * @param data + * @param outbuffer the result + * @return + */ + private int alac_decode(byte[] data, int[] outbuffer){ + byte[] packet = new byte[MAX_PACKET]; + + // Init AES + initAES(); + + int i; + for (i=0; i+16<=data.length; i += 16){ + // Decrypt + this.decryptAES(data, i, 16, packet, i); + } + + // The rest of the packet is unencrypted + System.arraycopy(data, i + 0, packet, i + 0, data.length % 16); + + int outputsize = 0; + outputsize = AlacDecodeUtils.decode_frame(session.getAlac(), packet, outbuffer, outputsize); + + assert outputsize==session.getFrameSize()*4; // FRAME_BYTES length + + return outputsize; + } + + + /** + * Initiate the cipher + */ + private void initAES(){ + // Init AES encryption + try { + k = new SecretKeySpec(session.getAESKEY(), "AES"); + c = Cipher.getInstance("AES/CBC/NoPadding"); + c.init(Cipher.DECRYPT_MODE, k, new IvParameterSpec(session.getAESIV())); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Decrypt array from input offset with a length of inputlen and puts it in output at outputoffsest + * @param array + * @param inputOffset + * @param inputLen + * @param output + * @param outputOffset + * @return + */ + private int decryptAES(byte[] array, int inputOffset, int inputLen, byte[] output, int outputOffset){ + try{ + return c.update(array, inputOffset, inputLen, output, outputOffset); + }catch(Exception e){ + e.printStackTrace(); + } + + return -1; + } + } diff --git a/src/AudioData.java b/src/AudioData.java index e5cddeb..c80e661 100644 --- a/src/AudioData.java +++ b/src/AudioData.java @@ -1,9 +1,39 @@ /** - * C struct to java + * Immutable Audio-Data class. * @author bencall * */ public class AudioData { - public boolean ready; - public int[] data; + private final int[] data; + private final int sequenceNumber; + + public AudioData(int[] data, int sequenceNumber) { + this.data = data; + this.sequenceNumber = sequenceNumber; + } + + public int[] getData() { + return data; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AudioData)) return false; + + AudioData audioData = (AudioData) o; + + if (sequenceNumber != audioData.sequenceNumber) return false; + + return true; + } + + @Override + public int hashCode() { + return sequenceNumber; + } }