├── .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 | --------------------------------------------------------------------------------