├── .gitignore
├── AndroidManifest.xml
├── README.md
├── lint.xml
├── proguard-project.txt
├── project.properties
├── res
├── drawable-hdpi
│ ├── conversation_recording_round.xml
│ ├── ic_launcher.png
│ ├── record_green.png
│ ├── record_red.png
│ └── record_white.png
├── drawable-mdpi
│ └── ic_launcher.png
├── drawable-xhdpi
│ └── ic_launcher.png
├── layout
│ └── conversation_recording_dialog.xml
├── raw
│ ├── play_end.mp3
│ ├── record_end.mp3
│ └── record_start.mp3
└── values
│ ├── color.xml
│ ├── strings.xml
│ └── styles.xml
└── src
├── android
└── media
│ ├── AmrInputStream.java
│ └── MediaMetadataRetriever.java
└── bz
└── tsung
├── media
└── audio
│ ├── AudioRecorder.java
│ ├── AudioUtil.java
│ ├── IAudioRecorder.java
│ ├── RehearsalAudioRecorder.java
│ └── converters
│ ├── AmrWaveConverter.java
│ ├── UnknownWaveConverter.java
│ └── WaveConverter.java
└── utils
└── android
└── DeviceUtil.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 |
15 | # Local configuration file (sdk path, etc)
16 | local.properties
17 |
18 | # Eclipse project files
19 | .classpath
20 | .project
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
25 | # Intellij project files
26 | *.iml
27 | *.ipr
28 | *.iws
29 | .idea/
30 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | android-realtime-audio-recorder
2 | ===============================
3 |
4 | android audio/sound recorder with events periodically
5 |
6 | base on: http://code.google.com/p/flatnote/source/browse/trunk/src/com/androsz/flatnote/soundrecorder/RehearsalAudioRecorder.java
7 |
--------------------------------------------------------------------------------
/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-17
15 | android.library=true
16 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/conversation_recording_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/record_green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/drawable-hdpi/record_green.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/record_red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/drawable-hdpi/record_red.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/record_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/drawable-hdpi/record_white.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/layout/conversation_recording_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
9 |
12 |
17 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/res/raw/play_end.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/raw/play_end.mp3
--------------------------------------------------------------------------------
/res/raw/record_end.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/raw/record_end.mp3
--------------------------------------------------------------------------------
/res/raw/record_start.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionull/android-realtime-audio-recorder/564c3830296f168773357a502018352e6e58f7a4/res/raw/record_start.mp3
--------------------------------------------------------------------------------
/res/values/color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #000000
5 | #FF0000
6 | #888888
7 | #FFFFFF
8 | #00000000
9 | #00000001
10 |
11 | #e4e4e2
12 | #434343
13 | #666666
14 |
15 | #999999
16 |
17 | #E36F00
18 |
19 | #FF9900
20 |
21 | #FFFFFFFF
22 | #FFE4E3E5
23 | #FFFFFFFF
24 | #00000000
25 | #ced7db
26 | #7e7e7f
27 | #FFFFFFFF
28 |
29 |
30 | #555555
31 |
32 |
33 | #139ec0
34 | #000000
35 | #3685BD
36 | #E0F7FD
37 |
38 | #CE5801
39 | #f4d9af
40 |
41 | #4C4F54
42 |
43 |
44 | #707070
45 | #8dceff
46 | #e26c00
47 | #454545
48 |
49 | #3d6481
50 | #6c6c6c
51 | #ffffff
52 | #b2e6f5
53 | #b2e6f5
54 |
55 |
56 | #589c27
57 | #0070bf
58 | #ff7800
59 | #eb3903
60 |
61 | #386a91
62 |
63 | #50000000
64 |
65 | #CBDFE8
66 | #9D9D9D
67 | #05819D
68 | #784A00
69 | #E0E8EB
70 | #3D6481
71 | #323232
72 | #74B044
73 | #9C9C9C
74 |
75 | #00a9d5
76 | #3a3a3a
77 |
78 |
79 |
80 |
81 | #e19910
82 | #56b60a
83 | #1d8ac7
84 | #df5c5a
85 |
86 | #9c9d9d
87 |
88 | #666666
89 |
90 | #10a1e3
91 | #d5dddf
92 | #d0d0d0
93 |
94 | #a6d2e0
95 | #cdcdcd
96 |
97 |
98 |
99 |
100 | #5E5D5D
101 |
102 | #9C9D9D
103 |
104 | #37393B
105 |
106 | #E0E8EB
107 |
108 | #E0E8EB
109 |
110 | #4e4e4e
111 |
112 | #9a9a9a
113 |
114 | #6c6d6d
115 |
116 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello World, RealtimeAudioRecorderActivity!
5 | RealtimeAudioRecorder
6 |
7 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/src/android/media/AmrInputStream.java:
--------------------------------------------------------------------------------
1 |
2 | package android.media;
3 |
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 |
7 | /**
8 | * AmrInputStream
9 | *
10 | * @hide
11 | */
12 | public final class AmrInputStream extends InputStream
13 | {
14 | static {
15 | System.loadLibrary("media_jni");
16 | }
17 |
18 | private final static String TAG = "AmrInputStream";
19 |
20 | // frame is 20 msec at 8.000 khz
21 | private final static int SAMPLES_PER_FRAME = 8000 * 20 / 1000;
22 |
23 | // pcm input stream
24 | private InputStream mInputStream;
25 |
26 | // native handle
27 | private int mGae;
28 |
29 | // result amr stream
30 | private byte[] mBuf = new byte[SAMPLES_PER_FRAME * 2];
31 |
32 | private int mBufIn = 0;
33 |
34 | private int mBufOut = 0;
35 |
36 | // helper for bytewise read()
37 | private byte[] mOneByte = new byte[1];
38 |
39 | /**
40 | * Create a new AmrInputStream, which converts 16 bit PCM to AMR
41 | *
42 | * @param inputStream InputStream containing 16 bit PCM.
43 | */
44 | public AmrInputStream(InputStream inputStream) {
45 | mInputStream = inputStream;
46 | mGae = GsmAmrEncoderNew();
47 | GsmAmrEncoderInitialize(mGae);
48 | }
49 |
50 | @Override
51 | public int read() throws IOException {
52 | int rtn = read(mOneByte, 0, 1);
53 | return rtn == 1 ? (0xff & mOneByte[0]) : -1;
54 | }
55 |
56 | @Override
57 | public int read(byte[] b) throws IOException {
58 | return read(b, 0, b.length);
59 | }
60 |
61 | @Override
62 | public int read(byte[] b, int offset, int length) throws IOException {
63 | if (mGae == 0)
64 | throw new IllegalStateException("not open");
65 |
66 | // local buffer of amr encoded audio empty
67 | if (mBufOut >= mBufIn) {
68 | // reset the buffer
69 | mBufOut = 0;
70 | mBufIn = 0;
71 |
72 | // fetch a 20 msec frame of pcm
73 | for (int i = 0; i < SAMPLES_PER_FRAME * 2;) {
74 | int n = mInputStream.read(mBuf, i, SAMPLES_PER_FRAME * 2 - i);
75 | if (n == -1)
76 | return -1;
77 | i += n;
78 | }
79 |
80 | // encode it
81 | mBufIn = GsmAmrEncoderEncode(mGae, mBuf, 0, mBuf, 0);
82 | }
83 |
84 | // return encoded audio to user
85 | if (length > mBufIn - mBufOut)
86 | length = mBufIn - mBufOut;
87 | System.arraycopy(mBuf, mBufOut, b, offset, length);
88 | mBufOut += length;
89 |
90 | return length;
91 | }
92 |
93 | @Override
94 | public void close() throws IOException {
95 | try {
96 | if (mInputStream != null)
97 | mInputStream.close();
98 | } finally {
99 | mInputStream = null;
100 | try {
101 | if (mGae != 0)
102 | GsmAmrEncoderCleanup(mGae);
103 | } finally {
104 | try {
105 | if (mGae != 0)
106 | GsmAmrEncoderDelete(mGae);
107 | } finally {
108 | mGae = 0;
109 | }
110 | }
111 | }
112 | }
113 |
114 | @Override
115 | protected void finalize() throws Throwable {
116 | if (mGae != 0) {
117 | close();
118 | throw new IllegalStateException("someone forgot to close AmrInputStream");
119 | }
120 | }
121 |
122 | //
123 | // AudioRecord JNI interface
124 | //
125 | private static native int GsmAmrEncoderNew();
126 |
127 | private static native void GsmAmrEncoderInitialize(int gae);
128 |
129 | private static native int GsmAmrEncoderEncode(int gae,
130 | byte[] pcm, int pcmOffset, byte[] amr, int amrOffset) throws IOException;
131 |
132 | private static native void GsmAmrEncoderCleanup(int gae);
133 |
134 | private static native void GsmAmrEncoderDelete(int gae);
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/src/android/media/MediaMetadataRetriever.java:
--------------------------------------------------------------------------------
1 |
2 | package android.media;
3 |
4 | import java.io.FileDescriptor;
5 | import java.io.FileNotFoundException;
6 | import java.io.IOException;
7 |
8 | import android.content.ContentResolver;
9 | import android.content.Context;
10 | import android.content.res.AssetFileDescriptor;
11 | import android.graphics.Bitmap;
12 | import android.net.Uri;
13 |
14 | /**
15 | * MediaMetadataRetriever class provides a unified interface for retrieving
16 | * frame and meta data from an input media file. {@hide}
17 | */
18 | public class MediaMetadataRetriever {
19 | static {
20 | System.loadLibrary("media_jni");
21 | native_init();
22 | }
23 |
24 | // The field below is accessed by native methods
25 | @SuppressWarnings("unused")
26 | private int mNativeContext;
27 |
28 | public MediaMetadataRetriever() {
29 | native_setup();
30 | }
31 |
32 | /**
33 | * Call this method before setDataSource() so that the mode becomes
34 | * effective for subsequent operations. This method can be called only once
35 | * at the beginning if the intended mode of operation for a
36 | * MediaMetadataRetriever object remains the same for its whole lifetime,
37 | * and thus it is unnecessary to call this method each time setDataSource()
38 | * is called. If this is not never called (which is allowed), by default the
39 | * intended mode of operation is to both capture frame and retrieve meta
40 | * data (i.e., MODE_GET_METADATA_ONLY | MODE_CAPTURE_FRAME_ONLY). Often,
41 | * this may not be what one wants, since doing this has negative performance
42 | * impact on execution time of a call to setDataSource(), since both types
43 | * of operations may be time consuming.
44 | *
45 | * @param mode The intended mode of operation. Can be any combination of
46 | * MODE_GET_METADATA_ONLY and MODE_CAPTURE_FRAME_ONLY: 1.
47 | * MODE_GET_METADATA_ONLY & MODE_CAPTURE_FRAME_ONLY: For neither
48 | * frame capture nor meta data retrieval 2.
49 | * MODE_GET_METADATA_ONLY: For meta data retrieval only 3.
50 | * MODE_CAPTURE_FRAME_ONLY: For frame capture only 4.
51 | * MODE_GET_METADATA_ONLY | MODE_CAPTURE_FRAME_ONLY: For both
52 | * frame capture and meta data retrieval
53 | */
54 | public native void setMode(int mode);
55 |
56 | /**
57 | * @return the current mode of operation. A negative return value indicates
58 | * some runtime error has occurred.
59 | */
60 | public native int getMode();
61 |
62 | /**
63 | * Sets the data source (file pathname) to use. Call this method before the
64 | * rest of the methods in this class. This method may be time-consuming.
65 | *
66 | * @param path The path of the input media file.
67 | * @throws IllegalArgumentException If the path is invalid.
68 | */
69 | public native void setDataSource(String path) throws IllegalArgumentException;
70 |
71 | /**
72 | * Sets the data source (FileDescriptor) to use. It is the caller's
73 | * responsibility to close the file descriptor. It is safe to do so as soon
74 | * as this call returns. Call this method before the rest of the methods in
75 | * this class. This method may be time-consuming.
76 | *
77 | * @param fd the FileDescriptor for the file you want to play
78 | * @param offset the offset into the file where the data to be played
79 | * starts, in bytes. It must be non-negative
80 | * @param length the length in bytes of the data to be played. It must be
81 | * non-negative.
82 | * @throws IllegalArgumentException if the arguments are invalid
83 | */
84 | public native void setDataSource(FileDescriptor fd, long offset, long length)
85 | throws IllegalArgumentException;
86 |
87 | /**
88 | * Sets the data source (FileDescriptor) to use. It is the caller's
89 | * responsibility to close the file descriptor. It is safe to do so as soon
90 | * as this call returns. Call this method before the rest of the methods in
91 | * this class. This method may be time-consuming.
92 | *
93 | * @param fd the FileDescriptor for the file you want to play
94 | * @throws IllegalArgumentException if the FileDescriptor is invalid
95 | */
96 | public void setDataSource(FileDescriptor fd)
97 | throws IllegalArgumentException {
98 | // intentionally less than LONG_MAX
99 | setDataSource(fd, 0, 0x7ffffffffffffffL);
100 | }
101 |
102 | /**
103 | * Sets the data source as a content Uri. Call this method before the rest
104 | * of the methods in this class. This method may be time-consuming.
105 | *
106 | * @param context the Context to use when resolving the Uri
107 | * @param uri the Content URI of the data you want to play
108 | * @throws IllegalArgumentException if the Uri is invalid
109 | * @throws SecurityException if the Uri cannot be used due to lack of
110 | * permission.
111 | */
112 | public void setDataSource(Context context, Uri uri)
113 | throws IllegalArgumentException, SecurityException {
114 | if (uri == null) {
115 | throw new IllegalArgumentException();
116 | }
117 |
118 | String scheme = uri.getScheme();
119 | if (scheme == null || scheme.equals("file")) {
120 | setDataSource(uri.getPath());
121 | return;
122 | }
123 |
124 | AssetFileDescriptor fd = null;
125 | try {
126 | ContentResolver resolver = context.getContentResolver();
127 | try {
128 | fd = resolver.openAssetFileDescriptor(uri, "r");
129 | } catch (FileNotFoundException e) {
130 | throw new IllegalArgumentException();
131 | }
132 | if (fd == null) {
133 | throw new IllegalArgumentException();
134 | }
135 | FileDescriptor descriptor = fd.getFileDescriptor();
136 | if (!descriptor.valid()) {
137 | throw new IllegalArgumentException();
138 | }
139 | // Note: using getDeclaredLength so that our behavior is the same
140 | // as previous versions when the content provider is returning
141 | // a full file.
142 | if (fd.getDeclaredLength() < 0) {
143 | setDataSource(descriptor);
144 | } else {
145 | setDataSource(descriptor, fd.getStartOffset(), fd.getDeclaredLength());
146 | }
147 | return;
148 | } catch (SecurityException ex) {
149 | } finally {
150 | try {
151 | if (fd != null) {
152 | fd.close();
153 | }
154 | } catch (IOException ioEx) {
155 | }
156 | }
157 | setDataSource(uri.toString());
158 | }
159 |
160 | /**
161 | * Call this method after setDataSource(). This method retrieves the meta
162 | * data value associated with the keyCode. The keyCode currently supported
163 | * is listed below as METADATA_XXX constants. With any other value, it
164 | * returns a null pointer.
165 | *
166 | * @param keyCode One of the constants listed below at the end of the class.
167 | * @return The meta data value associate with the given keyCode on success;
168 | * null on failure.
169 | */
170 | public native String extractMetadata(int keyCode);
171 |
172 | /**
173 | * Call this method after setDataSource(). This method finds a
174 | * representative frame if successful and returns it as a bitmap. This is
175 | * useful for generating a thumbnail for an input media source.
176 | *
177 | * @return A Bitmap containing a representative video frame, which can be
178 | * null, if such a frame cannot be retrieved.
179 | */
180 | public native Bitmap captureFrame();
181 |
182 | /**
183 | * Call this method after setDataSource(). This method finds the optional
184 | * graphic or album art associated (embedded or external url linked) the
185 | * related data source.
186 | *
187 | * @return null if no such graphic is found.
188 | */
189 | public native byte[] extractAlbumArt();
190 |
191 | /**
192 | * Call it when one is done with the object. This method releases the memory
193 | * allocated internally.
194 | */
195 | public native void release();
196 |
197 | private native void native_setup();
198 |
199 | private static native void native_init();
200 |
201 | private native final void native_finalize();
202 |
203 | @Override
204 | protected void finalize() throws Throwable {
205 | try {
206 | native_finalize();
207 | } finally {
208 | super.finalize();
209 | }
210 | }
211 |
212 | public static final int MODE_GET_METADATA_ONLY = 0x01;
213 |
214 | public static final int MODE_CAPTURE_FRAME_ONLY = 0x02;
215 |
216 | /*
217 | * Do not change these values without updating their counterparts in
218 | * include/media/mediametadataretriever.h!
219 | */
220 | public static final int METADATA_KEY_CD_TRACK_NUMBER = 0;
221 |
222 | public static final int METADATA_KEY_ALBUM = 1;
223 |
224 | public static final int METADATA_KEY_ARTIST = 2;
225 |
226 | public static final int METADATA_KEY_AUTHOR = 3;
227 |
228 | public static final int METADATA_KEY_COMPOSER = 4;
229 |
230 | public static final int METADATA_KEY_DATE = 5;
231 |
232 | public static final int METADATA_KEY_GENRE = 6;
233 |
234 | public static final int METADATA_KEY_TITLE = 7;
235 |
236 | public static final int METADATA_KEY_YEAR = 8;
237 |
238 | public static final int METADATA_KEY_DURATION = 9;
239 |
240 | public static final int METADATA_KEY_NUM_TRACKS = 10;
241 |
242 | public static final int METADATA_KEY_IS_DRM_CRIPPLED = 11;
243 |
244 | public static final int METADATA_KEY_CODEC = 12;
245 |
246 | public static final int METADATA_KEY_RATING = 13;
247 |
248 | public static final int METADATA_KEY_COMMENT = 14;
249 |
250 | public static final int METADATA_KEY_COPYRIGHT = 15;
251 |
252 | public static final int METADATA_KEY_BIT_RATE = 16;
253 |
254 | public static final int METADATA_KEY_FRAME_RATE = 17;
255 |
256 | public static final int METADATA_KEY_VIDEO_FORMAT = 18;
257 |
258 | public static final int METADATA_KEY_VIDEO_HEIGHT = 19;
259 |
260 | public static final int METADATA_KEY_VIDEO_WIDTH = 20;
261 |
262 | public static final int METADATA_KEY_WRITER = 21;
263 |
264 | public static final int METADATA_KEY_MIMETYPE = 22;
265 |
266 | public static final int METADATA_KEY_DISCNUMBER = 23;
267 |
268 | public static final int METADATA_KEY_ALBUMARTIST = 24;
269 | // Add more here...
270 | }
271 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/AudioRecorder.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.media.audio;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.io.PipedInputStream;
6 | import java.io.PipedOutputStream;
7 | import java.util.Date;
8 | import java.util.Timer;
9 | import java.util.TimerTask;
10 |
11 | import android.app.Activity;
12 | import android.app.AlertDialog;
13 | import android.content.Context;
14 | import android.media.MediaPlayer;
15 | import android.media.MediaPlayer.OnCompletionListener;
16 | import android.os.Handler;
17 | import android.util.AttributeSet;
18 | import android.util.Log;
19 | import android.view.LayoutInflater;
20 | import android.view.MotionEvent;
21 | import android.view.View;
22 | import android.view.View.OnTouchListener;
23 | import android.view.ViewGroup;
24 | import android.widget.Button;
25 | import android.widget.FrameLayout;
26 | import android.widget.ImageView;
27 | import android.widget.TextView;
28 | import android.widget.Toast;
29 | import bz.tsung.media.audio.RehearsalAudioRecorder.IRehearsalAudioRecorderListener;
30 | import bz.tsung.media.audio.converters.UnknownWaveConverter;
31 | import bz.tsung.media.audio.converters.WaveConverter;
32 | import bz.tsung.media.audio.converters.WaveConverter.WaveConvertComplete;
33 | import bz.tsung.utils.android.DeviceUtil;
34 |
35 | /**
36 | * 录音按钮控件
37 | *
38 | * @date Dec 2, 2011
39 | * @author Tsung Wu
40 | */
41 | public class AudioRecorder extends Button implements OnTouchListener, IAudioRecorder {
42 | private static final String TAG = "AudioRecorder";
43 |
44 | private Context mContext;
45 | private MediaPlayer mPlayerHintStart;
46 | private MediaPlayer mPlayerHintEnd;
47 |
48 | public AudioRecorder(Context context) {
49 | super(context);
50 | init(context);
51 | }
52 |
53 | public AudioRecorder(Context context, AttributeSet attrs) {
54 | super(context, attrs);
55 | init(context);
56 | }
57 |
58 | public AudioRecorder(Context context, AttributeSet attrs, int defStyle) {
59 | super(context, attrs, defStyle);
60 | init(context);
61 | }
62 |
63 | private void init(Context context) {
64 | mContext = context;
65 | this.setOnTouchListener(this);
66 | mPlayerHintStart = MediaPlayer.create(context, R.raw.record_start);
67 | mPlayerHintEnd = MediaPlayer.create(context, R.raw.record_end);
68 | createWaveConverter();
69 | }
70 |
71 | private AudioUtil audioUtil;
72 | protected WaveConverter waveConverter = new UnknownWaveConverter();
73 |
74 | private Date mBegin;
75 |
76 | private AlertDialog alertDialog;
77 |
78 | private ImageView recordingImageBG;
79 |
80 | private ImageView recordingImage;
81 |
82 | private TextView recordingText;
83 |
84 | private Timer recordingTimer;
85 |
86 | private Handler mHandler = new Handler();
87 |
88 | private boolean start = false;
89 |
90 | private Timer sixtySecondsTimer;
91 |
92 | public void createWaveConverter() {
93 | waveConverter = new UnknownWaveConverter();
94 | }
95 |
96 | private class VolumeTimerTask extends TimerTask {
97 |
98 | @Override
99 | public void run() {
100 | mHandler.post(new Runnable() {
101 | @Override
102 | public void run() {
103 | int max = audioUtil.getMaxAmplitude();
104 |
105 | if (max != 0) {
106 | FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) recordingImage
107 | .getLayoutParams();
108 | float scale = max / 7000.0f;
109 | if (scale < 0.3) {
110 | recordingImage
111 | .setImageResource(R.drawable.record_red);
112 | } else {
113 | recordingImage
114 | .setImageResource(R.drawable.record_green);
115 | }
116 | if (scale > 1) {
117 | scale = 1;
118 | }
119 | int height = recordingImageBG.getHeight()
120 | - (int) (scale * recordingImageBG.getHeight());
121 | params.setMargins(0, 0, 0, -1 * height);
122 | recordingImage.setLayoutParams(params);
123 |
124 | ((View) recordingImage).scrollTo(0, height);
125 | // Log.i(TAG, "max amplitude: " + max);
126 | /**
127 | * 倒计时提醒
128 | */
129 | Date now = new Date();
130 | long between = (mBegin.getTime() + 60000)
131 | - now.getTime();
132 | if (between < 10000) {
133 | int second = (int) (Math.floor((between / 1000)));
134 | if (second == 0) {
135 | second = 1;
136 | }
137 | recordingText.setText("还剩: " + second + "秒");
138 | }
139 | }
140 | }
141 | });
142 | }
143 |
144 | }
145 |
146 | @Override
147 | public boolean onTouch(View v, MotionEvent event) {
148 | if (event.getAction() == MotionEvent.ACTION_DOWN) {
149 | if (DeviceUtil.isFullStorage()) {
150 | Toast.makeText(mContext, "您没有可用的SD卡,请退出U盘模式或者插入SD卡", 5000)
151 | .show();
152 | return false;
153 | }
154 | try {
155 | touchDown = true;
156 |
157 | if (mPlayerHintStart != null) {
158 | mPlayerHintStart.release();
159 | }
160 | mPlayerHintStart = MediaPlayer.create(mContext,
161 | R.raw.record_start);
162 | mPlayerHintStart.start();
163 | mPlayerHintStart
164 | .setOnCompletionListener(new OnCompletionListener() {
165 |
166 | @Override
167 | public void onCompletion(MediaPlayer arg0) {
168 | if (!touchDown) {
169 | return;
170 | }
171 | start = true;
172 |
173 | recordingTimer = new Timer();
174 | recordingTimer.schedule(new VolumeTimerTask(),
175 | 0, 100);
176 |
177 | audioUtil
178 | .setOnRecordListener(mInsideRecordListener);
179 | try {
180 | Log.i(TAG, "start record");
181 | audioUtil.startRecord();
182 | mInsideRecordListener.onRecordStart();
183 |
184 | mBegin = new Date();
185 |
186 | sixtySecondsTimer = new Timer();
187 | sixtySecondsTimer.schedule(new TimerTask() {
188 |
189 | @Override
190 | public void run() {
191 | mHandler.post(new Runnable() {
192 |
193 | @Override
194 | public void run() {
195 | Log.i(TAG,
196 | "recording end by timeout");
197 | onRecordEnd();
198 | }
199 | });
200 | }
201 | }, 60000);
202 | } catch (Exception e) {
203 | Log.e(TAG,
204 | "Record start error: " + e != null ? e
205 | .getMessage() : "");
206 | onRecordEnd();
207 | }
208 | }
209 | });
210 |
211 | /*
212 | * show dialog
213 | */
214 | AlertDialog.Builder builder;
215 |
216 | LayoutInflater inflater = (LayoutInflater) mContext
217 | .getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
218 | View layout = inflater.inflate(
219 | R.layout.conversation_recording_dialog,
220 | (ViewGroup) findViewById(R.id.conversation_recording));
221 |
222 | recordingImage = (ImageView) layout
223 | .findViewById(R.id.conversation_recording_range);
224 | recordingImageBG = (ImageView) layout
225 | .findViewById(R.id.conversation_recording_white);
226 |
227 | recordingText = (TextView) layout
228 | .findViewById(R.id.conversation_recording_text);
229 | recordingText.setText("正在准备");
230 |
231 | builder = new AlertDialog.Builder(mContext);
232 | alertDialog = builder.create();
233 | alertDialog.show();
234 | alertDialog.getWindow().setContentView(layout);
235 | } catch (Exception e) {
236 | return onRecordEnd();
237 | }
238 | } else if (event.getAction() == MotionEvent.ACTION_UP) {
239 | Log.i(TAG, "recording end by action button up");
240 | return onRecordEnd();
241 | } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
242 | Log.i(TAG, "recording end by action button outside");
243 | return onRecordEnd();
244 | }
245 | return false;
246 | }
247 |
248 | private boolean touchDown = false;
249 |
250 | public boolean onRecordEnd() {
251 | touchDown = false;
252 | if (start) {
253 | if (mPlayerHintEnd != null) {
254 | mPlayerHintEnd.release();
255 | }
256 | mPlayerHintEnd = MediaPlayer.create(mContext, R.raw.record_end);
257 | mPlayerHintEnd.start();
258 | }
259 | boolean result = onRecordEnding();
260 | // audioUtil.resetRecorder();
261 | return result;
262 | }
263 |
264 | private boolean onRecordEnding() {
265 | /**
266 | * TODO unfixed 30 seconds timeout by android button, (button action up
267 | * called by android itself) and will make next recording crash bug,
268 | * this bug now only found on Milstone2 and not procced. (no way to
269 | * prevent system button action up, It's not an human action up)
270 | */
271 | Log.i(TAG, "recording end occur");
272 | // stop sixty seconds limit
273 | if (sixtySecondsTimer != null) {
274 | sixtySecondsTimer.cancel();
275 | sixtySecondsTimer = null;
276 | }
277 | // stop volume task
278 | if (recordingTimer != null) {
279 | recordingTimer.cancel();
280 | recordingTimer = null;
281 | }
282 |
283 | String tfile = audioUtil.stopRecord();
284 | if (alertDialog != null) {
285 | alertDialog.dismiss();
286 | }
287 | if (mBegin == null) {
288 | if (start) {
289 | Toast.makeText(mContext, "录音时间太短或出错了,请将SD卡插好", 5000).show();
290 | // 删除录制的文件
291 | if (!(tfile == null || "".equals(tfile))) {
292 | File hey = new File(tfile);
293 | if (hey.exists()) {
294 | hey.delete();
295 | }
296 | }
297 | mInsideRecordListener.onStreamEnd();
298 | }
299 | start = false;
300 | return false;
301 | }
302 | start = false;
303 | Date now = new Date();
304 | if (now.getTime() - mBegin.getTime() < 1000) {
305 | mBegin = null;
306 | Toast.makeText(mContext, "录音时间太短了", 5000).show();
307 | (new File(tfile)).delete();
308 | // mInsideRecordListener.onRecordFail(null);
309 | audioRecorderListener.onRecordFail();
310 | mInsideRecordListener.onStreamEnd();
311 | return false;
312 | }
313 | if (tfile != null) {
314 | try {
315 | Date end = new Date();
316 | long fileTime = 0;
317 | try {
318 | fileTime = AudioUtil.getAudioDuration(tfile);
319 | } catch (IOException e) {
320 | e.printStackTrace();
321 | }
322 | long duration = fileTime == 0 ? end.getTime()
323 | - mBegin.getTime() : fileTime;
324 | // mInsideRecordListener.onRecordComplete(tfile, duration);
325 | audioRecorderListener.onRecordComplete(tfile, duration);
326 | mInsideRecordListener.onStreamEnd();
327 | mBegin = null;
328 | } catch (IllegalStateException e) {
329 | e.printStackTrace();
330 | }
331 | }
332 | return false;
333 | }
334 |
335 | class AudioRecorderInsideListener implements
336 | IRehearsalAudioRecorderListener {
337 |
338 | public void onStreamEnd() {
339 | if (waveConverter instanceof UnknownWaveConverter) {
340 | return;
341 | }
342 | try {
343 | if (output != null) {
344 | output.close();
345 | }
346 | if (waveConverter != null) {
347 | waveConverter.end();
348 | }
349 |
350 | if (input != null) {
351 | input.close();
352 | }
353 | } catch (IOException e) {
354 | e.printStackTrace();
355 | }
356 | }
357 |
358 | private void onConvertedBuffer(final byte[] buffer) {
359 | // Log.i(TAG, "onBuffer: " + buffer.length + " buffer 0: " +
360 | // buffer[0]);
361 | audioRecorderListener.onRecordConvertedBuffer(buffer);
362 | // TODO save to file
363 | }
364 |
365 | private PipedOutputStream output;
366 |
367 | private PipedInputStream input;
368 |
369 | class WaveConvertThread extends Thread {
370 | public void run() {
371 | Log.i(TAG, "writing buffer to ouput stream end.");
372 | if (waveConverter instanceof UnknownWaveConverter) {
373 | return;
374 | }
375 | if (waveConverter != null) {
376 | waveConverter.convert(new WaveConvertComplete() {
377 |
378 | @Override
379 | public void done(byte[] buffer) {
380 | onConvertedBuffer(buffer);
381 | }
382 | });
383 | }
384 | }
385 | }
386 |
387 | @Override
388 | public void onRecordBuffer(final byte[] buffer) {
389 | Log.i(TAG, "raw wave buffer size: " + buffer.length);
390 | if (waveConverter instanceof UnknownWaveConverter) {
391 | onConvertedBuffer(buffer);
392 | } else {
393 | try {
394 | output.write(buffer);
395 | } catch (IOException e) {
396 | e.printStackTrace();
397 | }
398 | }
399 | if (mRecordListener != null) {
400 | mRecordListener.onRecordBuffer(buffer);
401 | }
402 | }
403 |
404 | @Override
405 | public void onRecordStart() {
406 | if (waveConverter instanceof UnknownWaveConverter) {
407 | //
408 | } else {
409 | try {
410 | output = new PipedOutputStream();
411 | input = new PipedInputStream(output);
412 | if (waveConverter != null) {
413 | waveConverter.init(input);
414 | }
415 | new WaveConvertThread().start();
416 | } catch (IOException e) {
417 | Log.e(TAG, e.getMessage());
418 | e.printStackTrace();
419 | }
420 | }
421 | if (mRecordListener != null) {
422 | mRecordListener.onRecordStart();
423 | }
424 | }
425 |
426 | // @Override
427 | public void onRecordFail() {
428 | // remove wave file and amr file?TODO
429 | }
430 |
431 | @Override
432 | public void onRecordPrepared() {
433 | mHandler.post(new Runnable() {
434 |
435 | @Override
436 | public void run() {
437 | recordingText.setText("正在录音");
438 | }
439 | });
440 | }
441 |
442 | @Override
443 | public void onRecordStop() {
444 | if (mRecordListener != null) {
445 | mRecordListener.onRecordStop();
446 | }
447 | }
448 | }
449 |
450 | private AudioRecorderInsideListener mInsideRecordListener = new AudioRecorderInsideListener();
451 |
452 | private IRehearsalAudioRecorderListener mRecordListener;
453 |
454 | public void setOnRecordListener(IRehearsalAudioRecorderListener l) {
455 | this.mRecordListener = l;
456 | if (audioUtil != null) {
457 | audioUtil.setOnRecordListener(mRecordListener);
458 | }
459 | }
460 |
461 | public void setAudioUtil(AudioUtil audioUtil) {
462 | this.audioUtil = audioUtil;
463 | }
464 |
465 | public AudioUtil getAudioUtil() {
466 | return audioUtil;
467 | }
468 |
469 | public interface IAudioRecorderListener {
470 | void onRecordFail();// time not enough
471 |
472 | void onRecordComplete(String file, long duration);
473 |
474 | void onRecordConvertedBuffer(byte[] buffer);
475 | }
476 |
477 | private IAudioRecorderListener audioRecorderListener;
478 |
479 | public IAudioRecorderListener getAudioRecorderListener() {
480 | return audioRecorderListener;
481 | }
482 |
483 | public void setAudioRecorderListener(
484 | IAudioRecorderListener audioRecorderListener) {
485 | this.audioRecorderListener = audioRecorderListener;
486 | }
487 |
488 | public void setWaveConverter(WaveConverter waveConverter) {
489 | this.waveConverter = waveConverter;
490 | }
491 | }
492 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/AudioUtil.java:
--------------------------------------------------------------------------------
1 |
2 | package bz.tsung.media.audio;
3 |
4 | import java.io.File;
5 | import java.io.IOException;
6 | import java.util.ArrayList;
7 | import java.util.Calendar;
8 | import java.util.Iterator;
9 |
10 | import android.content.Context;
11 | import android.media.AudioFormat;
12 | import android.media.MediaPlayer;
13 | import android.media.MediaPlayer.OnCompletionListener;
14 | import android.media.MediaRecorder.AudioSource;
15 | import android.os.Environment;
16 | import android.os.Handler;
17 | import android.os.Looper;
18 | import android.os.Message;
19 | import android.util.Log;
20 | import bz.tsung.media.audio.RehearsalAudioRecorder.IRehearsalAudioRecorderListener;
21 |
22 | /**
23 | * 录音、播音的操作都是异步的,不用另起线程来执行
24 | *
25 | * @author mk
26 | */
27 | public class AudioUtil{
28 | // 录音的时侯,声音的最大幅度
29 | public final static int MAX_SAMPLING_VOLUME = 0x7FFF;
30 |
31 | private static final String TAG = "AudioUtil";
32 |
33 | private String mFilePath;
34 |
35 | private final String baseAudioPath = Environment.getExternalStorageDirectory().getPath()
36 | + "/.RealtimeAudioRecorder/";
37 |
38 | private Context mContext;
39 |
40 | private MediaPlayer mPlayer;
41 |
42 | private MediaPlayer mPlayerEnd;
43 |
44 | private static MediaPlayer mDurationPlayer = new MediaPlayer();
45 |
46 | private RehearsalAudioRecorder mRecorder;
47 |
48 | private ArrayList mOnCompletionListeners = new ArrayList();
49 |
50 | private ArrayList mOnStopListeners = new ArrayList();
51 |
52 | private boolean isRecording = false;
53 |
54 | private long mRecordTimeStamp = 0;
55 |
56 | public final static int STOP_REASON_RECORDING = 0;
57 |
58 | public final static int STOP_REASON_OTHER = 1;
59 |
60 | // 正在播放的文件名,多播放控制用
61 | public String playingFile = "";
62 |
63 | private Looper mCurrentLooper;
64 |
65 | class LooperThread extends Thread {
66 | public Handler mHandler;
67 |
68 | public void run() {
69 | Looper.prepare();
70 |
71 | // Log.i(TAG, "is null? " + ((Looper.myLooper() == null) ? "yes" :
72 | // "no"));
73 | // Log.i(TAG, "is null? " + ((Looper.myLooper() ==
74 | // Looper.getMainLooper()) ? "yes" : "no"));
75 | mCurrentLooper = Looper.myLooper();
76 | mRecorder = new RehearsalAudioRecorder(true, AudioSource.DEFAULT, 8000,
77 | AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
78 |
79 | mHandler = new Handler() {
80 | public void handleMessage(Message msg) {
81 | // process incoming messages here
82 | }
83 | };
84 |
85 | Looper.loop();
86 | }
87 | }
88 |
89 | public AudioUtil(Context context) {
90 | mContext = context;
91 | mPlayer = new MediaPlayer();
92 | new LooperThread().start();
93 | }
94 |
95 | /**
96 | * 播放结束时调用此函数
97 | *
98 | * @param l
99 | */
100 | public void setOnCompletionListener(OnCompletionListener l) {
101 | mOnCompletionListeners.add(l);
102 | }
103 |
104 | public void setOnStopListener(OnStopListener l) {
105 | mOnStopListeners.add(l);
106 | }
107 |
108 | public static long getAudioDuration(String fileName) throws IOException {
109 | long duration = 0;
110 | if (mDurationPlayer == null) {
111 | return duration;
112 | }
113 | try {
114 | mDurationPlayer.reset();
115 | mDurationPlayer.setDataSource(fileName);
116 | mDurationPlayer.prepare();
117 | duration = mDurationPlayer.getDuration();
118 | mDurationPlayer.stop();
119 | } catch (IOException e) {
120 | Log.e(TAG, "IOException:" + e.getMessage());
121 | throw e;
122 | } catch (IllegalStateException e) {
123 | Log.e(TAG, "getAudioDuration start playing IllegalStateException");
124 | throw e;
125 | }
126 | return duration;
127 | }
128 |
129 | /**
130 | * 开始录音
131 | */
132 | public void startRecord() throws IllegalStateException, IOException {
133 | if (mPlayer.isPlaying()) {
134 | stopPlaying(AudioUtil.STOP_REASON_RECORDING);
135 | }
136 |
137 | if (isRecording) { // 先停止录音
138 | stopRecording();
139 | }
140 |
141 | startRecording();
142 | }
143 |
144 | /**
145 | * 录音结束
146 | *
147 | * @return 录音文件的绝对路径。如果录音失败返回null
148 | */
149 | public String stopRecord() throws IllegalStateException {
150 | Log.i(TAG, "stopRecord");
151 | if (isRecording) {
152 | stopRecording();
153 | long interval = Calendar.getInstance().getTimeInMillis() - mRecordTimeStamp;
154 | if (interval < 1100) {
155 | // mContext.deleteFile(mFilePath);
156 | // return null;
157 | }
158 | return mFilePath;
159 | } else {
160 | return null;
161 | }
162 |
163 | }
164 |
165 | /**
166 | * 是否正在录音
167 | *
168 | * @return true-正在录音;false-录音结束
169 | */
170 | public synchronized boolean isRecording() {
171 | return isRecording;
172 | }
173 |
174 | /**
175 | * 开始播放录音
176 | *
177 | * @param path 录音文件的路径
178 | * @return START_SUCCESS:播放成功;(TODO:文件不存在,编码不支持等)
179 | * @throws IOException
180 | * @throws IllegalStateException
181 | */
182 | public void startPlay(final String fileName) throws IllegalStateException, IOException {
183 | if (fileName == null) {
184 | Log.e(TAG, "file name is null");
185 | return;
186 | }
187 | playingFile = fileName;
188 | if (isRecording) { // 停止录音
189 | stopRecording();
190 | }
191 |
192 | if (mPlayer == null) {
193 | return;
194 | }
195 | if (mPlayer.isPlaying()) { // 先停止当然的播放
196 | stopPlaying();
197 | }
198 |
199 | startPlaying(fileName);
200 | }
201 |
202 | /**
203 | * 手动停止播音(正常情况下会自己结束)
204 | */
205 | public void stopPlay() throws IllegalStateException {
206 | if (mPlayer != null && mPlayer.isPlaying()) {
207 | stopPlaying();
208 | }
209 | }
210 |
211 | /**
212 | * 是否正在播放录音
213 | *
214 | * @return true-正在播放录音
215 | */
216 | public synchronized boolean isPlaying() {
217 | return mPlayer != null && mPlayer.isPlaying();
218 | }
219 |
220 | /**
221 | * 释放录音,播音的资源。(可以在退出单个私聊界面的时侯释放,不必每次录音结束都调用。 释放完之后,这个实例将不可再用)
222 | */
223 | public void release() {
224 | if (mRecorder != null) {
225 | mRecorder.release();
226 | mRecorder = null;
227 | }
228 |
229 | if (mPlayer != null) {
230 | mPlayer.release();
231 | mPlayer = null;
232 | }
233 |
234 | if (mCurrentLooper != null) {
235 | mCurrentLooper.quit();
236 | mCurrentLooper = null;
237 | }
238 | }
239 |
240 | // public void resetRecorder() {
241 | // if (mRecorder != null) {
242 | // mRecorder.release();
243 | // mRecorder = null;
244 | // }
245 | //
246 | // new LooperThread().start();
247 | // }
248 |
249 | private void startPlaying(final String fileName)
250 | throws IllegalStateException, IOException {
251 | if (mPlayer == null) {
252 | return;
253 | }
254 | try {
255 | mPlayer.reset();
256 | mPlayer.setDataSource(fileName);
257 | mPlayer.prepare();
258 | mPlayer.start();
259 | Log.i(TAG, "start play");
260 | OnCompletionListener mOnCompletionListener = new OnCompletionListener() {
261 |
262 | @Override
263 | public void onCompletion(MediaPlayer arg0) {
264 | if (mPlayerEnd != null) {
265 | mPlayerEnd.release();
266 | }
267 | mPlayerEnd = MediaPlayer.create(mContext, R.raw.play_end);
268 | mPlayerEnd.start();
269 | for (Iterator itr = mOnCompletionListeners
270 | .iterator(); itr
271 | .hasNext();) {
272 | OnCompletionListener curr = itr.next();
273 | curr.onCompletion(arg0);
274 | }
275 | }
276 | };
277 | mPlayer.setOnCompletionListener(mOnCompletionListener);
278 | } catch (IOException e) {
279 | Log.e(TAG, "IOException");
280 | throw e;
281 | } catch (IllegalStateException e) {
282 | Log.e(TAG, "start playing IllegalStateException");
283 | throw e;
284 | }
285 | }
286 |
287 | private void stopPlaying() throws IllegalStateException {
288 | stopPlaying(AudioUtil.STOP_REASON_OTHER);
289 | }
290 |
291 | private void stopPlaying(int reason) throws IllegalStateException {
292 | if (mPlayer != null) {
293 | try {
294 | Log.i(TAG, "_stop play");
295 | mPlayer.stop();
296 | for (Iterator itr = mOnStopListeners.iterator(); itr
297 | .hasNext();) {
298 | OnStopListener curr = itr.next();
299 | curr.onStop(reason);
300 | }
301 | } catch (IllegalStateException e) {
302 | Log.e(TAG, "stop playing IllegalStateException");
303 | throw e;
304 | }
305 | }
306 | }
307 |
308 | private void startRecording() throws IllegalStateException, IOException {
309 | if (mRecorder != null) {
310 | // 获取录音文件存放的路径
311 | Calendar calendar = Calendar.getInstance();
312 | long timestamp = calendar.getTimeInMillis();
313 | String fileName = String.valueOf(timestamp);
314 | File destDir = new File(baseAudioPath);
315 | if (!destDir.exists()) {
316 | destDir.mkdirs();
317 | }
318 | mFilePath = baseAudioPath + fileName;
319 | Log.i(TAG, "file path:" + mFilePath);
320 | try { // 准备并开始录音
321 | // mRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
322 | // mRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
323 | // mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
324 | mRecorder.setOutputFile(mFilePath);
325 | mRecorder.prepare();
326 | mRecorder.start();
327 | mRecordTimeStamp = Calendar.getInstance().getTimeInMillis();
328 | // 监听录音时的声音振幅
329 | Log.i(TAG, "_start record");
330 | } catch (IOException e) {
331 | Log.e(TAG, "IOException");
332 | throw e;
333 | } catch (IllegalStateException e) {
334 | Log.e(TAG, "start recording IllegalStateException");
335 | throw e;
336 | }
337 |
338 | isRecording = true;
339 | }
340 | }
341 |
342 | private void stopRecording() throws IllegalStateException {
343 | if (mRecorder != null) {
344 | try {
345 | Log.i(TAG, "_stop record");
346 | isRecording = false;
347 | mRecorder.stop();
348 | // mRecorder.reset();
349 | } catch (IllegalStateException e) {
350 | Log.e(TAG, "stop recording IllegalStateException");
351 | throw e;
352 | } catch (Exception e) {
353 | Log.e(TAG, "exception:" + e.getMessage());
354 | }
355 | }
356 | // if (mCurrentLooper != null) {
357 | // mCurrentLooper.quit();
358 | // mCurrentLooper = null;
359 | // }
360 | }
361 |
362 | public int getMaxAmplitude() {
363 | if (isRecording && mRecorder != null) {
364 | return mRecorder.getMaxAmplitude();
365 | } else {
366 | return 0;
367 | }
368 | }
369 |
370 | public interface OnStopListener {
371 | void onStop(int reason);
372 | }
373 |
374 | public interface RecordAmplitudeListener {
375 | void onMessage(int amplitude);
376 | }
377 |
378 | public void setOnRecordListener(IRehearsalAudioRecorderListener l) {
379 | if (mRecorder != null) {
380 | mRecorder.setOnRecordListener(l);
381 | }
382 | }
383 | }
384 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/IAudioRecorder.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.media.audio;
2 |
3 | public interface IAudioRecorder {
4 | void createWaveConverter();
5 | }
6 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/RehearsalAudioRecorder.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.media.audio;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.io.RandomAccessFile;
6 |
7 | import android.media.AudioFormat;
8 | import android.media.AudioRecord;
9 | import android.media.MediaRecorder;
10 | import android.util.Log;
11 |
12 | public class RehearsalAudioRecorder {
13 | private final static String TAG = "RehearsalAudioRecorder";
14 |
15 | /**
16 | * INITIALIZING : recorder is initializing; READY : recorder has been
17 | * initialized, recorder not yet started RECORDING : recording ERROR :
18 | * reconstruction needed STOPPED: reset needed
19 | */
20 | public enum State {
21 | INITIALIZING, READY, RECORDING, ERROR, STOPPED
22 | };
23 |
24 | public static final boolean RECORDING_UNCOMPRESSED = true;
25 |
26 | public static final boolean RECORDING_COMPRESSED = false;
27 |
28 | // The interval in which the recorded samples are output to the file
29 | // Used only in uncompressed mode
30 | private static final int TIMER_INTERVAL = 20;
31 |
32 | // private static final int TIMER_INTERVAL = 50;
33 |
34 | // Toggles uncompressed recording on/off; RECORDING_UNCOMPRESSED /
35 | // RECORDING_COMPRESSED
36 | private boolean rUncompressed;
37 |
38 | // Recorder used for uncompressed recording
39 | private AudioRecord mAudioRecord = null;
40 |
41 | // Recorder used for compressed recording
42 | private MediaRecorder mRecorder = null;
43 |
44 | // Stores current amplitude (only in uncompressed mode)
45 | private int cAmplitude = 0;
46 |
47 | // Output file path
48 | private String fPath = null;
49 |
50 | // Recorder state; see State
51 | private State state;
52 |
53 | // File writer (only in uncompressed mode)
54 | private RandomAccessFile fWriter;
55 |
56 | // Number of channels, sample rate, sample size(size in bits), buffer size,
57 | // audio source, sample size(see AudioFormat)
58 | private short nChannels;
59 |
60 | private int sRate;
61 |
62 | private short bSamples;
63 |
64 | private int mBufferSize;
65 |
66 | // private int mAudioSource;
67 | //
68 | // private int mAudioFormat;
69 |
70 | // Number of frames written to file on each output(only in uncompressed
71 | // mode)
72 | private int framePeriod;
73 |
74 | // Buffer for output(only in uncompressed mode)
75 | private byte[] buffer;
76 |
77 | // Number of bytes written to file after header(only in uncompressed mode)
78 | // after stop() is called, this size is written to the header/data chunk in
79 | // the wave file
80 | private int payloadSize;
81 |
82 | /*
83 | * Method used for recording.
84 | */
85 | private final AudioRecord.OnRecordPositionUpdateListener updateListener = new AudioRecord.OnRecordPositionUpdateListener() {
86 | @Override
87 | public void onMarkerReached(AudioRecord recorder) {
88 | // NOT USED
89 | }
90 |
91 | // private int i = 0;
92 |
93 | @Override
94 | public void onPeriodicNotification(AudioRecord recorder) {
95 | Log.i(TAG, "onPeriodicNotification");
96 | // 读缓冲区
97 |
98 | // if (i++ > 100) {
99 | // stop();
100 | // Log.i(TAG, "stop automaull");
101 | // return;
102 | // }
103 | int count = mAudioRecord.read(buffer, 0, buffer.length); // Fill
104 | // buffer
105 | Log.i(TAG, "on buffer: " + count + " orig: " + buffer.length);
106 |
107 | mOnRecordListener.onRecordBuffer(buffer);
108 | try {
109 | // 将缓冲区数据写入文件
110 | fWriter.write(buffer); // Write buffer to file
111 | payloadSize += buffer.length;
112 | // 计算振幅
113 |
114 | if (bSamples == 16) {
115 | // int i = buffer.length / 4;
116 | // cAmplitude = getShort(buffer[i * 2],
117 | // buffer[i * 2 + 1]);
118 | for (int i = 0; i < buffer.length / 2; i++) { // 16bit
119 | // sample
120 | // size
121 | final short curSample = getShort(buffer[i * 2],
122 | buffer[i * 2 + 1]);
123 | if (curSample > cAmplitude) { // Check amplitude
124 | cAmplitude = curSample;
125 | }
126 | }
127 | } else { // 8bit sample size
128 | for (int i = 0; i < buffer.length; i++) {
129 | if (buffer[i] > cAmplitude) { // Check amplitude
130 | cAmplitude = buffer[i];
131 | }
132 | }
133 | // cAmplitude = buffer[buffer.length / 2];
134 | }
135 | Log.i(TAG, "onPeriodicNotification end");
136 | } catch (final IOException e) {
137 | Log.e(TAG,
138 | "Error occured in updateListener, recording is aborted");
139 | stop();
140 | }
141 | }
142 | };
143 |
144 | /**
145 | * Default constructor Instantiates a new recorder, in case of compressed
146 | * recording the parameters can be left as 0. In case of errors, no
147 | * exception is thrown, but the state is set to ERROR
148 | *
149 | * @param uncompressed
150 | * whether compress.if true the record would be compressed,else
151 | * not.
152 | * @param audioSource
153 | * Sets the audio source to be used for recording.It could be
154 | * AudioSource.MIC etc.
155 | * @param sampleRate
156 | * sample rate expressed in Hertz. Examples of rates are (but not
157 | * limited to) 44100, 22050 and 11025. IMPORTANT, here should set
158 | * 8000HZ to get normal audio record
159 | * @param channelConfig
160 | * describes the configuration of the audio channels. See
161 | * CHANNEL_IN_MONO and CHANNEL_IN_STEREO
162 | * @param audioFormat
163 | * the format in which the audio data is represented. See
164 | * ENCODING_PCM_16BIT and ENCODING_PCM_8BIT
165 | */
166 | public RehearsalAudioRecorder(boolean uncompressed, int audioSource,
167 | int sampleRate, int channelConfig, int audioFormat) {
168 | try {
169 | rUncompressed = uncompressed;
170 | if (rUncompressed) { // RECORDING_UNCOMPRESSED
171 | if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
172 | bSamples = 16;
173 | } else {
174 | bSamples = 8;
175 | }
176 | if (channelConfig == AudioFormat.CHANNEL_CONFIGURATION_MONO) {
177 | nChannels = 2;
178 | } else {
179 | nChannels = 1;
180 | }
181 |
182 | // mAudioSource = audioSource;
183 | sRate = sampleRate;
184 | // mAudioFormat = audioFormat;
185 |
186 | // TIMER_INTERVAL时间内的采样次数,(帧大小)
187 | framePeriod = sampleRate * TIMER_INTERVAL / 1000;
188 | // 存储空间大小,2倍于最佳值
189 | mBufferSize = framePeriod * 2 * bSamples * nChannels / 8;
190 | int minBufferSize = AudioRecord.getMinBufferSize(sampleRate,
191 | channelConfig, audioFormat);
192 | if (mBufferSize < minBufferSize) { // Check to make sure
193 | // buffer size is not
194 | // smaller than the
195 | // smallest allowed one
196 | mBufferSize = AudioRecord.getMinBufferSize(sampleRate,
197 | channelConfig, audioFormat);
198 | // Set frame period and timer interval accordingly
199 | framePeriod = mBufferSize / (2 * bSamples * nChannels / 8);
200 | Log.w(TAG,
201 | "Increasing buffer size to "
202 | + Integer.toString(mBufferSize));
203 | }
204 |
205 | mAudioRecord = new AudioRecord(audioSource, sampleRate,
206 | channelConfig, audioFormat, mBufferSize);
207 |
208 | if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
209 | throw new IllegalArgumentException(
210 | "AudioRecord initialization failed");
211 | }
212 |
213 | mAudioRecord.setPositionNotificationPeriod(framePeriod);
214 | //
215 | mAudioRecord.setRecordPositionUpdateListener(updateListener);
216 |
217 | } else { // RECORDING_COMPRESSED
218 | mRecorder = new MediaRecorder();
219 | mRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
220 | mRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
221 | mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
222 | }
223 |
224 | cAmplitude = 0;
225 | fPath = null;
226 | state = State.INITIALIZING;
227 | } catch (final IllegalStateException e) {
228 | if (e.getMessage() != null) {
229 | Log.e(TAG, e.getMessage());
230 | } else {
231 | Log.e(TAG, "Unknown error occured while initializing recording");
232 | }
233 | state = State.ERROR;
234 | }
235 | }
236 |
237 | /**
238 | * Returns the largest amplitude sampled since the last call to this method.
239 | *
240 | * @return returns the largest amplitude since the last call, or 0 when not
241 | * in recording state.
242 | */
243 | public int getMaxAmplitude() {
244 | if (state == State.RECORDING) {
245 | if (rUncompressed) {
246 | final int result = cAmplitude;
247 | cAmplitude = 0;
248 | return result;
249 | } else {
250 | try {
251 | return mRecorder.getMaxAmplitude();
252 | } catch (final IllegalStateException e) {
253 | return 0;
254 | }
255 | }
256 | } else
257 | return 0;
258 | }
259 |
260 | /*
261 | * Converts a byte[2] to a short, in LITTLE_ENDIAN format
262 | */
263 | private short getShort(byte argB1, byte argB2) {
264 | return (short) (argB1 | (argB2 << 8));
265 | }
266 |
267 | /**
268 | * Returns the state of the recorder in a RehearsalAudioRecord.State typed
269 | * object. Useful, as no exceptions are thrown.
270 | *
271 | * @return recorder state
272 | */
273 | public State getState() {
274 | return state;
275 | }
276 |
277 | /**
278 | * Prepares the recorder for recording, in case the recorder is not in the
279 | * INITIALIZING state and the file path was not set the recorder is set to
280 | * the ERROR state, which makes a reconstruction necessary. In case
281 | * uncompressed recording is toggled, the header of the wave file is
282 | * written. In case of an exception, the state is changed to ERROR
283 | */
284 | public void prepare() throws IOException {
285 | try {
286 | if (state == State.INITIALIZING) {
287 | if (rUncompressed) {
288 | if ((mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED)
289 | & (fPath != null)) {
290 | // write file header
291 |
292 | fWriter = new RandomAccessFile(fPath, "rw");
293 |
294 | fWriter.setLength(0); // Set file length to 0, to
295 | // prevent unexpected behavior
296 | // in case the file already
297 | // existed
298 | fWriter.writeBytes("RIFF");
299 | fWriter.writeInt(0); // Final file size not known yet,
300 | // write 0
301 | fWriter.writeBytes("WAVE");
302 | fWriter.writeBytes("fmt ");
303 | fWriter.writeInt(Integer.reverseBytes(16)); // Sub-chunk
304 | // size, 16
305 | // for PCM
306 | fWriter.writeShort(Short.reverseBytes((short) 1)); // AudioFormat,
307 | // 1
308 | // for
309 | // PCM
310 | fWriter.writeShort(Short.reverseBytes(nChannels));// Number
311 | // of
312 | // channels,
313 | // 1
314 | // for
315 | // mono,
316 | // 2
317 | // for
318 | // stereo
319 | fWriter.writeInt(Integer.reverseBytes(sRate)); // Sample
320 | // rate
321 | fWriter.writeInt(Integer.reverseBytes(sRate * bSamples
322 | * nChannels / 8)); // Byte rate,
323 | // SampleRate*NumberOfChannels*BitsPerSample/8
324 | fWriter.writeShort(Short
325 | .reverseBytes((short) (nChannels * bSamples / 8))); // Block
326 | // align,
327 | // NumberOfChannels*BitsPerSample/8
328 | fWriter.writeShort(Short.reverseBytes(bSamples)); // Bits
329 | // per
330 | // sample
331 | fWriter.writeBytes("data");
332 | fWriter.writeInt(0); // Data chunk size not known yet,
333 | // write 0
334 |
335 | buffer = new byte[framePeriod * bSamples / 8
336 | * nChannels];
337 | state = State.READY;
338 | } else {
339 | Log.e(TAG,
340 | "prepare() method called on uninitialized recorder");
341 | state = State.ERROR;
342 | }
343 | } else {
344 | mRecorder.prepare();
345 | state = State.READY;
346 | }
347 | } else {
348 | Log.e(TAG, "prepare() method called on illegal state");
349 | release();
350 | state = State.ERROR;
351 | }
352 | } catch (IOException ioe) {
353 | if (ioe.getMessage() != null) {
354 | Log.e(TAG, ioe.getMessage());
355 | } else {
356 | Log.e(TAG, "Unknown error occured in prepare()");
357 | }
358 | state = State.ERROR;
359 | throw ioe;
360 | }
361 | mOnRecordListener.onRecordPrepared();
362 | }
363 |
364 | /**
365 | * Releases the resources associated with this class, and removes the
366 | * unnecessary files, when necessary
367 | */
368 | public void release() {
369 | if (state == State.RECORDING) {
370 | stop();
371 | } else {
372 | if ((state == State.READY) & (rUncompressed)) {
373 | try {
374 | fWriter.close(); // Remove prepared file
375 | } catch (final IOException e) {
376 | Log.e(TAG,
377 | "I/O exception occured while closing output file");
378 | }
379 | (new File(fPath)).delete();
380 | }
381 | }
382 |
383 | if (rUncompressed) {
384 | if (mAudioRecord != null) {
385 | mAudioRecord.release();
386 | }
387 | } else {
388 | if (mRecorder != null) {
389 | mRecorder.release();
390 | }
391 | }
392 | }
393 |
394 | /**
395 | * Resets the recorder to the INITIALIZING state, as if it was just created.
396 | * In case the class was in RECORDING state, the recording is stopped. In
397 | * case of exceptions the class is set to the ERROR state.
398 | */
399 | public void reset() {
400 | state = State.INITIALIZING;
401 | try {
402 | if (state != State.ERROR) {
403 | // release();
404 | fPath = null; // Reset file path
405 | cAmplitude = 0; // Reset amplitude
406 | if (rUncompressed) {
407 | // mAudioRecord = new AudioRecord(mAudioSource, sRate,
408 | // nChannels + 1,
409 | // mAudioFormat, mBufferSize);
410 | //
411 | // if (mAudioRecord.getState() !=
412 | // AudioRecord.STATE_INITIALIZED) {
413 | // throw new
414 | // IllegalArgumentException("AudioRecord initialization failed");
415 | // }
416 | // mAudioRecord.setPositionNotificationPeriod(framePeriod);
417 | // //
418 | // mAudioRecord.setRecordPositionUpdateListener(updateListener);
419 | } else {
420 | // mRecorder = new MediaRecorder();
421 | // mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
422 | // mRecorder
423 | // .setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
424 | // mRecorder
425 | // .setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
426 | }
427 | state = State.INITIALIZING;
428 | }
429 | } catch (final IllegalStateException e) {
430 | Log.e(TAG, e.getMessage());
431 | state = State.ERROR;
432 | }
433 | }
434 |
435 | /**
436 | * Sets output file path, call directly after construction/reset.
437 | *
438 | * @param output
439 | * file path
440 | */
441 | public void setOutputFile(String argPath) {
442 | try {
443 | if (state == State.INITIALIZING) {
444 | fPath = argPath;
445 | if (!rUncompressed) {
446 | mRecorder.setOutputFile(fPath);
447 | }
448 | }
449 | } catch (final IllegalStateException e) {
450 | if (e.getMessage() != null) {
451 | Log.e(TAG, e.getMessage());
452 | } else {
453 | Log.e(TAG, "Unknown error occured while setting output path");
454 | }
455 | state = State.ERROR;
456 | }
457 | }
458 |
459 | /**
460 | * Starts the recording, and sets the state to RECORDING. Call after
461 | * prepare().
462 | */
463 | public void start() {
464 | mOnRecordListener.onRecordStart();
465 | if (state == State.READY) {
466 | if (rUncompressed) {
467 | payloadSize = 0;
468 | Log.i(TAG, "before recording");
469 | try {
470 | mAudioRecord.startRecording();
471 | } catch (Exception e) {
472 | Log.e(TAG,
473 | "recording start error: "
474 | + (e == null ? "" : e.getMessage()));
475 | }
476 | Log.i(TAG, "after recording");
477 | mAudioRecord.read(buffer, 0, buffer.length);
478 | } else {
479 | mRecorder.start();
480 | }
481 | state = State.RECORDING;
482 | } else {
483 | Log.e(TAG, "start() called on illegal state");
484 | state = State.ERROR;
485 | }
486 | }
487 |
488 | /**
489 | * Stops the recording, and sets the state to STOPPED. In case of further
490 | * usage, a reset is needed. Also finalizes the wave file in case of
491 | * uncompressed recording.
492 | */
493 | public void stop() {
494 | mOnRecordListener.onRecordStop();
495 | if (state == State.RECORDING) {
496 | if (rUncompressed) {
497 | Log.d(TAG, "stop legal");
498 | mAudioRecord.stop();
499 | Log.d(TAG, "stop legal after");
500 | // mAudioRecord.release();
501 |
502 | try {
503 | fWriter.seek(4); // Write size to RIFF header
504 | fWriter.writeInt(Integer.reverseBytes(36 + payloadSize));
505 |
506 | fWriter.seek(40); // Write size to Subchunk2Size field
507 | fWriter.writeInt(Integer.reverseBytes(payloadSize));
508 |
509 | fWriter.close();
510 | } catch (final IOException e) {
511 | Log.e(TAG,
512 | "I/O exception occured while closing output file");
513 | state = State.ERROR;
514 | }
515 | } else {
516 | mRecorder.stop();
517 | }
518 | state = State.STOPPED;
519 | } else {
520 | Log.e(TAG, "stop() called on illegal state" + state);
521 | state = State.ERROR;
522 | }
523 | reset();
524 | }
525 |
526 | /**
527 | * interface for wave callback
528 | * @date Nov 22, 2012
529 | * @author Tsung Wu
530 | *
531 | */
532 | public interface IRehearsalAudioRecorderListener {
533 |
534 | void onRecordPrepared();
535 |
536 | void onRecordStart();
537 |
538 | void onRecordStop();
539 |
540 | void onRecordBuffer(byte[] buffer);
541 |
542 | }
543 |
544 | private IRehearsalAudioRecorderListener mOnRecordListenerOut;
545 |
546 | public void setOnRecordListener(IRehearsalAudioRecorderListener l) {
547 | mOnRecordListenerOut = l;
548 | }
549 |
550 | private IRehearsalAudioRecorderListener mOnRecordListener = new IRehearsalAudioRecorderListener() {
551 |
552 | @Override
553 | public void onRecordPrepared() {
554 | if(mOnRecordListenerOut != null) {
555 | mOnRecordListenerOut.onRecordPrepared();
556 | }
557 | }
558 |
559 | @Override
560 | public void onRecordStart() {
561 | if(mOnRecordListenerOut != null) {
562 | mOnRecordListenerOut.onRecordStart();
563 | }
564 | }
565 |
566 | @Override
567 | public void onRecordStop() {
568 | if(mOnRecordListenerOut != null) {
569 | mOnRecordListenerOut.onRecordStop();
570 | }
571 | }
572 |
573 | @Override
574 | public void onRecordBuffer(byte[] buffer) {
575 | if(mOnRecordListenerOut != null) {
576 | mOnRecordListenerOut.onRecordBuffer(buffer);
577 | }
578 | }};
579 | }
580 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/converters/AmrWaveConverter.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.media.audio.converters;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 |
7 | import android.media.AmrInputStream;
8 | import android.util.Log;
9 |
10 | public class AmrWaveConverter implements WaveConverter {
11 | private static final String TAG = "AmrWaveConverter";
12 | private AmrInputStream ais;
13 |
14 | @Override
15 | public void init(InputStream input) {
16 | ais = new AmrInputStream(input);
17 | }
18 |
19 | @Override
20 | public void end() {
21 | if (ais != null) {
22 | try {
23 | ais.close();
24 | } catch (IOException e) {
25 | e.printStackTrace();
26 | }
27 | }
28 | }
29 |
30 | @Override
31 | public void convert(WaveConvertComplete complete) {
32 | ByteArrayOutputStream os = new ByteArrayOutputStream();
33 | byte[] tmp = new byte[32];
34 | try {
35 | Log.i(TAG, "trying to convert amr");
36 | int len = 0;
37 | while ((len = ais.read(tmp)) > 0) {
38 | Log.i(TAG, "ais length: " + len);
39 | os.write(tmp, 0, len);
40 | Log.i(TAG, "os length: " + os.size());
41 | if (os.size() >= 800) {
42 | byte[] amr = os.toByteArray();
43 | Log.i(TAG, "amr buffer size: " + amr.length);
44 | os.reset();
45 | complete.done(amr);
46 | }
47 | }
48 | byte[] amr = os.toByteArray();
49 | Log.i(TAG, "amr buffer size ending....: " + amr.length);
50 | os.close();
51 | complete.done(amr);
52 | } catch (Exception e) {
53 | Log.e(TAG, "converting" + e.getMessage());
54 | e.printStackTrace();
55 | if (os.size() > 0) {
56 | byte[] amr = os.toByteArray();
57 | Log.i(TAG, "amr buffer size ending....broken: "
58 | + amr.length);
59 | complete.done(amr);
60 | }
61 | try {
62 | os.close();
63 | } catch (IOException e1) {
64 | e1.printStackTrace();
65 | }
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/converters/UnknownWaveConverter.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.media.audio.converters;
2 |
3 | import java.io.InputStream;
4 |
5 | public class UnknownWaveConverter implements WaveConverter {
6 |
7 | @Override
8 | public void init(InputStream input) {
9 | // TODO Auto-generated method stub
10 |
11 | }
12 |
13 | @Override
14 | public void end() {
15 | // TODO Auto-generated method stub
16 |
17 | }
18 |
19 | @Override
20 | public void convert(WaveConvertComplete complete) {
21 | // TODO Auto-generated method stub
22 |
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/bz/tsung/media/audio/converters/WaveConverter.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.media.audio.converters;
2 |
3 | import java.io.InputStream;
4 |
5 | public interface WaveConverter {
6 | void init(InputStream input);
7 |
8 | void end();
9 |
10 | void convert(WaveConvertComplete complete);
11 |
12 | public interface WaveConvertComplete {
13 | void done(byte[] buffer);
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/bz/tsung/utils/android/DeviceUtil.java:
--------------------------------------------------------------------------------
1 | package bz.tsung.utils.android;
2 |
3 | import java.io.File;
4 |
5 | import android.os.Environment;
6 | import android.os.StatFs;
7 |
8 | public class DeviceUtil {
9 | public static boolean isFullStorage() {
10 | if (!isMediaMounted())
11 | return true;
12 |
13 | File path = Environment.getExternalStorageDirectory();
14 | // 取得sdcard文件路径
15 | StatFs statfs = new StatFs(path.getPath());
16 | // 获取block的SIZE
17 | long blocSize = statfs.getBlockSize();
18 | // 己使用的Block的数量
19 | long availaBlock = statfs.getAvailableBlocks();
20 | return availaBlock * blocSize < 1048576;
21 | }
22 |
23 | public static boolean isMediaMounted() {
24 | return Environment.getExternalStorageState().equals(
25 | Environment.MEDIA_MOUNTED);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------