import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.beatofthedrum.alacdecoder.AlacDecodeUtils;
/**
* A ring buffer where every frame is decrypted, decoded and stored
* Basically, you can put and get packets
* @author bencall
*
*/
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();
// 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<buf.data.length; i++){
buf.data[i] = 0;
}
}
buf.ready = false;
// SEQNO is stored in a short an come back to 0 when equal to 65536 (2 bytes)
if(readIndex == 65536){
readIndex = 0;
}
return buf.data;
}
}
/**
* Adds packet into the buffer
* @param seqno seqno of the given packet. Used as index
* @param data
*/
public void putPacketInBuffer(int seqno, byte[] data){
// Ring buffer may be implemented in a Hashtable in java (simplier), but is it fast enough?
// We lock the thread
synchronized(lock){
if(!synced){
writeIndex = seqno;
readIndex = seqno;
synced = true;
}
@SuppressWarnings("unused")
int outputSize = 0;
if (seqno == writeIndex){ // Packet we expected
outputSize = this.alac_decode(data, audioBuffer[(seqno % BUFFER_FRAMES)].data); // With (seqno % BUFFER_FRAMES) we loop from 0 to BUFFER_FRAMES
audioBuffer[(seqno % BUFFER_FRAMES)].ready = true;
writeIndex++;
} else if(seqno > 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;
}
}