summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHung-ying Tyan <tyanh@google.com>2010-03-11 18:35:58 +0800
committerHung-ying Tyan <tyanh@google.com>2010-03-11 19:08:08 +0800
commit61614b6f15f23e1721f9c278aa4b7976cc6f1f52 (patch)
treec4095c4cee1c900922de8dd7b65eb26ffd3b3086
parentebadf5174f8ab1efe437686e868a27ac4e92fb20 (diff)
downloadnist-sip-61614b6f15f23e1721f9c278aa4b7976cc6f1f52.tar.gz
Add G711 codec and minimal RTP.
-rw-r--r--src/android/net/sip/SdpSessionDescription.java7
-rw-r--r--src/com/android/sip/SipMain.java57
-rw-r--r--src/com/android/sip/media/AudioStream.java503
-rw-r--r--src/com/android/sip/media/Decoder.java34
-rw-r--r--src/com/android/sip/media/Encoder.java31
-rw-r--r--src/com/android/sip/media/G711Codec.java83
-rw-r--r--src/com/android/sip/media/RtpPacket.java134
7 files changed, 823 insertions, 26 deletions
diff --git a/src/android/net/sip/SdpSessionDescription.java b/src/android/net/sip/SdpSessionDescription.java
index dc97570..d169a62 100644
--- a/src/android/net/sip/SdpSessionDescription.java
+++ b/src/android/net/sip/SdpSessionDescription.java
@@ -27,6 +27,7 @@ import gov.nist.javax.sdp.fields.TimeField;
import gov.nist.javax.sdp.parser.SDPAnnounceParser;
import java.text.ParseException;
+import java.util.Collections;
import java.util.Vector;
import javax.sdp.Connection;
import javax.sdp.MediaDescription;
@@ -94,14 +95,16 @@ public class SdpSessionDescription implements SessionDescription {
}
public Builder addMedia(String media, int port, int numPorts,
- String transport, Vector types) throws SdpException {
+ String transport, Integer... types) throws SdpException {
MediaField field = new MediaField();
+ Vector<Integer> typeVector = new Vector<Integer>();
+ Collections.addAll(typeVector, types);
try {
field.setMediaType(media);
field.setMediaPort(port);
field.setPortCount(numPorts);
field.setProtocol(transport);
- field.setMediaFormats(types);
+ field.setMediaFormats(typeVector);
mSessionDescription.addField(field);
} catch (Exception e) {
throw new SdpException(e.toString(), e);
diff --git a/src/com/android/sip/SipMain.java b/src/com/android/sip/SipMain.java
index 3ad4743..39c594a 100644
--- a/src/com/android/sip/SipMain.java
+++ b/src/com/android/sip/SipMain.java
@@ -16,6 +16,8 @@
package com.android.sip;
+import com.android.sip.media.AudioStream;
+
import android.net.sip.SdpSessionDescription;
import android.net.sip.SessionDescription;
import android.net.sip.SipProfile;
@@ -39,7 +41,6 @@ import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.text.ParseException;
-import java.util.Vector;
import javax.sdp.SdpException;
import javax.sip.SipException;
@@ -64,6 +65,7 @@ public class SipMain extends PreferenceActivity
private SipSessionLayer mSipSessionLayer;
private SipSession mSipSession;
private SipSession mSipCallSession;
+ private AudioStream mAudio;
private boolean mHolding = false;
@Override
@@ -337,23 +339,16 @@ public class SipMain extends PreferenceActivity
}
private SdpSessionDescription.Builder getSdpSampleBuilder() {
- // TODO: integrate with SDP
String localIp = getLocalIp();
SdpSessionDescription.Builder sdpBuilder;
try {
- Vector v = new Vector();
- v.add(0);
- v.add(4);
- v.add(18);
sdpBuilder = new SdpSessionDescription.Builder("SIP Call")
.setOrigin(mLocalProfile, (long)Math.random() * 10000000L,
(long)Math.random() * 10000000L, SDPKeywords.IN,
SDPKeywords.IPV4, localIp)
.setConnectionInfo(SDPKeywords.IN, SDPKeywords.IPV4, localIp)
- .addMedia("audio", getLocalMediaPort(), 1, "RTP/AVP", v)
- .addMediaAttribute("rtpmap", "0 PCMU/8000")
- .addMediaAttribute("rtpmap", "4 G723/8000")
- .addMediaAttribute("rtpmap", "18 G729A/8000")
+ .addMedia("audio", getLocalMediaPort(), 1, "RTP/AVP", 8)
+ .addMediaAttribute("rtpmap", "8 PCMA/8000")
.addMediaAttribute("ptime", "20");
} catch (SdpException e) {
throw new RuntimeException(e);
@@ -375,18 +370,42 @@ public class SipMain extends PreferenceActivity
return getSdpSampleBuilder().build();
}
+ private void setAllPreferencesEnabled(final boolean enabled) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ for (Preference preference : allPreferences()) {
+ preference.setEnabled(enabled);
+ }
+ }
+ });
+ }
+
private void startAudioCall(SdpSessionDescription sd) {
String peerMediaAddress = sd.getPeerMediaAddress();
int peerMediaPort = sd.getPeerMediaPort();
Log.i(TAG, "start audiocall " + peerMediaAddress + ":" + peerMediaPort);
- // TODO
+ setAllPreferencesEnabled(false);
+
+ int localPort = getLocalMediaPort();
+ int sampleRate = 8000;
+ int frameSize = sampleRate / 50; // 160
+ try {
+ // TODO: get sample rate from sdp
+ mAudio = new AudioStream(sampleRate, sampleRate, localPort,
+ peerMediaAddress, peerMediaPort);
+ mAudio.start();
+ } catch (Exception e) {
+ Log.e(TAG, "call(): " + e);
+ }
+ Log.v(TAG, " ~~~~~~~~~~~ start media: localPort=" + localPort
+ + ", peer=" + peerMediaAddress + ":" + peerMediaPort);
}
private void stopAudioCall() {
Log.i(TAG, "stop audiocall");
-
- // TODO
+ if (mAudio != null) mAudio.stop();
+ setAllPreferencesEnabled(true);
}
private Preference[] allPreferences() {
@@ -427,11 +446,6 @@ public class SipMain extends PreferenceActivity
return getText(mPeerUri);
}
- private String getPeerMediaAddress() {
- // TODO: from peer SDP
- return createPeerSipProfile().getServerAddress();
- }
-
private String getServerUri() {
return getText(mServerUri);
}
@@ -440,11 +454,6 @@ public class SipMain extends PreferenceActivity
return Integer.parseInt(getText(mLocalMediaPort));
}
- private int getPeerMediaPort() {
- // TODO: from peer SDP
- return getLocalMediaPort();
- }
-
private String getLocalIp() {
try {
DatagramSocket s = new DatagramSocket();
@@ -452,7 +461,7 @@ public class SipMain extends PreferenceActivity
return s.getLocalAddress().getHostAddress();
} catch (IOException e) {
Log.w(TAG, "getLocalIp(): " + e);
- return "127.0.0.1";
+ return "127.0.0.1";
}
}
diff --git a/src/com/android/sip/media/AudioStream.java b/src/com/android/sip/media/AudioStream.java
new file mode 100644
index 0000000..1173199
--- /dev/null
+++ b/src/com/android/sip/media/AudioStream.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sip.media;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.MediaRecorder;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class AudioStream {
+ private static final String TAG = AudioStream.class.getSimpleName();
+
+ private boolean mRunning = false;
+ private RecordTask mRecordTask;
+ private PlayTask mPlayTask;
+
+ private DatagramSocket mSocket;
+ private InetAddress mRemoteAddr;
+ private int mRemotePort;
+ private int mLocalPort;
+
+ public AudioStream(int localSampleRate, int remoteSampleRate,
+ int localPort) {
+ mLocalPort = localPort;
+ int localFrameSize = localSampleRate / 50; // 50 frames / sec
+ mRecordTask = new RecordTask(localSampleRate, localFrameSize);
+ mPlayTask = new PlayTask(remoteSampleRate, localFrameSize);
+ }
+
+ public AudioStream(int localSampleRate, int remoteSampleRate, int localPort,
+ String remoteIp, int remotePort) throws UnknownHostException {
+ this(localSampleRate, remoteSampleRate, localPort);
+ mRemoteAddr = TextUtils.isEmpty(remoteIp)
+ ? null
+ : InetAddress.getByName(remoteIp);
+ mRemotePort = remotePort;
+ }
+
+ public void start() throws SocketException {
+ if (mRunning) return;
+
+ mSocket = new DatagramSocket(mLocalPort);
+
+ mRunning = true;
+ Log.v(TAG, "start AudioStream: remoteAddr=" + mRemoteAddr);
+ if (mRemoteAddr != null) {
+ mRecordTask.start(mRemoteAddr, mRemotePort);
+ }
+ mPlayTask.start();
+ }
+
+ public void stop() {
+ mRunning = false;
+ mSocket.close();
+ // TODO: wait until both threads are stopped
+ }
+
+ private void startRecordTask(InetAddress remoteAddr, int remotePort) {
+ mRemoteAddr = remoteAddr;
+ mRemotePort = remotePort;
+ if ((mRemoteAddr != null) && mRunning && !mSocket.isConnected()) {
+ mRecordTask.start(remoteAddr, remotePort);
+ }
+ }
+
+ private class PlayTask implements Runnable {
+ private int mSampleRate;
+ private int mFrameSize;
+
+ PlayTask(int sampleRate, int frameSize) {
+ mSampleRate = sampleRate;
+ mFrameSize = frameSize;
+ }
+
+ void start() {
+ new Thread(this).start();
+ }
+
+ public void run() {
+ Decoder decoder = new G711Codec();
+ int playBufferSize = decoder.getSampleCount(mFrameSize);
+ short[] playBuffer = new short[playBufferSize];
+
+ RtpReceiver receiver = new RtpReceiver(mFrameSize);
+ byte[] buffer = receiver.getBuffer();
+ int offset = receiver.getPayloadOffset();
+
+ int minBufferSize = AudioTrack.getMinBufferSize(mSampleRate,
+ AudioFormat.CHANNEL_CONFIGURATION_MONO,
+ AudioFormat.ENCODING_PCM_16BIT) * 6;
+ minBufferSize = Math.max(minBufferSize, playBufferSize);
+ int bufferHighMark = minBufferSize / 2 * 8 / 10;
+ Log.d(TAG, " play buffer = " + minBufferSize + ", high water mark="
+ + bufferHighMark);
+ AudioTrack aplayer = new AudioTrack(AudioManager.STREAM_MUSIC,
+ mSampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
+ AudioFormat.ENCODING_PCM_16BIT, minBufferSize,
+ AudioTrack.MODE_STREAM);
+ AudioPlayer player =
+ new AudioPlayer(aplayer, minBufferSize, mFrameSize);
+
+ Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
+ player.play();
+ int playState = player.getPlayState();
+ boolean socketConnected = mSocket.isConnected();
+ long receiveCount = 0;
+ long cyclePeriod = mFrameSize * 1000L / mSampleRate;
+ long accumulatedExcessDelay = 0;
+ long accumulatedBytes = 0;
+ long bytesPerMillis = mSampleRate / 1000L;
+ long cycleStart = 0, thisCycle;
+ long time;
+ int seqNo = 0;
+ int packetLossCount = 0;
+ int bytesDropped = 0;
+
+ player.flush();
+ int writeHead = player.getPlaybackHeadPosition();
+
+ // skip the first byte
+ receiver.receive();
+ cycleStart = System.currentTimeMillis();
+ seqNo = receiver.getSequenceNumber();
+
+ if (!socketConnected) {
+ socketConnected = true;
+ startRecordTask(receiver.getRemoteAddress(),
+ receiver.getRemotePort());
+ }
+
+ long startTime = System.currentTimeMillis();
+ long virtualClock = startTime;
+ float delta = 0f;
+
+ while (mRunning) {
+ int count = receiver.receive();
+ if (count > 0) {
+ try {
+ int decodeCount = decoder.decode(
+ playBuffer, buffer, count, offset);
+ if (playState == AudioTrack.PLAYSTATE_STOPPED) {
+ player.play();
+ playState = player.getPlayState();
+ }
+
+ receiveCount ++;
+ int sn = receiver.getSequenceNumber();
+ int lossCount = sn - seqNo - 1;
+ if (lossCount > 0) {
+ packetLossCount += lossCount;
+ virtualClock += lossCount * cyclePeriod;
+ }
+ virtualClock += cyclePeriod;
+ long now = System.currentTimeMillis();
+ long late = now - virtualClock;
+ if (late < 0) {
+ Log.d(TAG, " move vc back: " + late);
+ virtualClock = now;
+ }
+
+ delta = delta * 0.96f + late * 0.04f;
+ late -= (long) delta;
+
+ if (late > 100) {
+ // drop
+ bytesDropped += decodeCount;
+ Log.d(TAG, " drop " + decodeCount + ", late: "
+ + late + ", d=" + delta);
+ cycleStart = now;
+ seqNo = sn;
+ continue;
+ }
+ int playHead = player.getPlaybackHeadPosition();
+ int buffered = writeHead - playHead;
+ if (buffered > bufferHighMark) {
+ player.flush();
+ buffered = 0;
+ writeHead = player.getPlaybackHeadPosition();
+ Log.d(TAG, " ~~~ flush: " + writeHead);
+ }
+
+ time = System.currentTimeMillis();
+ writeHead +=
+ player.write(playBuffer, 0, decodeCount);
+ thisCycle = time - cycleStart;
+
+ accumulatedExcessDelay = late;
+ accumulatedBytes = late * bytesPerMillis;
+
+ cycleStart = time;
+ seqNo = sn;
+ } catch (IOException e) {
+ Log.w(TAG, " ~~~ xxx ~~~ decode error: " + e);
+ }
+ } else {
+ Log.w(TAG, "network disconnected; playback stopped");
+ player.stop();
+ playState = player.getPlayState();
+ }
+ }
+ Log.d(TAG, " receiveCount = " + receiveCount);
+ Log.d(TAG, " acc excess delay =" + accumulatedExcessDelay);
+ Log.d(TAG, " acc bytes =" + accumulatedBytes);
+ Log.d(TAG, " # packets lost =" + packetLossCount);
+ Log.d(TAG, " bytes dropped =" + bytesDropped);
+ Log.d(TAG, "stop sound playing...");
+ player.stop();
+ player.release();
+ }
+ }
+
+ private class RecordTask implements Runnable {
+ private int mSampleRate;
+ private int mFrameSize;
+
+ RecordTask(int sampleRate, int frameSize) {
+ mSampleRate = sampleRate;
+ mFrameSize = frameSize;
+ }
+
+ void start(InetAddress addr, int port) {
+ Log.d(TAG, "start RecordTask, connect to " + addr + ":" + port);
+ mSocket.connect(addr, port);
+ new Thread(this).start();
+ }
+
+ public void run() {
+ Encoder encoder = new G711Codec();
+ int recordBufferSize = encoder.getSampleCount(mFrameSize);
+ short[] recordBuffer = new short[recordBufferSize];
+ RtpSender sender = new RtpSender(mFrameSize);
+ byte[] buffer = sender.getBuffer();
+ int offset = sender.getPayloadOffset();
+
+ int bufferSize = AudioRecord.getMinBufferSize(mSampleRate,
+ AudioFormat.CHANNEL_CONFIGURATION_MONO,
+ AudioFormat.ENCODING_PCM_16BIT) * 3 / 2;
+
+ AudioRecord recorder = new AudioRecord(
+ MediaRecorder.AudioSource.MIC, mSampleRate,
+ AudioFormat.CHANNEL_CONFIGURATION_MONO,
+ AudioFormat.ENCODING_PCM_16BIT, bufferSize);
+
+ recorder.startRecording();
+ Log.d(TAG, "start sound recording..." + recorder.getState());
+
+ // skip the first read, kick off read pipeline
+ recorder.read(recordBuffer, 0, recordBufferSize);
+
+ long sendCount = 0;
+ long startTime = System.currentTimeMillis();
+
+ while (mRunning) {
+ int count = recorder.read(recordBuffer, 0, recordBufferSize);
+
+ int encodeCount =
+ encoder.encode(recordBuffer, count, buffer, offset);
+ sender.send(encodeCount);
+ sendCount ++;
+ }
+ long now = System.currentTimeMillis();
+ Log.d(TAG, " sendCount = " + sendCount);
+ Log.d(TAG, " avg send cycle ="
+ + ((double) (now - startTime) / sendCount));
+ Log.d(TAG, "stop sound recording...");
+ recorder.stop();
+ }
+ }
+
+ private class RtpReceiver {
+ RtpPacket mPacket;
+ DatagramPacket mDatagram;
+
+ RtpReceiver(int size) {
+ byte[] buffer = new byte[size + 12];
+ mPacket = new RtpPacket(buffer);
+ mPacket.setPayloadType(8);
+ mDatagram = new DatagramPacket(buffer, buffer.length);
+ }
+
+ byte[] getBuffer() {
+ return mPacket.getRawPacket();
+ }
+
+ int getPayloadOffset() {
+ return 12;
+ }
+
+ // return received payload size
+ int receive() {
+ DatagramPacket datagram = mDatagram;
+
+ try {
+ mSocket.receive(datagram);
+ } catch (IOException e) {
+ return 0;
+ }
+
+ return datagram.getLength() - 12;
+ }
+
+ InetAddress getRemoteAddress() {
+ return mDatagram.getAddress();
+ }
+
+ int getRemotePort() {
+ return mDatagram.getPort();
+ }
+
+ int getSequenceNumber() {
+ return mPacket.getSequenceNumber();
+ }
+ }
+
+ private class RtpSender extends RtpReceiver {
+ private int mSequence = 0;
+ private long mTimeStamp = 0;
+
+ RtpSender(int size) {
+ super(size);
+ }
+
+ void send(int count) {
+ mTimeStamp += count;
+ RtpPacket packet = mPacket;
+ packet.setSequenceNumber(mSequence++);
+ packet.setTimestamp(mTimeStamp);
+ packet.setPayloadLength(count);
+
+ DatagramPacket datagram = mDatagram;
+ datagram.setLength(packet.getPacketLength());
+ try {
+ mSocket.send(datagram);
+ } catch (IOException e) {
+ Log.w("RtpSender", "running..." + e);
+ }
+ }
+ }
+
+ // Use another thread to play back to avoid playback blocks network
+ // receiving thread
+ private class AudioPlayer implements
+ AudioTrack.OnPlaybackPositionUpdateListener {
+ private short[] mBuffer;
+ private int mStartMarker;
+ private int mEndMarker;
+ private AudioTrack mTrack;
+ private int mFrameSize;
+ private int mOffset;
+ private boolean mIsPlaying = false;
+ private boolean mNotificationStarted = false;
+
+ AudioPlayer(AudioTrack track, int bufferSize, int frameSize) {
+ mTrack = track;
+ mBuffer = new short[bufferSize];
+ mFrameSize = frameSize;
+ }
+
+ synchronized int write(short[] buffer, int offset, int count) {
+ int bufferSize = mBuffer.length;
+ while (getBufferedDataSize() + count > bufferSize) {
+ try {
+ wait();
+ } catch (Exception e) {
+ //
+ }
+ }
+
+ int end = mEndMarker % bufferSize;
+ if (end + count > bufferSize) {
+ int partialSize = bufferSize - end;
+ System.arraycopy(buffer, offset, mBuffer, end, partialSize);
+ System.arraycopy(buffer, offset + partialSize, mBuffer, 0,
+ count - partialSize);
+ } else {
+ System.arraycopy(buffer, 0, mBuffer, end, count);
+ }
+ mEndMarker += count;
+
+ if (!mNotificationStarted) {
+ writeToTrack();
+ }
+
+ return count;
+ }
+
+ synchronized void flush() {
+ mEndMarker = mStartMarker;
+ notify();
+ }
+
+ int getBufferedDataSize() {
+ return mEndMarker - mStartMarker;
+ }
+
+ synchronized void play() {
+ if (!mIsPlaying) {
+ mTrack.setPositionNotificationPeriod(mFrameSize);
+ mTrack.setPlaybackPositionUpdateListener(this);
+ mIsPlaying = true;
+ mTrack.play();
+ mOffset = mTrack.getPlaybackHeadPosition();
+ }
+ }
+
+ synchronized void stop() {
+ mTrack.stop();
+ mTrack.setPlaybackPositionUpdateListener(null);
+ mIsPlaying = false;
+ Log.d(TAG, "mIsPlaying set false");
+ }
+
+ synchronized void release() {
+ mTrack.release();
+ }
+
+ int getPlaybackHeadPosition() {
+ return mStartMarker;
+ }
+
+ int getPlayState() {
+ return mTrack.getPlayState();
+ }
+
+ int getState() {
+ return mTrack.getState();
+ }
+
+ // callback
+ public void onMarkerReached(AudioTrack track) {
+ }
+
+ // callback
+ public synchronized void onPeriodicNotification(AudioTrack track) {
+ if (!mNotificationStarted) {
+ mNotificationStarted = true;
+ Log.d(TAG, " ~~~ notification callback started");
+ } else if (!mIsPlaying) {
+ Log.d(TAG, " ~x~ notification callback quit");
+ return;
+ }
+ try {
+ writeToTrack();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "writeToTrack()", e);
+ }
+ }
+
+ private synchronized void writeToTrack() {
+ int bufferSize = mBuffer.length;
+ int start = mStartMarker % bufferSize;
+ int end = mEndMarker % bufferSize;
+ if (end == start) {
+ int head = mTrack.getPlaybackHeadPosition() - mOffset;
+ if (mStartMarker == head) {
+ // feed noise; keep mTrack busy
+ // TODO: create real noise
+ mTrack.write(mBuffer, 0, mFrameSize);
+ mOffset += mFrameSize;
+ }
+ return;
+ }
+
+ int count = mFrameSize;
+ if (count < getBufferedDataSize()) count = getBufferedDataSize();
+
+ if ((start + count) <= bufferSize) {
+ mTrack.write(mBuffer, start, count);
+ } else {
+ int partialSize = bufferSize - start;
+ mTrack.write(mBuffer, start, partialSize);
+ mTrack.write(mBuffer, 0, count - partialSize);
+ }
+ mStartMarker += count;
+ notify();
+ }
+ }
+}
diff --git a/src/com/android/sip/media/Decoder.java b/src/com/android/sip/media/Decoder.java
new file mode 100644
index 0000000..c0f1060
--- /dev/null
+++ b/src/com/android/sip/media/Decoder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sip.media;
+
+import java.io.IOException;
+
+public interface Decoder {
+ int getSampleCount(int frameSize);
+
+ /**
+ * Decodes from the encoded data array.
+ *
+ * @param result the decoded result array
+ * @param src encoded data
+ * @param count valid data length in src
+ * @param offset offset of data bytes in src
+ */
+ int decode(short[] result, byte[] src, int count, int offset)
+ throws IOException;
+}
diff --git a/src/com/android/sip/media/Encoder.java b/src/com/android/sip/media/Encoder.java
new file mode 100644
index 0000000..f1845e5
--- /dev/null
+++ b/src/com/android/sip/media/Encoder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sip.media;
+
+public interface Encoder {
+ int getSampleCount(int frameSize);
+
+ /**
+ * Encodes the sample data.
+ *
+ * @param src sample data
+ * @param count valid data length in src
+ * @param result the encoded result array
+ * @param offset offset of data bytes in result
+ */
+ int encode(short[] src, int count, byte[] result, int offset);
+}
diff --git a/src/com/android/sip/media/G711Codec.java b/src/com/android/sip/media/G711Codec.java
new file mode 100644
index 0000000..f58edfd
--- /dev/null
+++ b/src/com/android/sip/media/G711Codec.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sip.media;
+
+/**
+ * G.711 codec. This class provides a-law conversion.
+ */
+public class G711Codec implements Encoder, Decoder {
+ // s0000000wxyz...s000wxyz
+ // s0000001wxyz...s001wxyz
+ // s000001wxyza...s010wxyz
+ // s00001wxyzab...s011wxyz
+ // s0001wxyzabc...s100wxyz
+ // s001wxyzabcd...s101wxyz
+ // s01wxyzabcde...s110wxyz
+ // s1wxyzabcdef...s111wxyz
+
+ private static int b8to16(int b8) {
+ int shift = ((b8 & 0x70) >> 4) - 1;
+ int b12 = b8 & 0xF;
+ if (shift >= 0) b12 = (b12 + 0x10) << shift;
+ return b12 << 4;
+ }
+
+ private static int b12to8(int b12) {
+ int shift = 0;
+ for (int tmp = b12 / 32; tmp > 0; tmp/=2) shift++;
+ int b8 = ((b12 >> shift) & 0xF);
+ if (b12 >= 16) b8 += ((shift + 1) << 4);
+
+ // invert even bits
+ return (b8 ^ 0x55);
+ }
+
+ private static byte[] table12to8 = new byte[4096];
+ private static short[] table8to16 = new short[256];
+
+ static {
+ for (int i = 0; i < 2048; i++) {
+ int v = b12to8(i);
+ table12to8[i] = (byte) v;
+ table12to8[4095 - i] = (byte) (v + 128);
+ }
+
+ for (int i = 0; i < 128; i++) {
+ int v = b8to16(i);
+ table8to16[i ^ 0x55] = (short) v;
+ table8to16[(i + 128) ^ 0x55] = (short) ((65536 - v) & 0xFFFF);
+ }
+ }
+
+ public int decode(short[] b16, byte[] b8, int count, int offset) {
+ for (int i = 0, j = offset; i < count; i++, j++) {
+ b16[i] = table8to16[b8[j] & 0xff];
+ }
+ return count;
+ }
+
+ public int encode(short[] b16, int count, byte[] b8, int offset) {
+ for (int i = 0, j = offset; i < count; i++, j++) {
+ b8[j] = table12to8[(b16[i] & 0xffff) >> 4];
+ }
+ return count;
+ }
+
+ public int getSampleCount(int frameSize) {
+ return frameSize;
+ }
+}
diff --git a/src/com/android/sip/media/RtpPacket.java b/src/com/android/sip/media/RtpPacket.java
new file mode 100644
index 0000000..60d79f2
--- /dev/null
+++ b/src/com/android/sip/media/RtpPacket.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sip.media;
+
+import java.util.Random;
+
+/**
+ * RtpPacket implements a RTP packet.
+ */
+public class RtpPacket {
+ private static Random sRandom = new Random();
+
+ // |0 0 1 2 |
+ // |0.......8.......6.......4.......|
+ // |V PXC MT Seqnum (16) |
+ // |................................|
+ // |Timestamp (32) |
+ // | |
+ // |................................|
+ // | SSRC (32) |
+ // | |
+ // |................................|
+ // | CSRC list (16 items x 32 bits) |
+ // | |
+ // |................................|
+ // V: version, 2 bits
+ // P: padding, 1 bit
+ // X: extension, 1 bit
+ // C: CSRC count, 4 bits
+ // M: marker, 1 bit
+ // T: payload type: 7 bits
+
+ private byte[] packet; // RTP header + payload
+ private int packetLength;
+
+ public RtpPacket(byte[] buffer) {
+ packet = buffer;
+ setVersion(2);
+ setPayloadType(0x0F);
+ setSequenceNumber(sRandom.nextInt());
+ setSscr(sRandom.nextLong());
+ }
+
+ /** Returns the RTP packet in raw bytes. */
+ public byte[] getRawPacket() {
+ return packet;
+ }
+
+ public int getPacketLength() {
+ return packetLength;
+ }
+
+ public int getHeaderLength() {
+ return (12 + 4 * getCscrCount());
+ }
+
+ public int getPayloadLength() {
+ return (packetLength - getHeaderLength());
+ }
+
+ public void setPayloadLength(int length) {
+ packetLength = getHeaderLength() + length;
+ }
+
+ public int getVersion() {
+ return ((packet[0] >> 6) & 0x03);
+ }
+
+ public void setVersion(int v) {
+ if (v > 3) throw new RuntimeException("illegal version: " + v);
+ packet[0] = (byte) ((packet[0] & 0x3F) | ((v & 0x03) << 6));
+ }
+
+ int getCscrCount() {
+ return (packet[0] & 0x0F);
+ }
+
+ public int getPayloadType() {
+ return (packet[1] & 0x7F);
+ }
+
+ public void setPayloadType(int pt) {
+ packet[1] = (byte) ((packet[1] & 0x80) | (pt & 0x7F));
+ }
+
+ public int getSequenceNumber() {
+ return (int) get(2, 2);
+ }
+
+ public void setSequenceNumber(int sn) {
+ set((long) sn, 2, 2);
+ }
+
+ public long getTimestamp() {
+ return get(4, 4);
+ }
+
+ public void setTimestamp(long timestamp) {
+ set(timestamp, 4, 4);
+ }
+
+ void setSscr(long ssrc) {
+ set(ssrc, 8, 4);
+ }
+
+ private long get(int begin, int length) {
+ long n = 0;
+ for (int i = begin, end = i + length; i < end; i++) {
+ n = (n << 8) | ((long) packet[i] & 0xFF);
+ }
+ return n;
+ }
+
+ private void set(long n, int begin, int length) {
+ for (int i = begin + length - 1; i >= begin; i--) {
+ packet[i] = (byte) (n & 0x0FFL);
+ n >>= 8;
+ }
+ }
+}