├── Audio Trimmer ├── .gitignore ├── .idea │ ├── code-comments.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ └── runConfigurations.xml ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── demo │ │ │ └── audiotrimmer │ │ │ ├── AudioTrimmerActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── customAudioViews │ │ │ ├── MP4Header.java │ │ │ ├── MarkerView.java │ │ │ ├── SamplePlayer.java │ │ │ ├── SongMetadataReader.java │ │ │ ├── SoundFile.java │ │ │ ├── WAVHeader.java │ │ │ └── WaveformView.java │ │ │ └── utils │ │ │ └── Utility.java │ │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_audiohandle.png │ │ ├── ic_audiostartrecord.png │ │ ├── ic_crop_btn_fill.png │ │ ├── ic_edit_btn.png │ │ ├── ic_pause_btn.png │ │ ├── ic_play_btn.png │ │ ├── ic_record_btn1.png │ │ ├── ic_refresh_btn.png │ │ └── ic_stop_btn1.png │ │ ├── drawable-xhdpi │ │ ├── ic_audiohandle.png │ │ ├── ic_audiostartrecord.png │ │ ├── ic_crop_btn_fill.png │ │ ├── ic_edit_btn.png │ │ ├── ic_pause_btn.png │ │ ├── ic_play_btn.png │ │ ├── ic_record_btn1.png │ │ ├── ic_refresh_btn.png │ │ └── ic_stop_btn1.png │ │ ├── drawable-xxhdpi │ │ ├── ic_audiohandle.png │ │ ├── ic_audiostartrecord.png │ │ ├── ic_crop_btn_fill.png │ │ ├── ic_edit_btn.png │ │ ├── ic_pause_btn.png │ │ ├── ic_play_btn.png │ │ ├── ic_record_btn1.png │ │ ├── ic_refresh_btn.png │ │ └── ic_stop_btn1.png │ │ ├── drawable │ │ ├── marker_left.xml │ │ └── marker_right.xml │ │ ├── layout │ │ ├── activity_audio_trim.xml │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── LICENSE ├── README.md └── Screenshots ├── .DS_Store ├── audio_trimmer.png ├── audiotrimmer_.gif └── logo.jpg /Audio Trimmer/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /Audio Trimmer/.idea/code-comments.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Audio Trimmer/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /Audio Trimmer/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /Audio Trimmer/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Audio Trimmer/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /Audio Trimmer/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Audio Trimmer/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "com.demo.audiotrimmer" 8 | minSdkVersion 18 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.3.1' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /Audio Trimmer/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android1\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/AudioTrimmerActivity.java: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2018 Intuz Pvt Ltd. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 6 | // (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 11 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 12 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | 15 | package com.demo.audiotrimmer; 16 | 17 | 18 | import android.app.ProgressDialog; 19 | import android.content.ContentValues; 20 | import android.content.Intent; 21 | import android.net.Uri; 22 | import android.os.Bundle; 23 | import android.os.Environment; 24 | import android.os.Handler; 25 | import android.provider.MediaStore; 26 | import android.support.annotation.Nullable; 27 | import android.support.v7.app.AppCompatActivity; 28 | import android.util.DisplayMetrics; 29 | import android.util.Log; 30 | import android.view.View; 31 | import android.widget.LinearLayout; 32 | import android.widget.RelativeLayout; 33 | import android.widget.TextView; 34 | import android.widget.Toast; 35 | 36 | import com.demo.audiotrimmer.customAudioViews.MarkerView; 37 | import com.demo.audiotrimmer.customAudioViews.SamplePlayer; 38 | import com.demo.audiotrimmer.customAudioViews.SoundFile; 39 | import com.demo.audiotrimmer.customAudioViews.WaveformView; 40 | import com.demo.audiotrimmer.utils.Utility; 41 | 42 | import java.io.File; 43 | import java.io.RandomAccessFile; 44 | import java.util.Locale; 45 | 46 | public class AudioTrimmerActivity extends AppCompatActivity implements View.OnClickListener, 47 | MarkerView.MarkerListener, 48 | WaveformView.WaveformListener { 49 | 50 | /* Audio trimmer*/ 51 | 52 | private TextView txtAudioCancel; 53 | private TextView txtAudioUpload; 54 | private TextView txtStartPosition; 55 | private TextView txtEndPosition; 56 | private LinearLayout llAudioCapture; 57 | private TextView txtAudioRecord; 58 | private TextView txtAudioRecordTime; 59 | private RelativeLayout rlAudioEdit; 60 | private MarkerView markerStart; 61 | private MarkerView markerEnd; 62 | private WaveformView audioWaveform; 63 | private TextView txtAudioRecordTimeUpdate; 64 | private TextView txtAudioReset; 65 | private TextView txtAudioDone; 66 | private TextView txtAudioPlay; 67 | private TextView txtAudioRecordUpdate; 68 | private TextView txtAudioCrop; 69 | 70 | private boolean isAudioRecording = false; 71 | private long mRecordingLastUpdateTime; 72 | private double mRecordingTime; 73 | private boolean mRecordingKeepGoing; 74 | private SoundFile mLoadedSoundFile; 75 | private SoundFile mRecordedSoundFile; 76 | private SamplePlayer mPlayer; 77 | 78 | private Handler mHandler; 79 | 80 | private boolean mTouchDragging; 81 | private float mTouchStart; 82 | private int mTouchInitialOffset; 83 | private int mTouchInitialStartPos; 84 | private int mTouchInitialEndPos; 85 | private float mDensity; 86 | private int mMarkerLeftInset; 87 | private int mMarkerRightInset; 88 | private int mMarkerTopOffset; 89 | private int mMarkerBottomOffset; 90 | 91 | private int mTextLeftInset; 92 | private int mTextRightInset; 93 | private int mTextTopOffset; 94 | private int mTextBottomOffset; 95 | 96 | private int mOffset; 97 | private int mOffsetGoal; 98 | private int mFlingVelocity; 99 | private int mPlayEndMillSec; 100 | private int mWidth; 101 | private int mMaxPos; 102 | private int mStartPos; 103 | private int mEndPos; 104 | 105 | private boolean mStartVisible; 106 | private boolean mEndVisible; 107 | private int mLastDisplayedStartPos; 108 | private int mLastDisplayedEndPos; 109 | private boolean mIsPlaying = false; 110 | private boolean mKeyDown; 111 | private ProgressDialog mProgressDialog; 112 | private long mLoadingLastUpdateTime; 113 | private boolean mLoadingKeepGoing; 114 | private File mFile; 115 | 116 | 117 | public AudioTrimmerActivity() { 118 | } 119 | 120 | @Override 121 | protected void onCreate(@Nullable Bundle savedInstanceState) { 122 | super.onCreate(savedInstanceState); 123 | setContentView(R.layout.activity_audio_trim); 124 | 125 | mHandler = new Handler(); 126 | 127 | txtAudioCancel = (TextView) findViewById(R.id.txtAudioCancel); 128 | txtAudioUpload = (TextView) findViewById(R.id.txtAudioUpload); 129 | txtStartPosition = (TextView) findViewById(R.id.txtStartPosition); 130 | txtEndPosition = (TextView) findViewById(R.id.txtEndPosition); 131 | llAudioCapture = (LinearLayout) findViewById(R.id.llAudioCapture); 132 | txtAudioRecord = (TextView) findViewById(R.id.txtAudioRecord); 133 | txtAudioRecordTime = (TextView) findViewById(R.id.txtAudioRecordTime); 134 | rlAudioEdit = (RelativeLayout) findViewById(R.id.rlAudioEdit); 135 | markerStart = (MarkerView) findViewById(R.id.markerStart); 136 | markerEnd = (MarkerView) findViewById(R.id.markerEnd); 137 | audioWaveform = (WaveformView) findViewById(R.id.audioWaveform); 138 | txtAudioRecordTimeUpdate = (TextView) findViewById(R.id.txtAudioRecordTimeUpdate); 139 | txtAudioReset = (TextView) findViewById(R.id.txtAudioReset); 140 | txtAudioDone = (TextView) findViewById(R.id.txtAudioDone); 141 | txtAudioPlay = (TextView) findViewById(R.id.txtAudioPlay); 142 | txtAudioRecordUpdate = (TextView) findViewById(R.id.txtAudioRecordUpdate); 143 | txtAudioCrop = (TextView) findViewById(R.id.txtAudioCrop); 144 | 145 | mRecordedSoundFile = null; 146 | mKeyDown = false; 147 | audioWaveform.setListener(this); 148 | 149 | markerStart.setListener(this); 150 | markerStart.setAlpha(1f); 151 | markerStart.setFocusable(true); 152 | markerStart.setFocusableInTouchMode(true); 153 | mStartVisible = true; 154 | 155 | markerEnd.setListener(this); 156 | markerEnd.setAlpha(1f); 157 | markerEnd.setFocusable(true); 158 | markerEnd.setFocusableInTouchMode(true); 159 | mEndVisible = true; 160 | 161 | DisplayMetrics metrics = new DisplayMetrics(); 162 | getWindowManager().getDefaultDisplay().getMetrics(metrics); 163 | mDensity = metrics.density; 164 | 165 | /** 166 | * Change this for marker handle as per your view 167 | */ 168 | mMarkerLeftInset = (int) (17.5 * mDensity); 169 | mMarkerRightInset = (int) (19.5 * mDensity); 170 | mMarkerTopOffset = (int) (6 * mDensity); 171 | mMarkerBottomOffset = (int) (6 * mDensity); 172 | 173 | /** 174 | * Change this for duration text as per your view 175 | */ 176 | 177 | mTextLeftInset = (int) (20 * mDensity); 178 | mTextTopOffset = (int) (-1 * mDensity); 179 | mTextRightInset = (int) (19 * mDensity); 180 | mTextBottomOffset = (int) (-40 * mDensity); 181 | 182 | txtAudioCancel.setOnClickListener(this); 183 | txtAudioUpload.setOnClickListener(this); 184 | txtAudioRecord.setOnClickListener(this); 185 | txtAudioDone.setOnClickListener(this); 186 | txtAudioPlay.setOnClickListener(this); 187 | txtAudioRecordUpdate.setOnClickListener(this); 188 | txtAudioCrop.setOnClickListener(this); 189 | txtAudioReset.setOnClickListener(this); 190 | 191 | mHandler.postDelayed(mTimerRunnable, 100); 192 | } 193 | 194 | 195 | private Runnable mTimerRunnable = new Runnable() { 196 | public void run() { 197 | // Updating Text is slow on Android. Make sure 198 | // we only do the update if the text has actually changed. 199 | if (mStartPos != mLastDisplayedStartPos) { 200 | txtStartPosition.setText(formatTime(mStartPos)); 201 | mLastDisplayedStartPos = mStartPos; 202 | } 203 | 204 | if (mEndPos != mLastDisplayedEndPos) { 205 | txtEndPosition.setText(formatTime(mEndPos)); 206 | mLastDisplayedEndPos = mEndPos; 207 | } 208 | 209 | mHandler.postDelayed(mTimerRunnable, 100); 210 | } 211 | }; 212 | 213 | 214 | @Override 215 | public void onClick(View view) { 216 | if (view == txtAudioRecord) { 217 | if (isAudioRecording) { 218 | isAudioRecording = false; 219 | mRecordingKeepGoing = false; 220 | } else { 221 | isAudioRecording = true; 222 | txtAudioRecord.setBackgroundResource(R.drawable.ic_stop_btn1); 223 | txtAudioRecordTime.setVisibility(View.VISIBLE); 224 | startRecording(); 225 | mRecordingLastUpdateTime = Utility.getCurrentTime(); 226 | mRecordingKeepGoing = true; 227 | } 228 | } else if (view == txtAudioCancel) { 229 | finish(); 230 | } else if (view == txtAudioRecordUpdate) { 231 | rlAudioEdit.setVisibility(View.GONE); 232 | txtAudioUpload.setVisibility(View.GONE); 233 | llAudioCapture.setVisibility(View.VISIBLE); 234 | isAudioRecording = true; 235 | txtAudioRecord.setBackgroundResource(R.drawable.ic_stop_btn1); 236 | txtAudioRecordTime.setVisibility(View.VISIBLE); 237 | startRecording(); 238 | mRecordingLastUpdateTime = Utility.getCurrentTime(); 239 | mRecordingKeepGoing = true; 240 | // txtAudioCrop.setBackgroundResource(R.drawable.ic_crop_btn); 241 | txtAudioDone.setVisibility(View.GONE); 242 | txtAudioCrop.setVisibility(View.VISIBLE); 243 | txtAudioPlay.setBackgroundResource(R.drawable.ic_play_btn); 244 | markerStart.setVisibility(View.INVISIBLE); 245 | markerEnd.setVisibility(View.INVISIBLE); 246 | txtStartPosition.setVisibility(View.VISIBLE); 247 | txtEndPosition.setVisibility(View.VISIBLE); 248 | 249 | } else if (view == txtAudioPlay) { 250 | if (!mIsPlaying) { 251 | txtAudioPlay.setBackgroundResource(R.drawable.ic_pause_btn); 252 | } else { 253 | txtAudioPlay.setBackgroundResource(R.drawable.ic_play_btn); 254 | } 255 | onPlay(mStartPos); 256 | } else if (view == txtAudioDone) { 257 | 258 | double startTime = audioWaveform.pixelsToSeconds(mStartPos); 259 | double endTime = audioWaveform.pixelsToSeconds(mEndPos); 260 | double difference = endTime - startTime; 261 | 262 | if (difference <= 0) { 263 | Toast.makeText(AudioTrimmerActivity.this, "Trim seconds should be greater than 0 seconds", Toast.LENGTH_SHORT).show(); 264 | } else if (difference > 60) { 265 | Toast.makeText(AudioTrimmerActivity.this, "Trim seconds should be less than 1 minute", Toast.LENGTH_SHORT).show(); 266 | } else { 267 | if (mIsPlaying) { 268 | handlePause(); 269 | } 270 | saveRingtone(0); 271 | 272 | txtAudioDone.setVisibility(View.GONE); 273 | txtAudioReset.setVisibility(View.VISIBLE); 274 | // txtAudioCrop.setBackgroundResource(R.drawable.ic_crop_btn_fill); 275 | txtAudioCrop.setVisibility(View.VISIBLE); 276 | 277 | markerStart.setVisibility(View.INVISIBLE); 278 | markerEnd.setVisibility(View.INVISIBLE); 279 | txtStartPosition.setVisibility(View.INVISIBLE); 280 | txtEndPosition.setVisibility(View.INVISIBLE); 281 | } 282 | 283 | } else if (view == txtAudioReset) { 284 | audioWaveform.setIsDrawBorder(true); 285 | mPlayer = new SamplePlayer(mRecordedSoundFile); 286 | finishOpeningSoundFile(mRecordedSoundFile, 1); 287 | } else if (view == txtAudioCrop) { 288 | 289 | // txtAudioCrop.setBackgroundResource(R.drawable.ic_crop_btn); 290 | txtAudioCrop.setVisibility(View.GONE); 291 | txtAudioDone.setVisibility(View.VISIBLE); 292 | txtAudioReset.setVisibility(View.VISIBLE); 293 | 294 | audioWaveform.setIsDrawBorder(true); 295 | audioWaveform.setBackgroundColor(getResources().getColor(R.color.colorWaveformBg)); 296 | markerStart.setVisibility(View.VISIBLE); 297 | markerEnd.setVisibility(View.VISIBLE); 298 | txtStartPosition.setVisibility(View.VISIBLE); 299 | txtEndPosition.setVisibility(View.VISIBLE); 300 | 301 | } else if (view == txtAudioUpload) { 302 | 303 | if (txtAudioDone.getVisibility() == View.VISIBLE) { 304 | if (mIsPlaying) { 305 | handlePause(); 306 | } 307 | saveRingtone(1); 308 | } else { 309 | Bundle conData = new Bundle(); 310 | conData.putString("INTENT_AUDIO_FILE", mFile.getAbsolutePath()); 311 | Intent intent = new Intent(); 312 | intent.putExtras(conData); 313 | setResult(RESULT_OK, intent); 314 | finish(); 315 | } 316 | 317 | 318 | } 319 | } 320 | 321 | /** 322 | * Start recording 323 | */ 324 | private void startRecording() { 325 | final SoundFile.ProgressListener listener = 326 | new SoundFile.ProgressListener() { 327 | public boolean reportProgress(double elapsedTime) { 328 | long now = Utility.getCurrentTime(); 329 | if (now - mRecordingLastUpdateTime > 5) { 330 | mRecordingTime = elapsedTime; 331 | // Only UI thread can update Views such as TextViews. 332 | runOnUiThread(new Runnable() { 333 | public void run() { 334 | int min = (int) (mRecordingTime / 60); 335 | float sec = (float) (mRecordingTime - 60 * min); 336 | txtAudioRecordTime.setText(String.format(Locale.US, "%02d:%05.2f", min, sec)); 337 | } 338 | }); 339 | mRecordingLastUpdateTime = now; 340 | } 341 | return mRecordingKeepGoing; 342 | } 343 | }; 344 | 345 | // Record the audio stream in a background thread 346 | Thread mRecordAudioThread = new Thread() { 347 | public void run() { 348 | try { 349 | mRecordedSoundFile = SoundFile.record(listener); 350 | if (mRecordedSoundFile == null) { 351 | finish(); 352 | Runnable runnable = new Runnable() { 353 | public void run() { 354 | Log.e("error >> ", "sound file null"); 355 | } 356 | }; 357 | mHandler.post(runnable); 358 | return; 359 | } 360 | mPlayer = new SamplePlayer(mRecordedSoundFile); 361 | } catch (final Exception e) { 362 | finish(); 363 | e.printStackTrace(); 364 | return; 365 | } 366 | 367 | Runnable runnable = new Runnable() { 368 | public void run() { 369 | 370 | audioWaveform.setIsDrawBorder(true); 371 | finishOpeningSoundFile(mRecordedSoundFile, 0); 372 | txtAudioRecord.setBackgroundResource(R.drawable.ic_stop_btn1); 373 | txtAudioRecordTime.setVisibility(View.INVISIBLE); 374 | txtStartPosition.setVisibility(View.VISIBLE); 375 | txtEndPosition.setVisibility(View.VISIBLE); 376 | markerEnd.setVisibility(View.VISIBLE); 377 | markerStart.setVisibility(View.VISIBLE); 378 | llAudioCapture.setVisibility(View.GONE); 379 | rlAudioEdit.setVisibility(View.VISIBLE); 380 | txtAudioUpload.setVisibility(View.VISIBLE); 381 | 382 | txtAudioReset.setVisibility(View.VISIBLE); 383 | txtAudioCrop.setVisibility(View.GONE); 384 | txtAudioDone.setVisibility(View.VISIBLE); 385 | 386 | } 387 | }; 388 | mHandler.post(runnable); 389 | } 390 | }; 391 | mRecordAudioThread.start(); 392 | } 393 | 394 | /** 395 | * After recording finish do necessary steps 396 | * @param mSoundFile sound file 397 | * @param isReset isReset 398 | */ 399 | private void finishOpeningSoundFile(SoundFile mSoundFile, int isReset) { 400 | audioWaveform.setVisibility(View.VISIBLE); 401 | audioWaveform.setSoundFile(mSoundFile); 402 | audioWaveform.recomputeHeights(mDensity); 403 | 404 | mMaxPos = audioWaveform.maxPos(); 405 | mLastDisplayedStartPos = -1; 406 | mLastDisplayedEndPos = -1; 407 | 408 | mTouchDragging = false; 409 | 410 | mOffset = 0; 411 | mOffsetGoal = 0; 412 | mFlingVelocity = 0; 413 | resetPositions(); 414 | if (mEndPos > mMaxPos) 415 | mEndPos = mMaxPos; 416 | 417 | if (isReset == 1) { 418 | mStartPos = audioWaveform.secondsToPixels(0); 419 | mEndPos = audioWaveform.secondsToPixels(audioWaveform.pixelsToSeconds(mMaxPos)); 420 | } 421 | 422 | if (audioWaveform != null && audioWaveform.isInitialized()) { 423 | double seconds = audioWaveform.pixelsToSeconds(mMaxPos); 424 | int min = (int) (seconds / 60); 425 | float sec = (float) (seconds - 60 * min); 426 | txtAudioRecordTimeUpdate.setText(String.format(Locale.US, "%02d:%05.2f", min, sec)); 427 | } 428 | 429 | updateDisplay(); 430 | } 431 | 432 | /** 433 | * Update views 434 | */ 435 | 436 | private synchronized void updateDisplay() { 437 | if (mIsPlaying) { 438 | int now = mPlayer.getCurrentPosition(); 439 | int frames = audioWaveform.millisecsToPixels(now); 440 | audioWaveform.setPlayback(frames); 441 | Log.e("mWidth >> ", "" + mWidth); 442 | setOffsetGoalNoUpdate(frames - mWidth / 2); 443 | if (now >= mPlayEndMillSec) { 444 | handlePause(); 445 | } 446 | } 447 | 448 | if (!mTouchDragging) { 449 | int offsetDelta; 450 | 451 | if (mFlingVelocity != 0) { 452 | offsetDelta = mFlingVelocity / 30; 453 | if (mFlingVelocity > 80) { 454 | mFlingVelocity -= 80; 455 | } else if (mFlingVelocity < -80) { 456 | mFlingVelocity += 80; 457 | } else { 458 | mFlingVelocity = 0; 459 | } 460 | 461 | mOffset += offsetDelta; 462 | 463 | if (mOffset + mWidth / 2 > mMaxPos) { 464 | mOffset = mMaxPos - mWidth / 2; 465 | mFlingVelocity = 0; 466 | } 467 | if (mOffset < 0) { 468 | mOffset = 0; 469 | mFlingVelocity = 0; 470 | } 471 | mOffsetGoal = mOffset; 472 | } else { 473 | offsetDelta = mOffsetGoal - mOffset; 474 | 475 | if (offsetDelta > 10) 476 | offsetDelta = offsetDelta / 10; 477 | else if (offsetDelta > 0) 478 | offsetDelta = 1; 479 | else if (offsetDelta < -10) 480 | offsetDelta = offsetDelta / 10; 481 | else if (offsetDelta < 0) 482 | offsetDelta = -1; 483 | else 484 | offsetDelta = 0; 485 | 486 | mOffset += offsetDelta; 487 | } 488 | } 489 | 490 | audioWaveform.setParameters(mStartPos, mEndPos, mOffset); 491 | audioWaveform.invalidate(); 492 | 493 | markerStart.setContentDescription( 494 | " Start Marker" + 495 | formatTime(mStartPos)); 496 | markerEnd.setContentDescription( 497 | " End Marker" + 498 | formatTime(mEndPos)); 499 | 500 | int startX = mStartPos - mOffset - mMarkerLeftInset; 501 | if (startX + markerStart.getWidth() >= 0) { 502 | if (!mStartVisible) { 503 | // Delay this to avoid flicker 504 | mHandler.postDelayed(new Runnable() { 505 | public void run() { 506 | mStartVisible = true; 507 | markerStart.setAlpha(1f); 508 | txtStartPosition.setAlpha(1f); 509 | } 510 | }, 0); 511 | } 512 | } else { 513 | if (mStartVisible) { 514 | markerStart.setAlpha(0f); 515 | txtStartPosition.setAlpha(0f); 516 | mStartVisible = false; 517 | } 518 | startX = 0; 519 | } 520 | 521 | 522 | int startTextX = mStartPos - mOffset - mTextLeftInset; 523 | if (startTextX + markerStart.getWidth() < 0) { 524 | startTextX = 0; 525 | } 526 | 527 | 528 | int endX = mEndPos - mOffset - markerEnd.getWidth() + mMarkerRightInset; 529 | if (endX + markerEnd.getWidth() >= 0) { 530 | if (!mEndVisible) { 531 | // Delay this to avoid flicker 532 | mHandler.postDelayed(new Runnable() { 533 | public void run() { 534 | mEndVisible = true; 535 | markerEnd.setAlpha(1f); 536 | } 537 | }, 0); 538 | } 539 | } else { 540 | if (mEndVisible) { 541 | markerEnd.setAlpha(0f); 542 | mEndVisible = false; 543 | } 544 | endX = 0; 545 | } 546 | 547 | int endTextX = mEndPos - mOffset - txtEndPosition.getWidth() + mTextRightInset; 548 | if (endTextX + markerEnd.getWidth() < 0) { 549 | endTextX = 0; 550 | } 551 | 552 | RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( 553 | RelativeLayout.LayoutParams.WRAP_CONTENT, 554 | RelativeLayout.LayoutParams.WRAP_CONTENT); 555 | // params.setMargins( 556 | // startX, 557 | // mMarkerTopOffset, 558 | // -markerStart.getWidth(), 559 | // -markerStart.getHeight()); 560 | params.setMargins( 561 | startX, 562 | audioWaveform.getMeasuredHeight() / 2 + mMarkerTopOffset, 563 | -markerStart.getWidth(), 564 | -markerStart.getHeight()); 565 | markerStart.setLayoutParams(params); 566 | 567 | 568 | params = new RelativeLayout.LayoutParams( 569 | RelativeLayout.LayoutParams.WRAP_CONTENT, 570 | RelativeLayout.LayoutParams.WRAP_CONTENT); 571 | params.setMargins( 572 | startTextX, 573 | mTextTopOffset, 574 | -txtStartPosition.getWidth(), 575 | -txtStartPosition.getHeight()); 576 | txtStartPosition.setLayoutParams(params); 577 | 578 | 579 | params = new RelativeLayout.LayoutParams( 580 | RelativeLayout.LayoutParams.WRAP_CONTENT, 581 | RelativeLayout.LayoutParams.WRAP_CONTENT); 582 | params.setMargins( 583 | endX, 584 | audioWaveform.getMeasuredHeight() / 2 + mMarkerBottomOffset, 585 | -markerEnd.getWidth(), 586 | -markerEnd.getHeight()); 587 | // params.setMargins( 588 | // endX, 589 | // audioWaveform.getMeasuredHeight() - markerEnd.getHeight() - mMarkerBottomOffset, 590 | // -markerEnd.getWidth(), 591 | // -markerEnd.getHeight()); 592 | markerEnd.setLayoutParams(params); 593 | 594 | 595 | params = new RelativeLayout.LayoutParams( 596 | RelativeLayout.LayoutParams.WRAP_CONTENT, 597 | RelativeLayout.LayoutParams.WRAP_CONTENT); 598 | params.setMargins( 599 | endTextX, 600 | audioWaveform.getMeasuredHeight() - txtEndPosition.getHeight() - mTextBottomOffset, 601 | -txtEndPosition.getWidth(), 602 | -txtEndPosition.getHeight()); 603 | 604 | txtEndPosition.setLayoutParams(params); 605 | } 606 | 607 | /** 608 | * Reset all positions 609 | */ 610 | 611 | private void resetPositions() { 612 | mStartPos = audioWaveform.secondsToPixels(0.0); 613 | mEndPos = audioWaveform.secondsToPixels(15.0); 614 | } 615 | 616 | private void setOffsetGoalNoUpdate(int offset) { 617 | if (mTouchDragging) { 618 | return; 619 | } 620 | 621 | mOffsetGoal = offset; 622 | if (mOffsetGoal + mWidth / 2 > mMaxPos) 623 | mOffsetGoal = mMaxPos - mWidth / 2; 624 | if (mOffsetGoal < 0) 625 | mOffsetGoal = 0; 626 | } 627 | 628 | private String formatTime(int pixels) { 629 | if (audioWaveform != null && audioWaveform.isInitialized()) { 630 | return formatDecimal(audioWaveform.pixelsToSeconds(pixels)); 631 | } else { 632 | return ""; 633 | } 634 | } 635 | 636 | private String formatDecimal(double x) { 637 | int xWhole = (int) x; 638 | int xFrac = (int) (100 * (x - xWhole) + 0.5); 639 | 640 | if (xFrac >= 100) { 641 | xWhole++; //Round up 642 | xFrac -= 100; //Now we need the remainder after the round up 643 | if (xFrac < 10) { 644 | xFrac *= 10; //we need a fraction that is 2 digits long 645 | } 646 | } 647 | 648 | if (xFrac < 10) { 649 | if (xWhole < 10) 650 | return "0" + xWhole + ".0" + xFrac; 651 | else 652 | return xWhole + ".0" + xFrac; 653 | } else { 654 | if (xWhole < 10) 655 | return "0" + xWhole + "." + xFrac; 656 | else 657 | return xWhole + "." + xFrac; 658 | 659 | } 660 | } 661 | 662 | private int trap(int pos) { 663 | if (pos < 0) 664 | return 0; 665 | if (pos > mMaxPos) 666 | return mMaxPos; 667 | return pos; 668 | } 669 | 670 | private void setOffsetGoalStart() { 671 | setOffsetGoal(mStartPos - mWidth / 2); 672 | } 673 | 674 | private void setOffsetGoalStartNoUpdate() { 675 | setOffsetGoalNoUpdate(mStartPos - mWidth / 2); 676 | } 677 | 678 | private void setOffsetGoalEnd() { 679 | setOffsetGoal(mEndPos - mWidth / 2); 680 | } 681 | 682 | private void setOffsetGoalEndNoUpdate() { 683 | setOffsetGoalNoUpdate(mEndPos - mWidth / 2); 684 | } 685 | 686 | private void setOffsetGoal(int offset) { 687 | setOffsetGoalNoUpdate(offset); 688 | updateDisplay(); 689 | } 690 | 691 | public void markerDraw() { 692 | } 693 | 694 | public void markerTouchStart(MarkerView marker, float x) { 695 | mTouchDragging = true; 696 | mTouchStart = x; 697 | mTouchInitialStartPos = mStartPos; 698 | mTouchInitialEndPos = mEndPos; 699 | handlePause(); 700 | } 701 | 702 | public void markerTouchMove(MarkerView marker, float x) { 703 | float delta = x - mTouchStart; 704 | 705 | if (marker == markerStart) { 706 | mStartPos = trap((int) (mTouchInitialStartPos + delta)); 707 | mEndPos = trap((int) (mTouchInitialEndPos + delta)); 708 | } else { 709 | mEndPos = trap((int) (mTouchInitialEndPos + delta)); 710 | if (mEndPos < mStartPos) 711 | mEndPos = mStartPos; 712 | } 713 | 714 | updateDisplay(); 715 | } 716 | 717 | public void markerTouchEnd(MarkerView marker) { 718 | mTouchDragging = false; 719 | if (marker == markerStart) { 720 | setOffsetGoalStart(); 721 | } else { 722 | setOffsetGoalEnd(); 723 | } 724 | } 725 | 726 | public void markerLeft(MarkerView marker, int velocity) { 727 | mKeyDown = true; 728 | 729 | if (marker == markerStart) { 730 | int saveStart = mStartPos; 731 | mStartPos = trap(mStartPos - velocity); 732 | mEndPos = trap(mEndPos - (saveStart - mStartPos)); 733 | setOffsetGoalStart(); 734 | } 735 | 736 | if (marker == markerEnd) { 737 | if (mEndPos == mStartPos) { 738 | mStartPos = trap(mStartPos - velocity); 739 | mEndPos = mStartPos; 740 | } else { 741 | mEndPos = trap(mEndPos - velocity); 742 | } 743 | 744 | setOffsetGoalEnd(); 745 | } 746 | 747 | updateDisplay(); 748 | } 749 | 750 | public void markerRight(MarkerView marker, int velocity) { 751 | mKeyDown = true; 752 | 753 | if (marker == markerStart) { 754 | int saveStart = mStartPos; 755 | mStartPos += velocity; 756 | if (mStartPos > mMaxPos) 757 | mStartPos = mMaxPos; 758 | mEndPos += (mStartPos - saveStart); 759 | if (mEndPos > mMaxPos) 760 | mEndPos = mMaxPos; 761 | 762 | setOffsetGoalStart(); 763 | } 764 | 765 | if (marker == markerEnd) { 766 | mEndPos += velocity; 767 | if (mEndPos > mMaxPos) 768 | mEndPos = mMaxPos; 769 | 770 | setOffsetGoalEnd(); 771 | } 772 | 773 | updateDisplay(); 774 | } 775 | 776 | public void markerEnter(MarkerView marker) { 777 | } 778 | 779 | public void markerKeyUp() { 780 | mKeyDown = false; 781 | updateDisplay(); 782 | } 783 | 784 | public void markerFocus(MarkerView marker) { 785 | mKeyDown = false; 786 | if (marker == markerStart) { 787 | setOffsetGoalStartNoUpdate(); 788 | } else { 789 | setOffsetGoalEndNoUpdate(); 790 | } 791 | 792 | // Delay updaing the display because if this focus was in 793 | // response to a touch event, we want to receive the touch 794 | // event too before updating the display. 795 | mHandler.postDelayed(new Runnable() { 796 | public void run() { 797 | updateDisplay(); 798 | } 799 | }, 100); 800 | } 801 | 802 | // 803 | // WaveformListener 804 | // 805 | 806 | /** 807 | * Every time we get a message that our waveform drew, see if we need to 808 | * animate and trigger another redraw. 809 | */ 810 | public void waveformDraw() { 811 | mWidth = audioWaveform.getMeasuredWidth(); 812 | if (mOffsetGoal != mOffset && !mKeyDown) 813 | updateDisplay(); 814 | else if (mIsPlaying) { 815 | updateDisplay(); 816 | } else if (mFlingVelocity != 0) { 817 | updateDisplay(); 818 | } 819 | } 820 | 821 | public void waveformTouchStart(float x) { 822 | mTouchDragging = true; 823 | mTouchStart = x; 824 | mTouchInitialOffset = mOffset; 825 | mFlingVelocity = 0; 826 | // long mWaveformTouchStartMsec = Utility.getCurrentTime(); 827 | } 828 | 829 | public void waveformTouchMove(float x) { 830 | mOffset = trap((int) (mTouchInitialOffset + (mTouchStart - x))); 831 | updateDisplay(); 832 | } 833 | 834 | public void waveformTouchEnd() { 835 | /*mTouchDragging = false; 836 | mOffsetGoal = mOffset; 837 | 838 | long elapsedMsec = Utility.Utility.getCurrentTime() - mWaveformTouchStartMsec; 839 | if (elapsedMsec < 300) { 840 | if (mIsPlaying) { 841 | int seekMsec = audioWaveform.pixelsToMillisecs( 842 | (int) (mTouchStart + mOffset)); 843 | if (seekMsec >= mPlayStartMsec && 844 | seekMsec < mPlayEndMillSec) { 845 | mPlayer.seekTo(seekMsec); 846 | } else { 847 | // handlePause(); 848 | } 849 | } else { 850 | onPlay((int) (mTouchStart + mOffset)); 851 | } 852 | }*/ 853 | } 854 | 855 | private synchronized void handlePause() { 856 | txtAudioPlay.setBackgroundResource(R.drawable.ic_play_btn); 857 | if (mPlayer != null && mPlayer.isPlaying()) { 858 | mPlayer.pause(); 859 | } 860 | audioWaveform.setPlayback(-1); 861 | mIsPlaying = false; 862 | } 863 | 864 | private synchronized void onPlay(int startPosition) { 865 | if (mIsPlaying) { 866 | handlePause(); 867 | return; 868 | } 869 | 870 | if (mPlayer == null) { 871 | // Not initialized yet 872 | return; 873 | } 874 | 875 | try { 876 | int mPlayStartMsec = audioWaveform.pixelsToMillisecs(startPosition); 877 | if (startPosition < mStartPos) { 878 | mPlayEndMillSec = audioWaveform.pixelsToMillisecs(mStartPos); 879 | } else if (startPosition > mEndPos) { 880 | mPlayEndMillSec = audioWaveform.pixelsToMillisecs(mMaxPos); 881 | } else { 882 | mPlayEndMillSec = audioWaveform.pixelsToMillisecs(mEndPos); 883 | } 884 | mPlayer.setOnCompletionListener(new SamplePlayer.OnCompletionListener() { 885 | @Override 886 | public void onCompletion() { 887 | handlePause(); 888 | } 889 | }); 890 | mIsPlaying = true; 891 | 892 | mPlayer.seekTo(mPlayStartMsec); 893 | mPlayer.start(); 894 | updateDisplay(); 895 | } catch (Exception e) { 896 | e.printStackTrace(); 897 | } 898 | } 899 | 900 | public void waveformFling(float vx) { 901 | mTouchDragging = false; 902 | mOffsetGoal = mOffset; 903 | mFlingVelocity = (int) (-vx); 904 | updateDisplay(); 905 | } 906 | 907 | public void waveformZoomIn() { 908 | /*audioWaveform.zoomIn(); 909 | mStartPos = audioWaveform.getStart(); 910 | mEndPos = audioWaveform.getEnd(); 911 | mMaxPos = audioWaveform.maxPos(); 912 | mOffset = audioWaveform.getOffset(); 913 | mOffsetGoal = mOffset; 914 | updateDisplay();*/ 915 | } 916 | 917 | public void waveformZoomOut() { 918 | /*audioWaveform.zoomOut(); 919 | mStartPos = audioWaveform.getStart(); 920 | mEndPos = audioWaveform.getEnd(); 921 | mMaxPos = audioWaveform.maxPos(); 922 | mOffset = audioWaveform.getOffset(); 923 | mOffsetGoal = mOffset; 924 | updateDisplay();*/ 925 | } 926 | 927 | /** 928 | * Save sound file as ringtone 929 | * @param finish flag for finish 930 | */ 931 | 932 | private void saveRingtone(final int finish) { 933 | double startTime = audioWaveform.pixelsToSeconds(mStartPos); 934 | double endTime = audioWaveform.pixelsToSeconds(mEndPos); 935 | final int startFrame = audioWaveform.secondsToFrames(startTime); 936 | final int endFrame = audioWaveform.secondsToFrames(endTime - 0.04); 937 | final int duration = (int) (endTime - startTime + 0.5); 938 | 939 | // Create an indeterminate progress dialog 940 | mProgressDialog = new ProgressDialog(this); 941 | mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); 942 | mProgressDialog.setTitle("Saving...."); 943 | mProgressDialog.setIndeterminate(true); 944 | mProgressDialog.setCancelable(false); 945 | mProgressDialog.show(); 946 | 947 | // Save the sound file in a background thread 948 | Thread mSaveSoundFileThread = new Thread() { 949 | public void run() { 950 | // Try AAC first. 951 | String outPath = makeRingtoneFilename("AUDIO_TEMP", Utility.AUDIO_FORMAT); 952 | if (outPath == null) { 953 | Log.e(" >> ", "Unable to find unique filename"); 954 | return; 955 | } 956 | File outFile = new File(outPath); 957 | try { 958 | // Write the new file 959 | mRecordedSoundFile.WriteFile(outFile, startFrame, endFrame - startFrame); 960 | } catch (Exception e) { 961 | // log the error and try to create a .wav file instead 962 | if (outFile.exists()) { 963 | outFile.delete(); 964 | } 965 | e.printStackTrace(); 966 | } 967 | 968 | mProgressDialog.dismiss(); 969 | 970 | final String finalOutPath = outPath; 971 | Runnable runnable = new Runnable() { 972 | public void run() { 973 | afterSavingRingtone("AUDIO_TEMP", 974 | finalOutPath, 975 | duration, finish); 976 | } 977 | }; 978 | mHandler.post(runnable); 979 | } 980 | }; 981 | mSaveSoundFileThread.start(); 982 | } 983 | 984 | /** 985 | * After saving as ringtone set its content values 986 | * @param title title 987 | * @param outPath output path 988 | * @param duration duration of file 989 | * @param finish flag for finish 990 | */ 991 | private void afterSavingRingtone(CharSequence title, 992 | String outPath, 993 | int duration, int finish) { 994 | File outFile = new File(outPath); 995 | long fileSize = outFile.length(); 996 | 997 | ContentValues values = new ContentValues(); 998 | values.put(MediaStore.MediaColumns.DATA, outPath); 999 | values.put(MediaStore.MediaColumns.TITLE, title.toString()); 1000 | values.put(MediaStore.MediaColumns.SIZE, fileSize); 1001 | values.put(MediaStore.MediaColumns.MIME_TYPE, Utility.AUDIO_MIME_TYPE); 1002 | 1003 | values.put(MediaStore.Audio.Media.ARTIST, getApplicationInfo().name); 1004 | values.put(MediaStore.Audio.Media.DURATION, duration); 1005 | 1006 | values.put(MediaStore.Audio.Media.IS_MUSIC, true); 1007 | 1008 | Uri uri = MediaStore.Audio.Media.getContentUriForPath(outPath); 1009 | final Uri newUri = getContentResolver().insert(uri, values); 1010 | Log.e("final URI >> ", newUri + " >> " + outPath); 1011 | 1012 | if (finish == 0) { 1013 | loadFromFile(outPath); 1014 | } else if (finish == 1) { 1015 | Bundle conData = new Bundle(); 1016 | conData.putString("INTENT_AUDIO_FILE", outPath); 1017 | Intent intent = getIntent(); 1018 | intent.putExtras(conData); 1019 | setResult(RESULT_OK, intent); 1020 | finish(); 1021 | } 1022 | } 1023 | 1024 | /** 1025 | * Generating name for ringtone 1026 | * @param title title of file 1027 | * @param extension extension for file 1028 | * @return filename 1029 | */ 1030 | 1031 | private String makeRingtoneFilename(CharSequence title, String extension) { 1032 | String subDir; 1033 | String externalRootDir = Environment.getExternalStorageDirectory().getPath(); 1034 | if (!externalRootDir.endsWith("/")) { 1035 | externalRootDir += "/"; 1036 | } 1037 | subDir = "media/audio/music/"; 1038 | String parentDir = externalRootDir + subDir; 1039 | 1040 | // Create the parent directory 1041 | File parentDirFile = new File(parentDir); 1042 | parentDirFile.mkdirs(); 1043 | 1044 | // If we can't write to that special path, try just writing 1045 | // directly to the sdcard 1046 | if (!parentDirFile.isDirectory()) { 1047 | parentDir = externalRootDir; 1048 | } 1049 | 1050 | // Turn the title into a filename 1051 | String filename = ""; 1052 | for (int i = 0; i < title.length(); i++) { 1053 | if (Character.isLetterOrDigit(title.charAt(i))) { 1054 | filename += title.charAt(i); 1055 | } 1056 | } 1057 | 1058 | // Try to make the filename unique 1059 | String path = null; 1060 | for (int i = 0; i < 100; i++) { 1061 | String testPath; 1062 | if (i > 0) 1063 | testPath = parentDir + filename + i + extension; 1064 | else 1065 | testPath = parentDir + filename + extension; 1066 | 1067 | try { 1068 | RandomAccessFile f = new RandomAccessFile(new File(testPath), "r"); 1069 | f.close(); 1070 | } catch (Exception e) { 1071 | // Good, the file didn't exist 1072 | path = testPath; 1073 | break; 1074 | } 1075 | } 1076 | 1077 | return path; 1078 | } 1079 | 1080 | /** 1081 | * Load file from path 1082 | * @param mFilename file name 1083 | */ 1084 | 1085 | private void loadFromFile(String mFilename) { 1086 | mFile = new File(mFilename); 1087 | // SongMetadataReader metadataReader = new SongMetadataReader(this, mFilename); 1088 | mLoadingLastUpdateTime = Utility.getCurrentTime(); 1089 | mLoadingKeepGoing = true; 1090 | mProgressDialog = new ProgressDialog(this); 1091 | mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 1092 | mProgressDialog.setTitle("Loading ..."); 1093 | mProgressDialog.show(); 1094 | 1095 | final SoundFile.ProgressListener listener = 1096 | new SoundFile.ProgressListener() { 1097 | public boolean reportProgress(double fractionComplete) { 1098 | 1099 | long now = Utility.getCurrentTime(); 1100 | if (now - mLoadingLastUpdateTime > 100) { 1101 | mProgressDialog.setProgress( 1102 | (int) (mProgressDialog.getMax() * fractionComplete)); 1103 | mLoadingLastUpdateTime = now; 1104 | } 1105 | return mLoadingKeepGoing; 1106 | } 1107 | }; 1108 | 1109 | // Load the sound file in a background thread 1110 | Thread mLoadSoundFileThread = new Thread() { 1111 | public void run() { 1112 | try { 1113 | mLoadedSoundFile = SoundFile.create(mFile.getAbsolutePath(), listener); 1114 | if (mLoadedSoundFile == null) { 1115 | mProgressDialog.dismiss(); 1116 | String name = mFile.getName().toLowerCase(); 1117 | String[] components = name.split("\\."); 1118 | String err; 1119 | if (components.length < 2) { 1120 | err = "No Extension"; 1121 | } else { 1122 | err = "Bad Extension"; 1123 | } 1124 | final String finalErr = err; 1125 | Log.e(" >> ", "" + finalErr); 1126 | return; 1127 | } 1128 | mPlayer = new SamplePlayer(mLoadedSoundFile); 1129 | } catch (final Exception e) { 1130 | mProgressDialog.dismiss(); 1131 | e.printStackTrace(); 1132 | return; 1133 | } 1134 | mProgressDialog.dismiss(); 1135 | if (mLoadingKeepGoing) { 1136 | Runnable runnable = new Runnable() { 1137 | public void run() { 1138 | audioWaveform.setVisibility(View.INVISIBLE); 1139 | audioWaveform.setBackgroundColor(getResources().getColor(R.color.waveformUnselectedBackground)); 1140 | audioWaveform.setIsDrawBorder(false); 1141 | finishOpeningSoundFile(mLoadedSoundFile, 0); 1142 | } 1143 | }; 1144 | mHandler.post(runnable); 1145 | } 1146 | } 1147 | }; 1148 | mLoadSoundFileThread.start(); 1149 | } 1150 | } 1151 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/MainActivity.java: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2018 Intuz Pvt Ltd. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 6 | // (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 11 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 12 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | 15 | package com.demo.audiotrimmer; 16 | 17 | import android.Manifest; 18 | import android.content.Intent; 19 | import android.content.pm.PackageManager; 20 | import android.os.Bundle; 21 | import android.support.annotation.NonNull; 22 | import android.support.v4.app.ActivityCompat; 23 | import android.support.v7.app.AppCompatActivity; 24 | import android.view.View; 25 | import android.widget.Button; 26 | import android.widget.Toast; 27 | 28 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 29 | 30 | private static final int ADD_AUDIO = 1001; 31 | private Button btnAudioTrim; 32 | private static final int REQUEST_ID_PERMISSIONS = 1; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | btnAudioTrim = (Button) findViewById(R.id.btnAudioTrim); 40 | btnAudioTrim.setOnClickListener(this); 41 | } 42 | 43 | @Override 44 | public void onClick(View view) { 45 | if (view == btnAudioTrim) { 46 | //check storage permission before start trimming 47 | if (checkStoragePermission()) { 48 | startActivityForResult(new Intent(MainActivity.this, AudioTrimmerActivity.class), ADD_AUDIO); 49 | overridePendingTransition(0, 0); 50 | } else { 51 | requestStoragePermission(); 52 | } 53 | } 54 | } 55 | 56 | private void requestStoragePermission() { 57 | ActivityCompat.requestPermissions(MainActivity.this, 58 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, 59 | Manifest.permission.RECORD_AUDIO}, 60 | REQUEST_ID_PERMISSIONS); 61 | } 62 | 63 | private boolean checkStoragePermission() { 64 | return (ActivityCompat.checkSelfPermission(MainActivity.this, 65 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && 66 | ActivityCompat.checkSelfPermission(MainActivity.this, 67 | Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED); 68 | } 69 | 70 | @Override 71 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 72 | @NonNull int[] grantResults) { 73 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 74 | if (requestCode == REQUEST_ID_PERMISSIONS) { 75 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && 76 | grantResults[1] == PackageManager.PERMISSION_GRANTED) { 77 | Toast.makeText(MainActivity.this, "Permission granted, Click again", Toast.LENGTH_SHORT).show(); 78 | } 79 | } 80 | } 81 | 82 | 83 | @Override 84 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 85 | super.onActivityResult(requestCode, resultCode, data); 86 | if (requestCode == ADD_AUDIO) { 87 | if (resultCode == RESULT_OK) { 88 | if (data != null) { 89 | //audio trim result will be saved at below path 90 | String path = data.getExtras().getString("INTENT_AUDIO_FILE"); 91 | Toast.makeText(MainActivity.this, "Audio stored at " + path, Toast.LENGTH_LONG).show(); 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/MP4Header.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | class Atom { // note: latest versions of spec simply call it 'box' instead of 'atom'. 20 | private int mSize; // includes atom header (8 bytes) 21 | private int mType; 22 | private byte[] mData; // an atom can either contain data or children, but not both. 23 | private Atom[] mChildren; 24 | private byte mVersion; // if negative, then the atom does not contain version and flags data. 25 | private int mFlags; 26 | 27 | // create an empty atom of the given type. 28 | public Atom(String type) { 29 | mSize = 8; 30 | mType = getTypeInt(type); 31 | mData = null; 32 | mChildren = null; 33 | mVersion = -1; 34 | mFlags = 0; 35 | } 36 | 37 | // create an empty atom of type type, with a given version and flags. 38 | public Atom(String type, byte version, int flags) { 39 | mSize = 12; 40 | mType = getTypeInt(type); 41 | mData = null; 42 | mChildren = null; 43 | mVersion = version; 44 | mFlags = flags; 45 | } 46 | 47 | // set the size field of the atom based on its content. 48 | private void setSize() { 49 | int size = 8; // type + size 50 | if (mVersion >= 0) { 51 | size += 4; // version + flags 52 | } 53 | if (mData != null) { 54 | size += mData.length; 55 | } else if (mChildren != null) { 56 | for (Atom child : mChildren) { 57 | size += child.getSize(); 58 | } 59 | } 60 | mSize = size; 61 | } 62 | 63 | // get the size of the this atom. 64 | public int getSize() { 65 | return mSize; 66 | } 67 | 68 | private int getTypeInt(String type_str) { 69 | int type = 0; 70 | type |= (byte) (type_str.charAt(0)) << 24; 71 | type |= (byte) (type_str.charAt(1)) << 16; 72 | type |= (byte) (type_str.charAt(2)) << 8; 73 | type |= (byte) (type_str.charAt(3)); 74 | return type; 75 | } 76 | 77 | public int getTypeInt() { 78 | return mType; 79 | } 80 | 81 | public String getTypeStr() { 82 | String type = ""; 83 | type += (char) ((byte) ((mType >> 24) & 0xFF)); 84 | type += (char) ((byte) ((mType >> 16) & 0xFF)); 85 | type += (char) ((byte) ((mType >> 8) & 0xFF)); 86 | type += (char) ((byte) (mType & 0xFF)); 87 | return type; 88 | } 89 | 90 | public boolean setData(byte[] data) { 91 | if (mChildren != null || data == null) { 92 | // TODO(nfaralli): log something here 93 | return false; 94 | } 95 | mData = data; 96 | setSize(); 97 | return true; 98 | } 99 | 100 | public byte[] getData() { 101 | return mData; 102 | } 103 | 104 | public boolean addChild(Atom child) { 105 | if (mData != null || child == null) { 106 | // TODO(nfaralli): log something here 107 | return false; 108 | } 109 | int numChildren = 1; 110 | if (mChildren != null) { 111 | numChildren += mChildren.length; 112 | } 113 | Atom[] children = new Atom[numChildren]; 114 | if (mChildren != null) { 115 | System.arraycopy(mChildren, 0, children, 0, mChildren.length); 116 | } 117 | children[numChildren - 1] = child; 118 | mChildren = children; 119 | setSize(); 120 | return true; 121 | } 122 | 123 | // return the child atom of the corresponding type. 124 | // type can contain grand children: e.g. type = "trak.mdia.minf" 125 | // return null if the atom does not contain such a child. 126 | public Atom getChild(String type) { 127 | if (mChildren == null) { 128 | return null; 129 | } 130 | String[] types = type.split("\\.", 2); 131 | for (Atom child : mChildren) { 132 | if (child.getTypeStr().equals(types[0])) { 133 | if (types.length == 1) { 134 | return child; 135 | } else { 136 | return child.getChild(types[1]); 137 | } 138 | } 139 | } 140 | return null; 141 | } 142 | 143 | // return a byte array containing the full content of the atom (including header) 144 | public byte[] getBytes() { 145 | byte[] atom_bytes = new byte[mSize]; 146 | int offset = 0; 147 | 148 | atom_bytes[offset++] = (byte) ((mSize >> 24) & 0xFF); 149 | atom_bytes[offset++] = (byte) ((mSize >> 16) & 0xFF); 150 | atom_bytes[offset++] = (byte) ((mSize >> 8) & 0xFF); 151 | atom_bytes[offset++] = (byte) (mSize & 0xFF); 152 | atom_bytes[offset++] = (byte) ((mType >> 24) & 0xFF); 153 | atom_bytes[offset++] = (byte) ((mType >> 16) & 0xFF); 154 | atom_bytes[offset++] = (byte) ((mType >> 8) & 0xFF); 155 | atom_bytes[offset++] = (byte) (mType & 0xFF); 156 | if (mVersion >= 0) { 157 | atom_bytes[offset++] = mVersion; 158 | atom_bytes[offset++] = (byte) ((mFlags >> 16) & 0xFF); 159 | atom_bytes[offset++] = (byte) ((mFlags >> 8) & 0xFF); 160 | atom_bytes[offset++] = (byte) (mFlags & 0xFF); 161 | } 162 | if (mData != null) { 163 | System.arraycopy(mData, 0, atom_bytes, offset, mData.length); 164 | } else if (mChildren != null) { 165 | byte[] child_bytes; 166 | for (Atom child : mChildren) { 167 | child_bytes = child.getBytes(); 168 | System.arraycopy(child_bytes, 0, atom_bytes, offset, child_bytes.length); 169 | offset += child_bytes.length; 170 | } 171 | } 172 | return atom_bytes; 173 | } 174 | 175 | // Used for debugging purpose only. 176 | public String toString() { 177 | String str = ""; 178 | byte[] atom_bytes = getBytes(); 179 | 180 | for (int i = 0; i < atom_bytes.length; i++) { 181 | if (i % 8 == 0 && i > 0) { 182 | str += '\n'; 183 | } 184 | str += String.format("0x%02X", atom_bytes[i]); 185 | if (i < atom_bytes.length - 1) { 186 | str += ','; 187 | if (i % 8 < 7) { 188 | str += ' '; 189 | } 190 | } 191 | } 192 | str += '\n'; 193 | return str; 194 | } 195 | } 196 | 197 | public class MP4Header { 198 | private int[] mFrameSize; // size of each AAC frames, in bytes. First one should be 2. 199 | private int mMaxFrameSize; // size of the biggest frame. 200 | private int mTotSize; // size of the AAC stream. 201 | private int mBitrate; // bitrate used to encode the AAC stream. 202 | private byte[] mTime; // time used for 'creation time' and 'modification time' fields. 203 | private byte[] mDurationMS; // duration of stream in milliseconds. 204 | private byte[] mNumSamples; // number of samples in the stream. 205 | private byte[] mHeader; // the complete header. 206 | private int mSampleRate; // sampling frequency in Hz (e.g. 44100). 207 | private int mChannels; // number of channels. 208 | 209 | // Creates a new MP4Header object that should be used to generate an .m4a file header. 210 | public MP4Header(int sampleRate, int numChannels, int[] frame_size, int bitrate) { 211 | if (frame_size == null || frame_size.length < 2 || frame_size[0] != 2) { 212 | //TODO(nfaralli): log something here 213 | return; 214 | } 215 | mSampleRate = sampleRate; 216 | mChannels = numChannels; 217 | mFrameSize = frame_size; 218 | mBitrate = bitrate; 219 | mMaxFrameSize = mFrameSize[0]; 220 | mTotSize = mFrameSize[0]; 221 | for (int i = 1; i < mFrameSize.length; i++) { 222 | if (mMaxFrameSize < mFrameSize[i]) { 223 | mMaxFrameSize = mFrameSize[i]; 224 | } 225 | mTotSize += mFrameSize[i]; 226 | } 227 | long time = System.currentTimeMillis() / 1000; 228 | time += (66 * 365 + 16) * 24 * 60 * 60; // number of seconds between 1904 and 1970 229 | mTime = new byte[4]; 230 | mTime[0] = (byte) ((time >> 24) & 0xFF); 231 | mTime[1] = (byte) ((time >> 16) & 0xFF); 232 | mTime[2] = (byte) ((time >> 8) & 0xFF); 233 | mTime[3] = (byte) (time & 0xFF); 234 | int numSamples = 1024 * (frame_size.length - 1); // 1st frame does not contain samples. 235 | int durationMS = (numSamples * 1000) / mSampleRate; 236 | if ((numSamples * 1000) % mSampleRate > 0) { // round the duration up. 237 | durationMS++; 238 | } 239 | mNumSamples = new byte[]{ 240 | (byte) ((numSamples >> 26) & 0XFF), 241 | (byte) ((numSamples >> 16) & 0XFF), 242 | (byte) ((numSamples >> 8) & 0XFF), 243 | (byte) (numSamples & 0XFF) 244 | }; 245 | mDurationMS = new byte[]{ 246 | (byte) ((durationMS >> 26) & 0XFF), 247 | (byte) ((durationMS >> 16) & 0XFF), 248 | (byte) ((durationMS >> 8) & 0XFF), 249 | (byte) (durationMS & 0XFF) 250 | }; 251 | setHeader(); 252 | } 253 | 254 | public byte[] getMP4Header() { 255 | return mHeader; 256 | } 257 | 258 | public static byte[] getMP4Header( 259 | int sampleRate, int numChannels, int[] frame_size, int bitrate) { 260 | return new MP4Header(sampleRate, numChannels, frame_size, bitrate).mHeader; 261 | } 262 | 263 | public String toString() { 264 | String str = ""; 265 | if (mHeader == null) { 266 | return str; 267 | } 268 | int num_32bits_per_lines = 8; 269 | int count = 0; 270 | for (byte b : mHeader) { 271 | boolean break_line = count > 0 && count % (num_32bits_per_lines * 4) == 0; 272 | boolean insert_space = count > 0 && count % 4 == 0 && !break_line; 273 | if (break_line) { 274 | str += '\n'; 275 | } 276 | if (insert_space) { 277 | str += ' '; 278 | } 279 | str += String.format("%02X", b); 280 | count++; 281 | } 282 | 283 | return str; 284 | } 285 | 286 | private void setHeader() { 287 | // create the atoms needed to build the header. 288 | Atom a_ftyp = getFTYPAtom(); 289 | Atom a_moov = getMOOVAtom(); 290 | Atom a_mdat = new Atom("mdat"); // create an empty atom. The AAC stream data should follow 291 | // immediately after. The correct size will be set later. 292 | 293 | // set the correct chunk offset in the stco atom. 294 | Atom a_stco = a_moov.getChild("trak.mdia.minf.stbl.stco"); 295 | if (a_stco == null) { 296 | mHeader = null; 297 | return; 298 | } 299 | byte[] data = a_stco.getData(); 300 | int chunk_offset = a_ftyp.getSize() + a_moov.getSize() + a_mdat.getSize(); 301 | int offset = data.length - 4; // here stco should contain only one chunk offset. 302 | data[offset++] = (byte) ((chunk_offset >> 24) & 0xFF); 303 | data[offset++] = (byte) ((chunk_offset >> 16) & 0xFF); 304 | data[offset++] = (byte) ((chunk_offset >> 8) & 0xFF); 305 | data[offset++] = (byte) (chunk_offset & 0xFF); 306 | 307 | // create the header byte array based on the previous atoms. 308 | byte[] header = new byte[chunk_offset]; // here chunk_offset is also the size of the header 309 | offset = 0; 310 | for (Atom atom : new Atom[]{a_ftyp, a_moov, a_mdat}) { 311 | byte[] atom_bytes = atom.getBytes(); 312 | System.arraycopy(atom_bytes, 0, header, offset, atom_bytes.length); 313 | offset += atom_bytes.length; 314 | } 315 | 316 | //set the correct size of the mdat atom 317 | int size = 8 + mTotSize; 318 | offset -= 8; 319 | header[offset++] = (byte) ((size >> 24) & 0xFF); 320 | header[offset++] = (byte) ((size >> 16) & 0xFF); 321 | header[offset++] = (byte) ((size >> 8) & 0xFF); 322 | header[offset++] = (byte) (size & 0xFF); 323 | 324 | mHeader = header; 325 | } 326 | 327 | private Atom getFTYPAtom() { 328 | Atom atom = new Atom("ftyp"); 329 | atom.setData(new byte[]{ 330 | 'M', '4', 'A', ' ', // Major brand 331 | 0, 0, 0, 0, // Minor version 332 | 'M', '4', 'A', ' ', // compatible brands 333 | 'm', 'p', '4', '2', 334 | 'i', 's', 'o', 'm' 335 | }); 336 | return atom; 337 | } 338 | 339 | private Atom getMOOVAtom() { 340 | Atom atom = new Atom("moov"); 341 | atom.addChild(getMVHDAtom()); 342 | atom.addChild(getTRAKAtom()); 343 | return atom; 344 | } 345 | 346 | private Atom getMVHDAtom() { 347 | Atom atom = new Atom("mvhd", (byte) 0, 0); 348 | atom.setData(new byte[]{ 349 | mTime[0], mTime[1], mTime[2], mTime[3], // creation time. 350 | mTime[0], mTime[1], mTime[2], mTime[3], // modification time. 351 | 0, 0, 0x03, (byte) 0xE8, // timescale = 1000 => duration expressed in ms. 352 | mDurationMS[0], mDurationMS[1], mDurationMS[2], mDurationMS[3], // duration in ms. 353 | 0, 1, 0, 0, // rate = 1.0 354 | 1, 0, // volume = 1.0 355 | 0, 0, // reserved 356 | 0, 0, 0, 0, // reserved 357 | 0, 0, 0, 0, // reserved 358 | 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // unity matrix 359 | 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 360 | 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0, 361 | 0, 0, 0, 0, // pre-defined 362 | 0, 0, 0, 0, // pre-defined 363 | 0, 0, 0, 0, // pre-defined 364 | 0, 0, 0, 0, // pre-defined 365 | 0, 0, 0, 0, // pre-defined 366 | 0, 0, 0, 0, // pre-defined 367 | 0, 0, 0, 2 // next track ID 368 | }); 369 | return atom; 370 | } 371 | 372 | private Atom getTRAKAtom() { 373 | Atom atom = new Atom("trak"); 374 | atom.addChild(getTKHDAtom()); 375 | atom.addChild(getMDIAAtom()); 376 | return atom; 377 | } 378 | 379 | private Atom getTKHDAtom() { 380 | Atom atom = new Atom("tkhd", (byte) 0, 0x07); // track enabled, in movie, and in preview. 381 | atom.setData(new byte[]{ 382 | mTime[0], mTime[1], mTime[2], mTime[3], // creation time. 383 | mTime[0], mTime[1], mTime[2], mTime[3], // modification time. 384 | 0, 0, 0, 1, // track ID 385 | 0, 0, 0, 0, // reserved 386 | mDurationMS[0], mDurationMS[1], mDurationMS[2], mDurationMS[3], // duration in ms. 387 | 0, 0, 0, 0, // reserved 388 | 0, 0, 0, 0, // reserved 389 | 0, 0, // layer 390 | 0, 0, // alternate group 391 | 1, 0, // volume = 1.0 392 | 0, 0, // reserved 393 | 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // unity matrix 394 | 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 395 | 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0, 396 | 0, 0, 0, 0, // width 397 | 0, 0, 0, 0 // height 398 | }); 399 | return atom; 400 | } 401 | 402 | private Atom getMDIAAtom() { 403 | Atom atom = new Atom("mdia"); 404 | atom.addChild(getMDHDAtom()); 405 | atom.addChild(getHDLRAtom()); 406 | atom.addChild(getMINFAtom()); 407 | return atom; 408 | } 409 | 410 | private Atom getMDHDAtom() { 411 | Atom atom = new Atom("mdhd", (byte) 0, 0); 412 | atom.setData(new byte[]{ 413 | mTime[0], mTime[1], mTime[2], mTime[3], // creation time. 414 | mTime[0], mTime[1], mTime[2], mTime[3], // modification time. 415 | (byte) (mSampleRate >> 24), (byte) (mSampleRate >> 16), // timescale = Fs => 416 | (byte) (mSampleRate >> 8), (byte) (mSampleRate), // duration expressed in samples. 417 | mNumSamples[0], mNumSamples[1], mNumSamples[2], mNumSamples[3], // duration 418 | 0, 0, // languages 419 | 0, 0 // pre-defined 420 | }); 421 | return atom; 422 | } 423 | 424 | private Atom getHDLRAtom() { 425 | Atom atom = new Atom("hdlr", (byte) 0, 0); 426 | atom.setData(new byte[]{ 427 | 0, 0, 0, 0, // pre-defined 428 | 's', 'o', 'u', 'n', // handler type 429 | 0, 0, 0, 0, // reserved 430 | 0, 0, 0, 0, // reserved 431 | 0, 0, 0, 0, // reserved 432 | 'S', 'o', 'u', 'n', // name (used only for debugging and inspection purposes). 433 | 'd', 'H', 'a', 'n', 434 | 'd', 'l', 'e', '\0' 435 | }); 436 | return atom; 437 | } 438 | 439 | private Atom getMINFAtom() { 440 | Atom atom = new Atom("minf"); 441 | atom.addChild(getSMHDAtom()); 442 | atom.addChild(getDINFAtom()); 443 | atom.addChild(getSTBLAtom()); 444 | return atom; 445 | } 446 | 447 | private Atom getSMHDAtom() { 448 | Atom atom = new Atom("smhd", (byte) 0, 0); 449 | atom.setData(new byte[]{ 450 | 0, 0, // balance (center) 451 | 0, 0 // reserved 452 | }); 453 | return atom; 454 | } 455 | 456 | private Atom getDINFAtom() { 457 | Atom atom = new Atom("dinf"); 458 | atom.addChild(getDREFAtom()); 459 | return atom; 460 | } 461 | 462 | private Atom getDREFAtom() { 463 | Atom atom = new Atom("dref", (byte) 0, 0); 464 | byte[] url = getURLAtom().getBytes(); 465 | byte[] data = new byte[4 + url.length]; 466 | data[3] = 0x01; // entry count = 1 467 | System.arraycopy(url, 0, data, 4, url.length); 468 | atom.setData(data); 469 | return atom; 470 | } 471 | 472 | private Atom getURLAtom() { 473 | Atom atom = new Atom("url ", (byte) 0, 0x01); // flags = 0x01: data is self contained. 474 | return atom; 475 | } 476 | 477 | private Atom getSTBLAtom() { 478 | Atom atom = new Atom("stbl"); 479 | atom.addChild(getSTSDAtom()); 480 | atom.addChild(getSTTSAtom()); 481 | atom.addChild(getSTSCAtom()); 482 | atom.addChild(getSTSZAtom()); 483 | atom.addChild(getSTCOAtom()); 484 | return atom; 485 | } 486 | 487 | private Atom getSTSDAtom() { 488 | Atom atom = new Atom("stsd", (byte) 0, 0); 489 | byte[] mp4a = getMP4AAtom().getBytes(); 490 | byte[] data = new byte[4 + mp4a.length]; 491 | data[3] = 0x01; // entry count = 1 492 | System.arraycopy(mp4a, 0, data, 4, mp4a.length); 493 | atom.setData(data); 494 | return atom; 495 | } 496 | 497 | // See also Part 14 section 5.6.1 of ISO/IEC 14496 for this atom. 498 | private Atom getMP4AAtom() { 499 | Atom atom = new Atom("mp4a"); 500 | byte[] ase = new byte[]{ // Audio Sample Entry data 501 | 0, 0, 0, 0, 0, 0, // reserved 502 | 0, 1, // data reference index 503 | 0, 0, 0, 0, // reserved 504 | 0, 0, 0, 0, // reserved 505 | (byte) (mChannels >> 8), (byte) mChannels, // channel count 506 | 0, 0x10, // sample size 507 | 0, 0, // pre-defined 508 | 0, 0, // reserved 509 | (byte) (mSampleRate >> 8), (byte) (mSampleRate), 0, 0, // sample rate 510 | }; 511 | byte[] esds = getESDSAtom().getBytes(); 512 | byte[] data = new byte[ase.length + esds.length]; 513 | System.arraycopy(ase, 0, data, 0, ase.length); 514 | System.arraycopy(esds, 0, data, ase.length, esds.length); 515 | atom.setData(data); 516 | return atom; 517 | } 518 | 519 | private Atom getESDSAtom() { 520 | Atom atom = new Atom("esds", (byte) 0, 0); 521 | atom.setData(getESDescriptor()); 522 | return atom; 523 | } 524 | 525 | // Returns an ES Descriptor for an ISO/IEC 14496-3 audio stream, AAC LC, 44100Hz, 2 channels, 526 | // 1024 samples per frame per channel. The decoder buffer size is set so that it can contain at 527 | // least 2 frames. (See section 7.2.6.5 of ISO/IEC 14496-1 for more details). 528 | private byte[] getESDescriptor() { 529 | int[] samplingFrequencies = new int[]{96000, 88200, 64000, 48000, 44100, 32000, 24000, 530 | 22050, 16000, 12000, 11025, 8000, 7350}; 531 | // First 5 bytes of the ES Descriptor. 532 | byte[] ESDescriptor_top = new byte[]{0x03, 0x19, 0x00, 0x00, 0x00}; 533 | // First 4 bytes of Decoder Configuration Descriptor. Audio ISO/IEC 14496-3, AudioStream. 534 | byte[] decConfigDescr_top = new byte[]{0x04, 0x11, 0x40, 0x15}; 535 | // Audio Specific Configuration: AAC LC, 1024 samples/frame/channel. 536 | // Sampling frequency and channels configuration are not set yet. 537 | byte[] audioSpecificConfig = new byte[]{0x05, 0x02, 0x10, 0x00}; 538 | byte[] slConfigDescr = new byte[]{0x06, 0x01, 0x02}; // specific for MP4 file. 539 | int offset; 540 | int bufferSize = 0x300; 541 | while (bufferSize < 2 * mMaxFrameSize) { 542 | // TODO(nfaralli): what should be the minimum size of the decoder buffer? 543 | // Should it be a multiple of 256? 544 | bufferSize += 0x100; 545 | } 546 | 547 | // create the Decoder Configuration Descriptor 548 | byte[] decConfigDescr = new byte[2 + decConfigDescr_top[1]]; 549 | System.arraycopy(decConfigDescr_top, 0, decConfigDescr, 0, decConfigDescr_top.length); 550 | offset = decConfigDescr_top.length; 551 | decConfigDescr[offset++] = (byte) ((bufferSize >> 16) & 0xFF); 552 | decConfigDescr[offset++] = (byte) ((bufferSize >> 8) & 0xFF); 553 | decConfigDescr[offset++] = (byte) (bufferSize & 0xFF); 554 | decConfigDescr[offset++] = (byte) ((mBitrate >> 24) & 0xFF); 555 | decConfigDescr[offset++] = (byte) ((mBitrate >> 16) & 0xFF); 556 | decConfigDescr[offset++] = (byte) ((mBitrate >> 8) & 0xFF); 557 | decConfigDescr[offset++] = (byte) (mBitrate & 0xFF); 558 | decConfigDescr[offset++] = (byte) ((mBitrate >> 24) & 0xFF); 559 | decConfigDescr[offset++] = (byte) ((mBitrate >> 16) & 0xFF); 560 | decConfigDescr[offset++] = (byte) ((mBitrate >> 8) & 0xFF); 561 | decConfigDescr[offset++] = (byte) (mBitrate & 0xFF); 562 | int index; 563 | for (index = 0; index < samplingFrequencies.length; index++) { 564 | if (samplingFrequencies[index] == mSampleRate) { 565 | break; 566 | } 567 | } 568 | if (index == samplingFrequencies.length) { 569 | // TODO(nfaralli): log something here. 570 | // Invalid sampling frequency. Default to 44100Hz... 571 | index = 4; 572 | } 573 | audioSpecificConfig[2] |= (byte) ((index >> 1) & 0x07); 574 | audioSpecificConfig[3] |= (byte) (((index & 1) << 7) | ((mChannels & 0x0F) << 3)); 575 | System.arraycopy( 576 | audioSpecificConfig, 0, decConfigDescr, offset, audioSpecificConfig.length); 577 | 578 | // create the ES Descriptor 579 | byte[] ESDescriptor = new byte[2 + ESDescriptor_top[1]]; 580 | System.arraycopy(ESDescriptor_top, 0, ESDescriptor, 0, ESDescriptor_top.length); 581 | offset = ESDescriptor_top.length; 582 | System.arraycopy(decConfigDescr, 0, ESDescriptor, offset, decConfigDescr.length); 583 | offset += decConfigDescr.length; 584 | System.arraycopy(slConfigDescr, 0, ESDescriptor, offset, slConfigDescr.length); 585 | return ESDescriptor; 586 | } 587 | 588 | private Atom getSTTSAtom() { 589 | Atom atom = new Atom("stts", (byte) 0, 0); 590 | int numAudioFrames = mFrameSize.length - 1; 591 | atom.setData(new byte[]{ 592 | 0, 0, 0, 0x02, // entry count 593 | 0, 0, 0, 0x01, // first frame contains no audio 594 | 0, 0, 0, 0, 595 | (byte) ((numAudioFrames >> 24) & 0xFF), (byte) ((numAudioFrames >> 16) & 0xFF), 596 | (byte) ((numAudioFrames >> 8) & 0xFF), (byte) (numAudioFrames & 0xFF), 597 | 0, 0, 0x04, 0, // delay between frames = 1024 samples (cf. timescale = Fs) 598 | }); 599 | return atom; 600 | } 601 | 602 | private Atom getSTSCAtom() { 603 | Atom atom = new Atom("stsc", (byte) 0, 0); 604 | int numFrames = mFrameSize.length; 605 | atom.setData(new byte[]{ 606 | 0, 0, 0, 0x01, // entry count 607 | 0, 0, 0, 0x01, // first chunk 608 | (byte) ((numFrames >> 24) & 0xFF), (byte) ((numFrames >> 16) & 0xFF), // samples per 609 | (byte) ((numFrames >> 8) & 0xFF), (byte) (numFrames & 0xFF), // chunk 610 | 0, 0, 0, 0x01, // sample description index 611 | }); 612 | return atom; 613 | } 614 | 615 | private Atom getSTSZAtom() { 616 | Atom atom = new Atom("stsz", (byte) 0, 0); 617 | int numFrames = mFrameSize.length; 618 | byte[] data = new byte[8 + 4 * numFrames]; 619 | int offset = 0; 620 | data[offset++] = 0; // sample size (=0 => each frame can have a different size) 621 | data[offset++] = 0; 622 | data[offset++] = 0; 623 | data[offset++] = 0; 624 | data[offset++] = (byte) ((numFrames >> 24) & 0xFF); // sample count 625 | data[offset++] = (byte) ((numFrames >> 16) & 0xFF); 626 | data[offset++] = (byte) ((numFrames >> 8) & 0xFF); 627 | data[offset++] = (byte) (numFrames & 0xFF); 628 | for (int size : mFrameSize) { 629 | data[offset++] = (byte) ((size >> 24) & 0xFF); 630 | data[offset++] = (byte) ((size >> 16) & 0xFF); 631 | data[offset++] = (byte) ((size >> 8) & 0xFF); 632 | data[offset++] = (byte) (size & 0xFF); 633 | } 634 | atom.setData(data); 635 | return atom; 636 | } 637 | 638 | private Atom getSTCOAtom() { 639 | Atom atom = new Atom("stco", (byte) 0, 0); 640 | atom.setData(new byte[]{ 641 | 0, 0, 0, 0x01, // entry count 642 | 0, 0, 0, 0 // chunk offset. Set to 0 here. Must be set later. Here it should be 643 | // the size of the complete header, as the AAC stream will follow 644 | // immediately. 645 | }); 646 | return atom; 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/MarkerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | import android.content.Context; 20 | import android.graphics.Canvas; 21 | import android.graphics.Rect; 22 | import android.util.AttributeSet; 23 | import android.view.KeyEvent; 24 | import android.view.MotionEvent; 25 | import android.widget.ImageView; 26 | 27 | /** 28 | * Represents a draggable start or end marker. 29 | *

30 | * Most events are passed back to the client class using a 31 | * listener interface. 32 | *

33 | * This class directly keeps track of its own velocity, though, 34 | * accelerating as the user holds down the left or right arrows 35 | * while this control is focused. 36 | */ 37 | public class MarkerView extends ImageView { 38 | 39 | public interface MarkerListener { 40 | public void markerTouchStart(MarkerView marker, float pos); 41 | 42 | public void markerTouchMove(MarkerView marker, float pos); 43 | 44 | public void markerTouchEnd(MarkerView marker); 45 | 46 | public void markerFocus(MarkerView marker); 47 | 48 | public void markerLeft(MarkerView marker, int velocity); 49 | 50 | public void markerRight(MarkerView marker, int velocity); 51 | 52 | public void markerEnter(MarkerView marker); 53 | 54 | public void markerKeyUp(); 55 | 56 | public void markerDraw(); 57 | } 58 | 59 | ; 60 | 61 | private int mVelocity; 62 | private MarkerListener mListener; 63 | 64 | public MarkerView(Context context, AttributeSet attrs) { 65 | super(context, attrs); 66 | 67 | // Make sure we get keys 68 | setFocusable(true); 69 | 70 | mVelocity = 0; 71 | mListener = null; 72 | } 73 | 74 | public void setListener(MarkerListener listener) { 75 | mListener = listener; 76 | } 77 | 78 | @Override 79 | public boolean onTouchEvent(MotionEvent event) { 80 | switch (event.getAction()) { 81 | case MotionEvent.ACTION_DOWN: 82 | requestFocus(); 83 | // We use raw x because this window itself is going to 84 | // move, which will screw up the "local" coordinates 85 | if (mListener != null) 86 | mListener.markerTouchStart(this, event.getRawX()); 87 | break; 88 | case MotionEvent.ACTION_MOVE: 89 | // We use raw x because this window itself is going to 90 | // move, which will screw up the "local" coordinates 91 | if (mListener != null) 92 | mListener.markerTouchMove(this, event.getRawX()); 93 | break; 94 | case MotionEvent.ACTION_UP: 95 | if (mListener != null) 96 | mListener.markerTouchEnd(this); 97 | break; 98 | } 99 | return true; 100 | } 101 | 102 | @Override 103 | protected void onFocusChanged(boolean gainFocus, int direction, 104 | Rect previouslyFocusedRect) { 105 | if (gainFocus && mListener != null) 106 | mListener.markerFocus(this); 107 | super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 108 | } 109 | 110 | @Override 111 | protected void onDraw(Canvas canvas) { 112 | super.onDraw(canvas); 113 | 114 | if (mListener != null) 115 | mListener.markerDraw(); 116 | } 117 | 118 | @Override 119 | public boolean onKeyDown(int keyCode, KeyEvent event) { 120 | mVelocity++; 121 | int v = (int) Math.sqrt(1 + mVelocity / 2); 122 | if (mListener != null) { 123 | if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { 124 | mListener.markerLeft(this, v); 125 | return true; 126 | } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 127 | mListener.markerRight(this, v); 128 | return true; 129 | } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 130 | mListener.markerEnter(this); 131 | return true; 132 | } 133 | } 134 | 135 | return super.onKeyDown(keyCode, event); 136 | } 137 | 138 | @Override 139 | public boolean onKeyUp(int keyCode, KeyEvent event) { 140 | mVelocity = 0; 141 | if (mListener != null) 142 | mListener.markerKeyUp(); 143 | return super.onKeyDown(keyCode, event); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/SamplePlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | import android.media.AudioFormat; 20 | import android.media.AudioManager; 21 | import android.media.AudioTrack; 22 | 23 | import java.nio.ShortBuffer; 24 | 25 | public class SamplePlayer { 26 | public interface OnCompletionListener { 27 | public void onCompletion(); 28 | } 29 | 30 | ; 31 | 32 | private ShortBuffer mSamples; 33 | private int mSampleRate; 34 | private int mChannels; 35 | private int mNumSamples; // Number of samples per channel. 36 | private AudioTrack mAudioTrack; 37 | private short[] mBuffer; 38 | private int mPlaybackStart; // Start offset, in samples. 39 | private Thread mPlayThread; 40 | private boolean mKeepPlaying; 41 | private OnCompletionListener mListener; 42 | 43 | public SamplePlayer(ShortBuffer samples, int sampleRate, int channels, int numSamples) { 44 | mSamples = samples; 45 | mSampleRate = sampleRate; 46 | mChannels = channels; 47 | mNumSamples = numSamples; 48 | mPlaybackStart = 0; 49 | 50 | int bufferSize = AudioTrack.getMinBufferSize( 51 | mSampleRate, 52 | mChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, 53 | AudioFormat.ENCODING_PCM_16BIT); 54 | // make sure minBufferSize can contain at least 1 second of audio (16 bits sample). 55 | if (bufferSize < mChannels * mSampleRate * 2) { 56 | bufferSize = mChannels * mSampleRate * 2; 57 | } 58 | mBuffer = new short[bufferSize / 2]; // bufferSize is in Bytes. 59 | mAudioTrack = new AudioTrack( 60 | AudioManager.STREAM_MUSIC, 61 | mSampleRate, 62 | mChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, 63 | AudioFormat.ENCODING_PCM_16BIT, 64 | mBuffer.length * 2, 65 | AudioTrack.MODE_STREAM); 66 | // Check when player played all the given data and notify user if mListener is set. 67 | mAudioTrack.setNotificationMarkerPosition(mNumSamples - 1); // Set the marker to the end. 68 | mAudioTrack.setPlaybackPositionUpdateListener( 69 | new AudioTrack.OnPlaybackPositionUpdateListener() { 70 | @Override 71 | public void onPeriodicNotification(AudioTrack track) { 72 | } 73 | 74 | @Override 75 | public void onMarkerReached(AudioTrack track) { 76 | stop(); 77 | if (mListener != null) { 78 | mListener.onCompletion(); 79 | } 80 | } 81 | }); 82 | mPlayThread = null; 83 | mKeepPlaying = true; 84 | mListener = null; 85 | } 86 | 87 | public SamplePlayer(SoundFile sf) { 88 | this(sf.getSamples(), sf.getSampleRate(), sf.getChannels(), sf.getNumSamples()); 89 | } 90 | 91 | public void setOnCompletionListener(OnCompletionListener listener) { 92 | mListener = listener; 93 | } 94 | 95 | public boolean isPlaying() { 96 | return mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING; 97 | } 98 | 99 | public boolean isPaused() { 100 | return mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED; 101 | } 102 | 103 | public void start() { 104 | if (isPlaying()) { 105 | return; 106 | } 107 | mKeepPlaying = true; 108 | mAudioTrack.flush(); 109 | mAudioTrack.play(); 110 | // Setting thread feeding the audio samples to the audio hardware. 111 | // (Assumes mChannels = 1 or 2). 112 | mPlayThread = new Thread() { 113 | public void run() { 114 | int position = mPlaybackStart * mChannels; 115 | mSamples.position(position); 116 | int limit = mNumSamples * mChannels; 117 | while (mSamples.position() < limit && mKeepPlaying) { 118 | int numSamplesLeft = limit - mSamples.position(); 119 | if (numSamplesLeft >= mBuffer.length) { 120 | mSamples.get(mBuffer); 121 | } else { 122 | for (int i = numSamplesLeft; i < mBuffer.length; i++) { 123 | mBuffer[i] = 0; 124 | } 125 | mSamples.get(mBuffer, 0, numSamplesLeft); 126 | } 127 | // TODO(nfaralli): use the write method that takes a ByteBuffer as argument. 128 | mAudioTrack.write(mBuffer, 0, mBuffer.length); 129 | } 130 | } 131 | }; 132 | mPlayThread.start(); 133 | } 134 | 135 | public void pause() { 136 | if (isPlaying()) { 137 | mAudioTrack.pause(); 138 | // mAudioTrack.write() should block if it cannot write. 139 | } 140 | } 141 | 142 | public void stop() { 143 | if (isPlaying() || isPaused()) { 144 | mKeepPlaying = false; 145 | mAudioTrack.pause(); // pause() stops the playback immediately. 146 | mAudioTrack.stop(); // Unblock mAudioTrack.write() to avoid deadlocks. 147 | if (mPlayThread != null) { 148 | try { 149 | mPlayThread.join(); 150 | } catch (InterruptedException e) { 151 | } 152 | mPlayThread = null; 153 | } 154 | mAudioTrack.flush(); // just in case... 155 | } 156 | } 157 | 158 | public void release() { 159 | stop(); 160 | mAudioTrack.release(); 161 | } 162 | 163 | public void seekTo(int msec) { 164 | boolean wasPlaying = isPlaying(); 165 | stop(); 166 | mPlaybackStart = (int) (msec * (mSampleRate / 1000.0)); 167 | if (mPlaybackStart > mNumSamples) { 168 | mPlaybackStart = mNumSamples; // Nothing to play... 169 | } 170 | mAudioTrack.setNotificationMarkerPosition(mNumSamples - 1 - mPlaybackStart); 171 | if (wasPlaying) { 172 | start(); 173 | } 174 | } 175 | 176 | public int getCurrentPosition() { 177 | return (int) ((mPlaybackStart + mAudioTrack.getPlaybackHeadPosition()) * 178 | (1000.0 / mSampleRate)); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/SongMetadataReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | import android.app.Activity; 20 | import android.database.Cursor; 21 | import android.net.Uri; 22 | import android.provider.MediaStore; 23 | 24 | import java.util.HashMap; 25 | 26 | public class SongMetadataReader { 27 | public Uri GENRES_URI = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; 28 | public Activity mActivity = null; 29 | public String mFilename = ""; 30 | public String mTitle = ""; 31 | public String mArtist = ""; 32 | public String mAlbum = ""; 33 | public String mGenre = ""; 34 | public int mYear = -1; 35 | 36 | public SongMetadataReader(Activity activity, String filename) { 37 | mActivity = activity; 38 | mFilename = filename; 39 | mTitle = getBasename(filename); 40 | try { 41 | ReadMetadata(); 42 | } catch (Exception e) { 43 | } 44 | } 45 | 46 | private void ReadMetadata() { 47 | // Get a map from genre ids to names 48 | HashMap genreIdMap = new HashMap(); 49 | Cursor c = mActivity.getContentResolver().query( 50 | GENRES_URI, 51 | new String[]{ 52 | MediaStore.Audio.Genres._ID, 53 | MediaStore.Audio.Genres.NAME}, 54 | null, null, null); 55 | for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) { 56 | genreIdMap.put(c.getString(0), c.getString(1)); 57 | } 58 | c.close(); 59 | mGenre = ""; 60 | for (String genreId : genreIdMap.keySet()) { 61 | c = mActivity.getContentResolver().query( 62 | makeGenreUri(genreId), 63 | new String[]{MediaStore.Audio.Media.DATA}, 64 | MediaStore.Audio.Media.DATA + " LIKE \"" + mFilename + "\"", 65 | null, null); 66 | if (c.getCount() != 0) { 67 | mGenre = genreIdMap.get(genreId); 68 | break; 69 | } 70 | c.close(); 71 | c = null; 72 | } 73 | 74 | Uri uri = MediaStore.Audio.Media.getContentUriForPath(mFilename); 75 | c = mActivity.getContentResolver().query( 76 | uri, 77 | new String[]{ 78 | MediaStore.Audio.Media._ID, 79 | MediaStore.Audio.Media.TITLE, 80 | MediaStore.Audio.Media.ARTIST, 81 | MediaStore.Audio.Media.ALBUM, 82 | MediaStore.Audio.Media.YEAR, 83 | MediaStore.Audio.Media.DATA}, 84 | MediaStore.Audio.Media.DATA + " LIKE \"" + mFilename + "\"", 85 | null, null); 86 | if (c.getCount() == 0) { 87 | mTitle = getBasename(mFilename); 88 | mArtist = ""; 89 | mAlbum = ""; 90 | mYear = -1; 91 | return; 92 | } 93 | c.moveToFirst(); 94 | mTitle = getStringFromColumn(c, MediaStore.Audio.Media.TITLE); 95 | if (mTitle == null || mTitle.length() == 0) { 96 | mTitle = getBasename(mFilename); 97 | } 98 | mArtist = getStringFromColumn(c, MediaStore.Audio.Media.ARTIST); 99 | mAlbum = getStringFromColumn(c, MediaStore.Audio.Media.ALBUM); 100 | mYear = getIntegerFromColumn(c, MediaStore.Audio.Media.YEAR); 101 | c.close(); 102 | } 103 | 104 | private Uri makeGenreUri(String genreId) { 105 | String CONTENTDIR = MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY; 106 | return Uri.parse( 107 | new StringBuilder() 108 | .append(GENRES_URI.toString()) 109 | .append("/") 110 | .append(genreId) 111 | .append("/") 112 | .append(CONTENTDIR) 113 | .toString()); 114 | } 115 | 116 | private String getStringFromColumn(Cursor c, String columnName) { 117 | int index = c.getColumnIndexOrThrow(columnName); 118 | String value = c.getString(index); 119 | if (value != null && value.length() > 0) { 120 | return value; 121 | } else { 122 | return null; 123 | } 124 | } 125 | 126 | private int getIntegerFromColumn(Cursor c, String columnName) { 127 | int index = c.getColumnIndexOrThrow(columnName); 128 | Integer value = c.getInt(index); 129 | if (value != null) { 130 | return value; 131 | } else { 132 | return -1; 133 | } 134 | } 135 | 136 | private String getBasename(String filename) { 137 | return filename.substring(filename.lastIndexOf('/') + 1, 138 | filename.lastIndexOf('.')); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/SoundFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | import android.media.AudioFormat; 20 | import android.media.AudioRecord; 21 | import android.media.MediaCodec; 22 | import android.media.MediaExtractor; 23 | import android.media.MediaFormat; 24 | import android.media.MediaRecorder; 25 | import android.os.Environment; 26 | import android.util.Log; 27 | 28 | import java.io.BufferedWriter; 29 | import java.io.File; 30 | import java.io.FileOutputStream; 31 | import java.io.FileWriter; 32 | import java.io.IOException; 33 | import java.io.PrintWriter; 34 | import java.io.StringWriter; 35 | import java.nio.ByteBuffer; 36 | import java.nio.ByteOrder; 37 | import java.nio.ShortBuffer; 38 | import java.util.Arrays; 39 | 40 | public class SoundFile { 41 | private ProgressListener mProgressListener = null; 42 | private File mInputFile = null; 43 | 44 | // Member variables representing frame data 45 | private String mFileType; 46 | private int mFileSize; 47 | private int mAvgBitRate; // Average bit rate in kbps. 48 | private int mSampleRate; 49 | private int mChannels; 50 | private int mNumSamples; // total number of samples per channel in audio file 51 | private ByteBuffer mDecodedBytes; // Raw audio data 52 | private ShortBuffer mDecodedSamples; // shared buffer with mDecodedBytes. 53 | // mDecodedSamples has the following format: 54 | // {s1c1, s1c2, ..., s1cM, s2c1, ..., s2cM, ..., sNc1, ..., sNcM} 55 | // where sicj is the ith sample of the jth channel (a sample is a signed short) 56 | // M is the number of channels (e.g. 2 for stereo) and N is the number of samples per channel. 57 | 58 | // Member variables for hack (making it work with old version, until app just uses the samples). 59 | private int mNumFrames; 60 | private int[] mFrameGains; 61 | private int[] mFrameLens; 62 | private int[] mFrameOffsets; 63 | 64 | // Progress listener interface. 65 | public interface ProgressListener { 66 | /** 67 | * Will be called by the SoundFile class periodically 68 | * with values between 0.0 and 1.0. Return true to continue 69 | * loading the file or recording the audio, and false to cancel or stop recording. 70 | */ 71 | boolean reportProgress(double fractionComplete); 72 | } 73 | 74 | // Custom exception for invalid inputs. 75 | public class InvalidInputException extends Exception { 76 | // Serial version ID generated by Eclipse. 77 | private static final long serialVersionUID = -2505698991597837165L; 78 | 79 | public InvalidInputException(String message) { 80 | super(message); 81 | } 82 | } 83 | 84 | // TODO(nfaralli): what is the real list of supported extensions? Is it device dependent? 85 | public static String[] getSupportedExtensions() { 86 | return new String[]{"mp3", "wav", "3gpp", "3gp", "amr", "aac", "m4a", "ogg"}; 87 | } 88 | 89 | public static boolean isFilenameSupported(String filename) { 90 | String[] extensions = getSupportedExtensions(); 91 | for (int i = 0; i < extensions.length; i++) { 92 | if (filename.endsWith("." + extensions[i])) { 93 | return true; 94 | } 95 | } 96 | return false; 97 | } 98 | 99 | // Create and return a SoundFile object using the file fileName. 100 | public static SoundFile create(String fileName, 101 | ProgressListener progressListener) 102 | throws java.io.FileNotFoundException, 103 | IOException, InvalidInputException { 104 | // First check that the file exists and that its extension is supported. 105 | File f = new File(fileName); 106 | if (!f.exists()) { 107 | throw new java.io.FileNotFoundException(fileName); 108 | } 109 | String name = f.getName().toLowerCase(); 110 | String[] components = name.split("\\."); 111 | if (components.length < 2) { 112 | return null; 113 | } 114 | if (!Arrays.asList(getSupportedExtensions()).contains(components[components.length - 1])) { 115 | return null; 116 | } 117 | SoundFile soundFile = new SoundFile(); 118 | soundFile.setProgressListener(progressListener); 119 | soundFile.ReadFile(f); 120 | return soundFile; 121 | } 122 | 123 | // Create and return a SoundFile object by recording a mono audio stream. 124 | public static SoundFile record(ProgressListener progressListener) { 125 | if (progressListener == null) { 126 | // must have a progessListener to stop the recording. 127 | return null; 128 | } 129 | SoundFile soundFile = new SoundFile(); 130 | soundFile.setProgressListener(progressListener); 131 | soundFile.RecordAudio(); 132 | return soundFile; 133 | } 134 | 135 | public String getFiletype() { 136 | return mFileType; 137 | } 138 | 139 | public int getFileSizeBytes() { 140 | return mFileSize; 141 | } 142 | 143 | public int getAvgBitrateKbps() { 144 | return mAvgBitRate; 145 | } 146 | 147 | public int getSampleRate() { 148 | return mSampleRate; 149 | } 150 | 151 | public int getChannels() { 152 | return mChannels; 153 | } 154 | 155 | public int getNumSamples() { 156 | return mNumSamples; // Number of samples per channel. 157 | } 158 | 159 | // Should be removed when the app will use directly the samples instead of the frames. 160 | public int getNumFrames() { 161 | return mNumFrames; 162 | } 163 | 164 | // Should be removed when the app will use directly the samples instead of the frames. 165 | public int getSamplesPerFrame() { 166 | return 1024; // just a fixed value here... 167 | } 168 | 169 | // Should be removed when the app will use directly the samples instead of the frames. 170 | public int[] getFrameGains() { 171 | return mFrameGains; 172 | } 173 | 174 | public ShortBuffer getSamples() { 175 | if (mDecodedSamples != null) { 176 | return mDecodedSamples; 177 | // return mDecodedSamples.asReadOnlyBuffer(); 178 | } else { 179 | return null; 180 | } 181 | } 182 | 183 | // A SoundFile object should only be created using the static methods create() and record(). 184 | private SoundFile() { 185 | } 186 | 187 | private void setProgressListener(ProgressListener progressListener) { 188 | mProgressListener = progressListener; 189 | } 190 | 191 | private void ReadFile(File inputFile) 192 | throws java.io.FileNotFoundException, 193 | IOException, InvalidInputException { 194 | MediaExtractor extractor = new MediaExtractor(); 195 | MediaFormat format = null; 196 | int i; 197 | 198 | mInputFile = inputFile; 199 | String[] components = mInputFile.getPath().split("\\."); 200 | mFileType = components[components.length - 1]; 201 | mFileSize = (int) mInputFile.length(); 202 | extractor.setDataSource(mInputFile.getPath()); 203 | int numTracks = extractor.getTrackCount(); 204 | // find and select the first audio track present in the file. 205 | for (i = 0; i < numTracks; i++) { 206 | format = extractor.getTrackFormat(i); 207 | if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) { 208 | extractor.selectTrack(i); 209 | break; 210 | } 211 | } 212 | if (i == numTracks) { 213 | throw new InvalidInputException("No audio track found in " + mInputFile); 214 | } 215 | mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); 216 | mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); 217 | // Expected total number of samples per channel. 218 | int expectedNumSamples = 219 | (int) ((format.getLong(MediaFormat.KEY_DURATION) / 1000000.f) * mSampleRate + 0.5f); 220 | 221 | MediaCodec codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)); 222 | codec.configure(format, null, null, 0); 223 | codec.start(); 224 | 225 | int decodedSamplesSize = 0; // size of the output buffer containing decoded samples. 226 | byte[] decodedSamples = null; 227 | ByteBuffer[] inputBuffers = codec.getInputBuffers(); 228 | ByteBuffer[] outputBuffers = codec.getOutputBuffers(); 229 | int sample_size; 230 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 231 | long presentation_time; 232 | int tot_size_read = 0; 233 | boolean done_reading = false; 234 | 235 | // Set the size of the decoded samples buffer to 1MB (~6sec of a stereo stream at 44.1kHz). 236 | // For longer streams, the buffer size will be increased later on, calculating a rough 237 | // estimate of the total size needed to store all the samples in order to resize the buffer 238 | // only once. 239 | mDecodedBytes = ByteBuffer.allocate(1 << 20); 240 | Boolean firstSampleData = true; 241 | while (true) { 242 | // read data from file and feed it to the decoder input buffers. 243 | int inputBufferIndex = codec.dequeueInputBuffer(100); 244 | if (!done_reading && inputBufferIndex >= 0) { 245 | sample_size = extractor.readSampleData(inputBuffers[inputBufferIndex], 0); 246 | if (firstSampleData 247 | && format.getString(MediaFormat.KEY_MIME).equals("audio/mp4a-latm") 248 | && sample_size == 2) { 249 | // For some reasons on some devices (e.g. the Samsung S3) you should not 250 | // provide the first two bytes of an AAC stream, otherwise the MediaCodec will 251 | // crash. These two bytes do not contain music data but basic info on the 252 | // stream (e.g. channel configuration and sampling frequency), and skipping them 253 | // seems OK with other devices (MediaCodec has already been configured and 254 | // already knows these parameters). 255 | extractor.advance(); 256 | tot_size_read += sample_size; 257 | } else if (sample_size < 0) { 258 | // All samples have been read. 259 | codec.queueInputBuffer( 260 | inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM); 261 | done_reading = true; 262 | } else { 263 | presentation_time = extractor.getSampleTime(); 264 | codec.queueInputBuffer(inputBufferIndex, 0, sample_size, presentation_time, 0); 265 | extractor.advance(); 266 | tot_size_read += sample_size; 267 | if (mProgressListener != null) { 268 | if (!mProgressListener.reportProgress((float) (tot_size_read) / mFileSize)) { 269 | // We are asked to stop reading the file. Returning immediately. The 270 | // SoundFile object is invalid and should NOT be used afterward! 271 | extractor.release(); 272 | extractor = null; 273 | codec.stop(); 274 | codec.release(); 275 | codec = null; 276 | return; 277 | } 278 | } 279 | } 280 | firstSampleData = false; 281 | } 282 | 283 | // Get decoded stream from the decoder output buffers. 284 | int outputBufferIndex = codec.dequeueOutputBuffer(info, 100); 285 | if (outputBufferIndex >= 0 && info.size > 0) { 286 | if (decodedSamplesSize < info.size) { 287 | decodedSamplesSize = info.size; 288 | decodedSamples = new byte[decodedSamplesSize]; 289 | } 290 | outputBuffers[outputBufferIndex].get(decodedSamples, 0, info.size); 291 | outputBuffers[outputBufferIndex].clear(); 292 | // Check if buffer is big enough. Resize it if it's too small. 293 | if (mDecodedBytes.remaining() < info.size) { 294 | // Getting a rough estimate of the total size, allocate 20% more, and 295 | // make sure to allocate at least 5MB more than the initial size. 296 | int position = mDecodedBytes.position(); 297 | int newSize = (int) ((position * (1.0 * mFileSize / tot_size_read)) * 1.2); 298 | if (newSize - position < info.size + 5 * (1 << 20)) { 299 | newSize = position + info.size + 5 * (1 << 20); 300 | } 301 | ByteBuffer newDecodedBytes = null; 302 | // Try to allocate memory. If we are OOM, try to run the garbage collector. 303 | int retry = 10; 304 | while (retry > 0) { 305 | try { 306 | newDecodedBytes = ByteBuffer.allocate(newSize); 307 | break; 308 | } catch (OutOfMemoryError oome) { 309 | // setting android:largeHeap="true" in seem to help not 310 | // reaching this section. 311 | retry--; 312 | } 313 | } 314 | if (retry == 0) { 315 | // Failed to allocate memory... Stop reading more data and finalize the 316 | // instance with the data decoded so far. 317 | break; 318 | } 319 | //ByteBuffer newDecodedBytes = ByteBuffer.allocate(newSize); 320 | mDecodedBytes.rewind(); 321 | newDecodedBytes.put(mDecodedBytes); 322 | mDecodedBytes = newDecodedBytes; 323 | mDecodedBytes.position(position); 324 | } 325 | mDecodedBytes.put(decodedSamples, 0, info.size); 326 | codec.releaseOutputBuffer(outputBufferIndex, false); 327 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 328 | outputBuffers = codec.getOutputBuffers(); 329 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 330 | // Subsequent data will conform to new format. 331 | // We could check that codec.getOutputFormat(), which is the new output format, 332 | // is what we expect. 333 | } 334 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 335 | || (mDecodedBytes.position() / (2 * mChannels)) >= expectedNumSamples) { 336 | // We got all the decoded data from the decoder. Stop here. 337 | // Theoretically dequeueOutputBuffer(info, ...) should have set info.flags to 338 | // MediaCodec.BUFFER_FLAG_END_OF_STREAM. However some phones (e.g. Samsung S3) 339 | // won't do that for some files (e.g. with mono AAC files), in which case subsequent 340 | // calls to dequeueOutputBuffer may result in the application crashing, without 341 | // even an exception being thrown... Hence the second check. 342 | // (for mono AAC files, the S3 will actually double each sample, as if the stream 343 | // was stereo. The resulting stream is half what it's supposed to be and with a much 344 | // lower pitch.) 345 | break; 346 | } 347 | } 348 | mNumSamples = mDecodedBytes.position() / (mChannels * 2); // One sample = 2 bytes. 349 | mDecodedBytes.rewind(); 350 | mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN); 351 | mDecodedSamples = mDecodedBytes.asShortBuffer(); 352 | mAvgBitRate = (int) ((mFileSize * 8) * ((float) mSampleRate / mNumSamples) / 1000); 353 | 354 | extractor.release(); 355 | extractor = null; 356 | codec.stop(); 357 | codec.release(); 358 | codec = null; 359 | 360 | // Temporary hack to make it work with the old version. 361 | mNumFrames = mNumSamples / getSamplesPerFrame(); 362 | if (mNumSamples % getSamplesPerFrame() != 0) { 363 | mNumFrames++; 364 | } 365 | mFrameGains = new int[mNumFrames]; 366 | mFrameLens = new int[mNumFrames]; 367 | mFrameOffsets = new int[mNumFrames]; 368 | int j; 369 | int gain, value; 370 | int frameLens = (int) ((1000 * mAvgBitRate / 8) * 371 | ((float) getSamplesPerFrame() / mSampleRate)); 372 | for (i = 0; i < mNumFrames; i++) { 373 | gain = -1; 374 | for (j = 0; j < getSamplesPerFrame(); j++) { 375 | value = 0; 376 | for (int k = 0; k < mChannels; k++) { 377 | if (mDecodedSamples.remaining() > 0) { 378 | value += Math.abs(mDecodedSamples.get()); 379 | } 380 | } 381 | value /= mChannels; 382 | if (gain < value) { 383 | gain = value; 384 | } 385 | } 386 | mFrameGains[i] = (int) Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)... 387 | mFrameLens[i] = frameLens; // totally not accurate... 388 | mFrameOffsets[i] = (int) (i * (1000 * mAvgBitRate / 8) * // = i * frameLens 389 | ((float) getSamplesPerFrame() / mSampleRate)); 390 | } 391 | mDecodedSamples.rewind(); 392 | // DumpSamples(); // Uncomment this line to dump the samples in a TSV file. 393 | } 394 | 395 | private void RecordAudio() { 396 | if (mProgressListener == null) { 397 | // A progress listener is mandatory here, as it will let us know when to stop recording. 398 | return; 399 | } 400 | mInputFile = null; 401 | mFileType = "raw"; 402 | mFileSize = 0; 403 | mSampleRate = 44100; 404 | mChannels = 1; // record mono audio. 405 | short[] buffer = new short[1024]; // buffer contains 1 mono frame of 1024 16 bits samples 406 | int minBufferSize = AudioRecord.getMinBufferSize( 407 | mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 408 | // make sure minBufferSize can contain at least 1 second of audio (16 bits sample). 409 | if (minBufferSize < mSampleRate * 2) { 410 | minBufferSize = mSampleRate * 2; 411 | } 412 | AudioRecord audioRecord = new AudioRecord( 413 | MediaRecorder.AudioSource.DEFAULT, 414 | mSampleRate, 415 | AudioFormat.CHANNEL_IN_MONO, 416 | AudioFormat.ENCODING_PCM_16BIT, 417 | minBufferSize 418 | ); 419 | 420 | // Allocate memory for 20 seconds first. Reallocate later if more is needed. 421 | mDecodedBytes = ByteBuffer.allocate(20 * mSampleRate * 2); 422 | mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN); 423 | mDecodedSamples = mDecodedBytes.asShortBuffer(); 424 | audioRecord.startRecording(); 425 | while (true) { 426 | // check if mDecodedSamples can contain 1024 additional samples. 427 | if (mDecodedSamples.remaining() < 1024) { 428 | // Try to allocate memory for 10 additional seconds. 429 | int newCapacity = mDecodedBytes.capacity() + 10 * mSampleRate * 2; 430 | ByteBuffer newDecodedBytes = null; 431 | try { 432 | newDecodedBytes = ByteBuffer.allocate(newCapacity); 433 | } catch (OutOfMemoryError oome) { 434 | break; 435 | } 436 | int position = mDecodedSamples.position(); 437 | mDecodedBytes.rewind(); 438 | newDecodedBytes.put(mDecodedBytes); 439 | mDecodedBytes = newDecodedBytes; 440 | mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN); 441 | mDecodedBytes.rewind(); 442 | mDecodedSamples = mDecodedBytes.asShortBuffer(); 443 | mDecodedSamples.position(position); 444 | } 445 | // TODO(nfaralli): maybe use the read method that takes a direct ByteBuffer argument. 446 | audioRecord.read(buffer, 0, buffer.length); 447 | mDecodedSamples.put(buffer); 448 | // Let the progress listener know how many seconds have been recorded. 449 | // The returned value tells us if we should keep recording or stop. 450 | if (!mProgressListener.reportProgress( 451 | (float) (mDecodedSamples.position()) / mSampleRate)) { 452 | break; 453 | } 454 | } 455 | audioRecord.stop(); 456 | audioRecord.release(); 457 | mNumSamples = mDecodedSamples.position(); 458 | mDecodedSamples.rewind(); 459 | mDecodedBytes.rewind(); 460 | mAvgBitRate = mSampleRate * 16 / 1000; 461 | 462 | // Temporary hack to make it work with the old version. 463 | mNumFrames = mNumSamples / getSamplesPerFrame(); 464 | if (mNumSamples % getSamplesPerFrame() != 0) { 465 | mNumFrames++; 466 | } 467 | mFrameGains = new int[mNumFrames]; 468 | mFrameLens = null; // not needed for recorded audio 469 | mFrameOffsets = null; // not needed for recorded audio 470 | int i, j; 471 | int gain, value; 472 | for (i = 0; i < mNumFrames; i++) { 473 | gain = -1; 474 | for (j = 0; j < getSamplesPerFrame(); j++) { 475 | if (mDecodedSamples.remaining() > 0) { 476 | value = Math.abs(mDecodedSamples.get()); 477 | } else { 478 | value = 0; 479 | } 480 | if (gain < value) { 481 | gain = value; 482 | } 483 | } 484 | mFrameGains[i] = (int) Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)... 485 | } 486 | mDecodedSamples.rewind(); 487 | // DumpSamples(); // Uncomment this line to dump the samples in a TSV file. 488 | } 489 | 490 | // should be removed in the near future... 491 | public void WriteFile(File outputFile, int startFrame, int numFrames) 492 | throws IOException { 493 | float startTime = (float) startFrame * getSamplesPerFrame() / mSampleRate; 494 | float endTime = (float) (startFrame + numFrames) * getSamplesPerFrame() / mSampleRate; 495 | WriteFile(outputFile, startTime, endTime); 496 | } 497 | 498 | public void WriteFile(File outputFile, float startTime, float endTime) 499 | throws IOException { 500 | int startOffset = (int) (startTime * mSampleRate) * 2 * mChannels; 501 | int numSamples = (int) ((endTime - startTime) * mSampleRate); 502 | // Some devices have problems reading mono AAC files (e.g. Samsung S3). Making it stereo. 503 | int numChannels = (mChannels == 1) ? 2 : mChannels; 504 | 505 | String mimeType = "audio/mp4a-latm"; 506 | int bitrate = 64000 * numChannels; // rule of thumb for a good quality: 64kbps per channel. 507 | MediaCodec codec = MediaCodec.createEncoderByType(mimeType); 508 | MediaFormat format = MediaFormat.createAudioFormat(mimeType, mSampleRate, numChannels); 509 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 510 | codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 511 | codec.start(); 512 | 513 | // Get an estimation of the encoded data based on the bitrate. Add 10% to it. 514 | int estimatedEncodedSize = (int) ((endTime - startTime) * (bitrate / 8) * 1.1); 515 | ByteBuffer encodedBytes = ByteBuffer.allocate(estimatedEncodedSize); 516 | ByteBuffer[] inputBuffers = codec.getInputBuffers(); 517 | ByteBuffer[] outputBuffers = codec.getOutputBuffers(); 518 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 519 | boolean done_reading = false; 520 | long presentation_time = 0; 521 | 522 | int frame_size = 1024; // number of samples per frame per channel for an mp4 (AAC) stream. 523 | byte buffer[] = new byte[frame_size * numChannels * 2]; // a sample is coded with a short. 524 | mDecodedBytes.position(startOffset); 525 | numSamples += (2 * frame_size); // Adding 2 frames, Cf. priming frames for AAC. 526 | int tot_num_frames = 1 + (numSamples / frame_size); // first AAC frame = 2 bytes 527 | if (numSamples % frame_size != 0) { 528 | tot_num_frames++; 529 | } 530 | int[] frame_sizes = new int[tot_num_frames]; 531 | int num_out_frames = 0; 532 | int num_frames = 0; 533 | int num_samples_left = numSamples; 534 | int encodedSamplesSize = 0; // size of the output buffer containing the encoded samples. 535 | byte[] encodedSamples = null; 536 | while (true) { 537 | // Feed the samples to the encoder. 538 | int inputBufferIndex = codec.dequeueInputBuffer(100); 539 | if (!done_reading && inputBufferIndex >= 0) { 540 | if (num_samples_left <= 0) { 541 | // All samples have been read. 542 | codec.queueInputBuffer( 543 | inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM); 544 | done_reading = true; 545 | } else { 546 | inputBuffers[inputBufferIndex].clear(); 547 | if (buffer.length > inputBuffers[inputBufferIndex].remaining()) { 548 | // Input buffer is smaller than one frame. This should never happen. 549 | continue; 550 | } 551 | // bufferSize is a hack to create a stereo file from a mono stream. 552 | int bufferSize = (mChannels == 1) ? (buffer.length / 2) : buffer.length; 553 | if (mDecodedBytes.remaining() < bufferSize) { 554 | for (int i = mDecodedBytes.remaining(); i < bufferSize; i++) { 555 | buffer[i] = 0; // pad with extra 0s to make a full frame. 556 | } 557 | mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining()); 558 | } else { 559 | mDecodedBytes.get(buffer, 0, bufferSize); 560 | } 561 | if (mChannels == 1) { 562 | for (int i = bufferSize - 1; i >= 1; i -= 2) { 563 | buffer[2 * i + 1] = buffer[i]; 564 | buffer[2 * i] = buffer[i - 1]; 565 | buffer[2 * i - 1] = buffer[2 * i + 1]; 566 | buffer[2 * i - 2] = buffer[2 * i]; 567 | } 568 | } 569 | num_samples_left -= frame_size; 570 | inputBuffers[inputBufferIndex].put(buffer); 571 | presentation_time = (long) (((num_frames++) * frame_size * 1e6) / mSampleRate); 572 | codec.queueInputBuffer( 573 | inputBufferIndex, 0, buffer.length, presentation_time, 0); 574 | } 575 | } 576 | 577 | // Get the encoded samples from the encoder. 578 | int outputBufferIndex = codec.dequeueOutputBuffer(info, 100); 579 | if (outputBufferIndex >= 0 && info.size > 0 && info.presentationTimeUs >= 0) { 580 | if (num_out_frames < frame_sizes.length) { 581 | frame_sizes[num_out_frames++] = info.size; 582 | } 583 | if (encodedSamplesSize < info.size) { 584 | encodedSamplesSize = info.size; 585 | encodedSamples = new byte[encodedSamplesSize]; 586 | } 587 | outputBuffers[outputBufferIndex].get(encodedSamples, 0, info.size); 588 | outputBuffers[outputBufferIndex].clear(); 589 | codec.releaseOutputBuffer(outputBufferIndex, false); 590 | if (encodedBytes.remaining() < info.size) { // Hopefully this should not happen. 591 | estimatedEncodedSize = (int) (estimatedEncodedSize * 1.2); // Add 20%. 592 | ByteBuffer newEncodedBytes = ByteBuffer.allocate(estimatedEncodedSize); 593 | int position = encodedBytes.position(); 594 | encodedBytes.rewind(); 595 | newEncodedBytes.put(encodedBytes); 596 | encodedBytes = newEncodedBytes; 597 | encodedBytes.position(position); 598 | } 599 | encodedBytes.put(encodedSamples, 0, info.size); 600 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 601 | outputBuffers = codec.getOutputBuffers(); 602 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 603 | // Subsequent data will conform to new format. 604 | // We could check that codec.getOutputFormat(), which is the new output format, 605 | // is what we expect. 606 | } 607 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 608 | // We got all the encoded data from the encoder. 609 | break; 610 | } 611 | } 612 | int encoded_size = encodedBytes.position(); 613 | encodedBytes.rewind(); 614 | codec.stop(); 615 | codec.release(); 616 | codec = null; 617 | 618 | // Write the encoded stream to the file, 4kB at a time. 619 | buffer = new byte[4096]; 620 | try { 621 | FileOutputStream outputStream = new FileOutputStream(outputFile); 622 | outputStream.write( 623 | MP4Header.getMP4Header(mSampleRate, numChannels, frame_sizes, bitrate)); 624 | while (encoded_size - encodedBytes.position() > buffer.length) { 625 | encodedBytes.get(buffer); 626 | outputStream.write(buffer); 627 | } 628 | int remaining = encoded_size - encodedBytes.position(); 629 | if (remaining > 0) { 630 | encodedBytes.get(buffer, 0, remaining); 631 | outputStream.write(buffer, 0, remaining); 632 | } 633 | outputStream.close(); 634 | } catch (IOException e) { 635 | Log.e("Ringdroid", "Failed to create the .m4a file."); 636 | Log.e("Ringdroid", getStackTrace(e)); 637 | } 638 | } 639 | 640 | // Method used to swap the left and right channels (needed for stereo WAV files). 641 | // buffer contains the PCM data: {sample 1 right, sample 1 left, sample 2 right, etc.} 642 | // The size of a sample is assumed to be 16 bits (for a single channel). 643 | // When done, buffer will contain {sample 1 left, sample 1 right, sample 2 left, etc.} 644 | private void swapLeftRightChannels(byte[] buffer) { 645 | byte left[] = new byte[2]; 646 | byte right[] = new byte[2]; 647 | if (buffer.length % 4 != 0) { // 2 channels, 2 bytes per sample (for one channel). 648 | // Invalid buffer size. 649 | return; 650 | } 651 | for (int offset = 0; offset < buffer.length; offset += 4) { 652 | left[0] = buffer[offset]; 653 | left[1] = buffer[offset + 1]; 654 | right[0] = buffer[offset + 2]; 655 | right[1] = buffer[offset + 3]; 656 | buffer[offset] = right[0]; 657 | buffer[offset + 1] = right[1]; 658 | buffer[offset + 2] = left[0]; 659 | buffer[offset + 3] = left[1]; 660 | } 661 | } 662 | 663 | // should be removed in the near future... 664 | public void WriteWAVFile(File outputFile, int startFrame, int numFrames) 665 | throws IOException { 666 | float startTime = (float) startFrame * getSamplesPerFrame() / mSampleRate; 667 | float endTime = (float) (startFrame + numFrames) * getSamplesPerFrame() / mSampleRate; 668 | WriteWAVFile(outputFile, startTime, endTime); 669 | } 670 | 671 | public void WriteWAVFile(File outputFile, float startTime, float endTime) 672 | throws IOException { 673 | int startOffset = (int) (startTime * mSampleRate) * 2 * mChannels; 674 | int numSamples = (int) ((endTime - startTime) * mSampleRate); 675 | 676 | // Start by writing the RIFF header. 677 | FileOutputStream outputStream = new FileOutputStream(outputFile); 678 | outputStream.write(WAVHeader.getWAVHeader(mSampleRate, mChannels, numSamples)); 679 | 680 | // Write the samples to the file, 1024 at a time. 681 | byte buffer[] = new byte[1024 * mChannels * 2]; // Each sample is coded with a short. 682 | mDecodedBytes.position(startOffset); 683 | int numBytesLeft = numSamples * mChannels * 2; 684 | while (numBytesLeft >= buffer.length) { 685 | if (mDecodedBytes.remaining() < buffer.length) { 686 | // This should not happen. 687 | for (int i = mDecodedBytes.remaining(); i < buffer.length; i++) { 688 | buffer[i] = 0; // pad with extra 0s to make a full frame. 689 | } 690 | mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining()); 691 | } else { 692 | mDecodedBytes.get(buffer); 693 | } 694 | if (mChannels == 2) { 695 | swapLeftRightChannels(buffer); 696 | } 697 | outputStream.write(buffer); 698 | numBytesLeft -= buffer.length; 699 | } 700 | if (numBytesLeft > 0) { 701 | if (mDecodedBytes.remaining() < numBytesLeft) { 702 | // This should not happen. 703 | for (int i = mDecodedBytes.remaining(); i < numBytesLeft; i++) { 704 | buffer[i] = 0; // pad with extra 0s to make a full frame. 705 | } 706 | mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining()); 707 | } else { 708 | mDecodedBytes.get(buffer, 0, numBytesLeft); 709 | } 710 | if (mChannels == 2) { 711 | swapLeftRightChannels(buffer); 712 | } 713 | outputStream.write(buffer, 0, numBytesLeft); 714 | } 715 | outputStream.close(); 716 | } 717 | 718 | // Debugging method dumping all the samples in mDecodedSamples in a TSV file. 719 | // Each row describes one sample and has the following format: 720 | // "\t\t...\t\n" 721 | // File will be written on the SDCard under media/audio/debug/ 722 | // If fileName is null or empty, then the default file name (samples.tsv) is used. 723 | private void DumpSamples(String fileName) { 724 | String externalRootDir = Environment.getExternalStorageDirectory().getPath(); 725 | if (!externalRootDir.endsWith("/")) { 726 | externalRootDir += "/"; 727 | } 728 | String parentDir = externalRootDir + "media/audio/debug/"; 729 | // Create the parent directory 730 | File parentDirFile = new File(parentDir); 731 | parentDirFile.mkdirs(); 732 | // If we can't write to that special path, try just writing directly to the SDCard. 733 | if (!parentDirFile.isDirectory()) { 734 | parentDir = externalRootDir; 735 | } 736 | if (fileName == null || fileName.isEmpty()) { 737 | fileName = "samples.tsv"; 738 | } 739 | File outFile = new File(parentDir + fileName); 740 | 741 | // Start dumping the samples. 742 | BufferedWriter writer = null; 743 | float presentationTime = 0; 744 | mDecodedSamples.rewind(); 745 | String row; 746 | try { 747 | writer = new BufferedWriter(new FileWriter(outFile)); 748 | for (int sampleIndex = 0; sampleIndex < mNumSamples; sampleIndex++) { 749 | presentationTime = (float) (sampleIndex) / mSampleRate; 750 | row = Float.toString(presentationTime); 751 | for (int channelIndex = 0; channelIndex < mChannels; channelIndex++) { 752 | row += "\t" + mDecodedSamples.get(); 753 | } 754 | row += "\n"; 755 | writer.write(row); 756 | } 757 | } catch (IOException e) { 758 | Log.w("Ringdroid", "Failed to create the sample TSV file."); 759 | Log.w("Ringdroid", getStackTrace(e)); 760 | } 761 | // We are done here. Close the file and rewind the buffer. 762 | try { 763 | writer.close(); 764 | } catch (Exception e) { 765 | Log.w("Ringdroid", "Failed to close sample TSV file."); 766 | Log.w("Ringdroid", getStackTrace(e)); 767 | } 768 | mDecodedSamples.rewind(); 769 | } 770 | 771 | // Helper method (samples will be dumped in media/audio/debug/samples.tsv). 772 | private void DumpSamples() { 773 | DumpSamples(null); 774 | } 775 | 776 | // Return the stack trace of a given exception. 777 | private String getStackTrace(Exception e) { 778 | StringWriter writer = new StringWriter(); 779 | e.printStackTrace(new PrintWriter(writer)); 780 | return writer.toString(); 781 | } 782 | } 783 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/WAVHeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | public class WAVHeader { 20 | private byte[] mHeader; // the complete header. 21 | private int mSampleRate; // sampling frequency in Hz (e.g. 44100). 22 | private int mChannels; // number of channels. 23 | private int mNumSamples; // total number of samples per channel. 24 | private int mNumBytesPerSample; // number of bytes per sample, all channels included. 25 | 26 | public WAVHeader(int sampleRate, int numChannels, int numSamples) { 27 | mSampleRate = sampleRate; 28 | mChannels = numChannels; 29 | mNumSamples = numSamples; 30 | mNumBytesPerSample = 2 * mChannels; // assuming 2 bytes per sample (for 1 channel) 31 | mHeader = null; 32 | setHeader(); 33 | } 34 | 35 | public byte[] getWAVHeader() { 36 | return mHeader; 37 | } 38 | 39 | public static byte[] getWAVHeader(int sampleRate, int numChannels, int numSamples) { 40 | return new WAVHeader(sampleRate, numChannels, numSamples).mHeader; 41 | } 42 | 43 | public String toString() { 44 | String str = ""; 45 | if (mHeader == null) { 46 | return str; 47 | } 48 | int num_32bits_per_lines = 8; 49 | int count = 0; 50 | for (byte b : mHeader) { 51 | boolean break_line = count > 0 && count % (num_32bits_per_lines * 4) == 0; 52 | boolean insert_space = count > 0 && count % 4 == 0 && !break_line; 53 | if (break_line) { 54 | str += '\n'; 55 | } 56 | if (insert_space) { 57 | str += ' '; 58 | } 59 | str += String.format("%02X", b); 60 | count++; 61 | } 62 | 63 | return str; 64 | } 65 | 66 | private void setHeader() { 67 | byte[] header = new byte[46]; 68 | int offset = 0; 69 | int size; 70 | 71 | // set the RIFF chunk 72 | System.arraycopy(new byte[]{'R', 'I', 'F', 'F'}, 0, header, offset, 4); 73 | offset += 4; 74 | size = 36 + mNumSamples * mNumBytesPerSample; 75 | header[offset++] = (byte) (size & 0xFF); 76 | header[offset++] = (byte) ((size >> 8) & 0xFF); 77 | header[offset++] = (byte) ((size >> 16) & 0xFF); 78 | header[offset++] = (byte) ((size >> 24) & 0xFF); 79 | System.arraycopy(new byte[]{'W', 'A', 'V', 'E'}, 0, header, offset, 4); 80 | offset += 4; 81 | 82 | // set the fmt chunk 83 | System.arraycopy(new byte[]{'f', 'm', 't', ' '}, 0, header, offset, 4); 84 | offset += 4; 85 | System.arraycopy(new byte[]{0x10, 0, 0, 0}, 0, header, offset, 4); // chunk size = 16 86 | offset += 4; 87 | System.arraycopy(new byte[]{1, 0}, 0, header, offset, 2); // format = 1 for PCM 88 | offset += 2; 89 | header[offset++] = (byte) (mChannels & 0xFF); 90 | header[offset++] = (byte) ((mChannels >> 8) & 0xFF); 91 | header[offset++] = (byte) (mSampleRate & 0xFF); 92 | header[offset++] = (byte) ((mSampleRate >> 8) & 0xFF); 93 | header[offset++] = (byte) ((mSampleRate >> 16) & 0xFF); 94 | header[offset++] = (byte) ((mSampleRate >> 24) & 0xFF); 95 | int byteRate = mSampleRate * mNumBytesPerSample; 96 | header[offset++] = (byte) (byteRate & 0xFF); 97 | header[offset++] = (byte) ((byteRate >> 8) & 0xFF); 98 | header[offset++] = (byte) ((byteRate >> 16) & 0xFF); 99 | header[offset++] = (byte) ((byteRate >> 24) & 0xFF); 100 | header[offset++] = (byte) (mNumBytesPerSample & 0xFF); 101 | header[offset++] = (byte) ((mNumBytesPerSample >> 8) & 0xFF); 102 | System.arraycopy(new byte[]{0x10, 0}, 0, header, offset, 2); 103 | offset += 2; 104 | 105 | // set the beginning of the data chunk 106 | System.arraycopy(new byte[]{'d', 'a', 't', 'a'}, 0, header, offset, 4); 107 | offset += 4; 108 | size = mNumSamples * mNumBytesPerSample; 109 | header[offset++] = (byte) (size & 0xFF); 110 | header[offset++] = (byte) ((size >> 8) & 0xFF); 111 | header[offset++] = (byte) ((size >> 16) & 0xFF); 112 | header[offset++] = (byte) ((size >> 24) & 0xFF); 113 | 114 | mHeader = header; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/customAudioViews/WaveformView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.demo.audiotrimmer.customAudioViews; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Canvas; 22 | import android.graphics.DashPathEffect; 23 | import android.graphics.Paint; 24 | import android.util.AttributeSet; 25 | import android.util.Log; 26 | import android.view.GestureDetector; 27 | import android.view.MotionEvent; 28 | import android.view.ScaleGestureDetector; 29 | import android.view.View; 30 | 31 | import com.demo.audiotrimmer.R; 32 | 33 | 34 | /** 35 | * WaveformView is an Android view that displays a visual representation 36 | * of an audio waveform. It retrieves the frame gains from a CheapSoundFile 37 | * object and recomputes the shape contour at several zoom levels. 38 | *

39 | * This class doesn't handle selection or any of the touch interactions 40 | * directly, so it exposes a listener interface. The class that embeds 41 | * this view should add itself as a listener and make the view scroll 42 | * and respond to other events appropriately. 43 | *

44 | * WaveformView doesn't actually handle selection, but it will just display 45 | * the selected part of the waveform in a different color. 46 | */ 47 | public class WaveformView extends View { 48 | 49 | private boolean isDrawBorder = true; 50 | 51 | public boolean isDrawBorder() { 52 | return isDrawBorder; 53 | } 54 | 55 | public void setIsDrawBorder(boolean isDrawBorder) { 56 | this.isDrawBorder = isDrawBorder; 57 | } 58 | 59 | public interface WaveformListener { 60 | public void waveformTouchStart(float x); 61 | 62 | public void waveformTouchMove(float x); 63 | 64 | public void waveformTouchEnd(); 65 | 66 | public void waveformFling(float x); 67 | 68 | public void waveformDraw(); 69 | 70 | public void waveformZoomIn(); 71 | 72 | public void waveformZoomOut(); 73 | } 74 | 75 | ; 76 | 77 | // Colors 78 | private Paint mGridPaint; 79 | private Paint mSelectedLinePaint; 80 | private Paint mUnselectedLinePaint; 81 | private Paint mUnselectedBkgndLinePaint; 82 | private Paint mBorderLinePaint; 83 | private Paint mPlaybackLinePaint; 84 | private Paint mTimecodePaint; 85 | 86 | private SoundFile mSoundFile; 87 | private int[] mLenByZoomLevel; 88 | private double[][] mValuesByZoomLevel; 89 | private double[] mZoomFactorByZoomLevel; 90 | private int[] mHeightsAtThisZoomLevel; 91 | private int mZoomLevel; 92 | private int mNumZoomLevels; 93 | private int mSampleRate; 94 | private int mSamplesPerFrame; 95 | private int mOffset; 96 | private int mSelectionStart; 97 | private int mSelectionEnd; 98 | private int mPlaybackPos; 99 | private float mDensity; 100 | private float mInitialScaleSpan; 101 | private WaveformListener mListener; 102 | private GestureDetector mGestureDetector; 103 | private ScaleGestureDetector mScaleGestureDetector; 104 | private boolean mInitialized; 105 | 106 | public WaveformView(Context context, AttributeSet attrs) { 107 | super(context, attrs); 108 | 109 | // We don't want keys, the markers get these 110 | setFocusable(false); 111 | 112 | Resources res = getResources(); 113 | mGridPaint = new Paint(); 114 | mGridPaint.setAntiAlias(false); 115 | mGridPaint.setColor(res.getColor(R.color.colorGridLine)); 116 | mSelectedLinePaint = new Paint(); 117 | mSelectedLinePaint.setAntiAlias(false); 118 | mSelectedLinePaint.setColor(res.getColor(R.color.waveformSelected)); 119 | mUnselectedLinePaint = new Paint(); 120 | mUnselectedLinePaint.setAntiAlias(false); 121 | mUnselectedLinePaint.setColor(res.getColor(R.color.waveformUnselected)); 122 | mUnselectedBkgndLinePaint = new Paint(); 123 | mUnselectedBkgndLinePaint.setAntiAlias(false); 124 | mUnselectedBkgndLinePaint.setColor(res.getColor(R.color.waveformUnselectedBackground)); 125 | mBorderLinePaint = new Paint(); 126 | mBorderLinePaint.setAntiAlias(true); 127 | mBorderLinePaint.setStrokeWidth(6f); 128 | mBorderLinePaint.setPathEffect(new DashPathEffect(new float[]{3.0f, 2.0f}, 0.0f)); 129 | mBorderLinePaint.setColor(res.getColor(R.color.colorSelectionBorder)); 130 | mPlaybackLinePaint = new Paint(); 131 | mPlaybackLinePaint.setAntiAlias(false); 132 | mPlaybackLinePaint.setStrokeWidth(3f); 133 | mPlaybackLinePaint.setColor(res.getColor(R.color.colorPlaybackIndicator)); 134 | mTimecodePaint = new Paint(); 135 | mTimecodePaint.setTextSize(12); 136 | mTimecodePaint.setAntiAlias(true); 137 | mTimecodePaint.setColor(res.getColor(R.color.colorTimeCode)); 138 | mTimecodePaint.setShadowLayer(2, 1, 1, res.getColor(R.color.colorTimeCodeShadow)); 139 | 140 | mGestureDetector = new GestureDetector( 141 | context, 142 | new GestureDetector.SimpleOnGestureListener() { 143 | public boolean onFling(MotionEvent e1, MotionEvent e2, float vx, float vy) { 144 | mListener.waveformFling(vx); 145 | return true; 146 | } 147 | } 148 | ); 149 | 150 | mScaleGestureDetector = new ScaleGestureDetector( 151 | context, 152 | new ScaleGestureDetector.SimpleOnScaleGestureListener() { 153 | public boolean onScaleBegin(ScaleGestureDetector d) { 154 | Log.v("Ringdroid", "ScaleBegin " + d.getCurrentSpanX()); 155 | mInitialScaleSpan = Math.abs(d.getCurrentSpanX()); 156 | return true; 157 | } 158 | 159 | public boolean onScale(ScaleGestureDetector d) { 160 | float scale = Math.abs(d.getCurrentSpanX()); 161 | Log.v("Ringdroid", "Scale " + (scale - mInitialScaleSpan)); 162 | if (scale - mInitialScaleSpan > 40) { 163 | mListener.waveformZoomIn(); 164 | mInitialScaleSpan = scale; 165 | } 166 | if (scale - mInitialScaleSpan < -40) { 167 | mListener.waveformZoomOut(); 168 | mInitialScaleSpan = scale; 169 | } 170 | return true; 171 | } 172 | 173 | public void onScaleEnd(ScaleGestureDetector d) { 174 | Log.v("Ringdroid", "ScaleEnd " + d.getCurrentSpanX()); 175 | } 176 | } 177 | ); 178 | 179 | mSoundFile = null; 180 | mLenByZoomLevel = null; 181 | mValuesByZoomLevel = null; 182 | mHeightsAtThisZoomLevel = null; 183 | mOffset = 0; 184 | mPlaybackPos = -1; 185 | mSelectionStart = 0; 186 | mSelectionEnd = 0; 187 | mDensity = 1.0f; 188 | mInitialized = false; 189 | } 190 | 191 | @Override 192 | public boolean onTouchEvent(MotionEvent event) { 193 | mScaleGestureDetector.onTouchEvent(event); 194 | if (mGestureDetector.onTouchEvent(event)) { 195 | return true; 196 | } 197 | 198 | switch (event.getAction()) { 199 | case MotionEvent.ACTION_DOWN: 200 | mListener.waveformTouchStart(event.getX()); 201 | break; 202 | case MotionEvent.ACTION_MOVE: 203 | mListener.waveformTouchMove(event.getX()); 204 | break; 205 | case MotionEvent.ACTION_UP: 206 | mListener.waveformTouchEnd(); 207 | break; 208 | } 209 | return true; 210 | } 211 | 212 | public boolean hasSoundFile() { 213 | return mSoundFile != null; 214 | } 215 | 216 | public void setSoundFile(SoundFile soundFile) { 217 | mSoundFile = soundFile; 218 | mSampleRate = mSoundFile.getSampleRate(); 219 | mSamplesPerFrame = mSoundFile.getSamplesPerFrame(); 220 | computeDoublesForAllZoomLevels(); 221 | mHeightsAtThisZoomLevel = null; 222 | } 223 | 224 | public boolean isInitialized() { 225 | return mInitialized; 226 | } 227 | 228 | public int getZoomLevel() { 229 | return mZoomLevel; 230 | } 231 | 232 | public void setZoomLevel(int zoomLevel) { 233 | while (mZoomLevel > zoomLevel) { 234 | zoomIn(); 235 | } 236 | while (mZoomLevel < zoomLevel) { 237 | zoomOut(); 238 | } 239 | } 240 | 241 | public boolean canZoomIn() { 242 | return (mZoomLevel > 0); 243 | } 244 | 245 | public void zoomIn() { 246 | if (canZoomIn()) { 247 | mZoomLevel--; 248 | mSelectionStart *= 2; 249 | mSelectionEnd *= 2; 250 | mHeightsAtThisZoomLevel = null; 251 | int offsetCenter = mOffset + getMeasuredWidth() / 2; 252 | offsetCenter *= 2; 253 | mOffset = offsetCenter - getMeasuredWidth() / 2; 254 | if (mOffset < 0) 255 | mOffset = 0; 256 | invalidate(); 257 | } 258 | } 259 | 260 | public boolean canZoomOut() { 261 | return (mZoomLevel < mNumZoomLevels - 1); 262 | } 263 | 264 | public void zoomOut() { 265 | if (canZoomOut()) { 266 | mZoomLevel++; 267 | mSelectionStart /= 2; 268 | mSelectionEnd /= 2; 269 | int offsetCenter = mOffset + getMeasuredWidth() / 2; 270 | offsetCenter /= 2; 271 | mOffset = offsetCenter - getMeasuredWidth() / 2; 272 | if (mOffset < 0) 273 | mOffset = 0; 274 | mHeightsAtThisZoomLevel = null; 275 | invalidate(); 276 | } 277 | } 278 | 279 | public int maxPos() { 280 | return mLenByZoomLevel[mZoomLevel]; 281 | } 282 | 283 | public int secondsToFrames(double seconds) { 284 | return (int) (1.0 * seconds * mSampleRate / mSamplesPerFrame + 0.5); 285 | } 286 | 287 | public int secondsToPixels(double seconds) { 288 | double z = mZoomFactorByZoomLevel[mZoomLevel]; 289 | return (int) (z * seconds * mSampleRate / mSamplesPerFrame + 0.5); 290 | } 291 | 292 | public double pixelsToSeconds(int pixels) { 293 | double z = mZoomFactorByZoomLevel[mZoomLevel]; 294 | return (pixels * (double) mSamplesPerFrame / (mSampleRate * z)); 295 | } 296 | 297 | public int millisecsToPixels(int msecs) { 298 | double z = mZoomFactorByZoomLevel[mZoomLevel]; 299 | return (int) ((msecs * 1.0 * mSampleRate * z) / 300 | (1000.0 * mSamplesPerFrame) + 0.5); 301 | } 302 | 303 | public int pixelsToMillisecs(int pixels) { 304 | double z = mZoomFactorByZoomLevel[mZoomLevel]; 305 | return (int) (pixels * (1000.0 * mSamplesPerFrame) / 306 | (mSampleRate * z) + 0.5); 307 | } 308 | 309 | public void setParameters(int start, int end, int offset) { 310 | mSelectionStart = start; 311 | mSelectionEnd = end; 312 | mOffset = offset; 313 | } 314 | 315 | public int getStart() { 316 | return mSelectionStart; 317 | } 318 | 319 | public int getEnd() { 320 | return mSelectionEnd; 321 | } 322 | 323 | public int getOffset() { 324 | return mOffset; 325 | } 326 | 327 | public void setPlayback(int pos) { 328 | mPlaybackPos = pos; 329 | } 330 | 331 | public void setListener(WaveformListener listener) { 332 | mListener = listener; 333 | } 334 | 335 | public void recomputeHeights(float density) { 336 | mHeightsAtThisZoomLevel = null; 337 | mDensity = density; 338 | mTimecodePaint.setTextSize((int) (12 * density)); 339 | 340 | invalidate(); 341 | } 342 | 343 | protected void drawWaveformLine(Canvas canvas, 344 | int x, int y0, int y1, 345 | Paint paint) { 346 | canvas.drawLine(x, y0, x, y1, paint); 347 | } 348 | 349 | @Override 350 | protected void onDraw(Canvas canvas) { 351 | super.onDraw(canvas); 352 | if (mSoundFile == null) 353 | return; 354 | 355 | if (mHeightsAtThisZoomLevel == null) 356 | computeIntsForThisZoomLevel(); 357 | 358 | // Draw waveform 359 | int measuredWidth = getMeasuredWidth(); 360 | int measuredHeight = getMeasuredHeight(); 361 | int start = mOffset; 362 | int width = mHeightsAtThisZoomLevel.length - start; 363 | int ctr = measuredHeight / 2; 364 | 365 | if (width > measuredWidth) 366 | width = measuredWidth; 367 | 368 | // Draw grid 369 | double onePixelInSecs = pixelsToSeconds(1); 370 | boolean onlyEveryFiveSecs = (onePixelInSecs > 1.0 / 50.0); 371 | double fractionalSecs = mOffset * onePixelInSecs; 372 | int integerSecs = (int) fractionalSecs; 373 | int i = 0; 374 | while (i < width) { 375 | i++; 376 | fractionalSecs += onePixelInSecs; 377 | int integerSecsNew = (int) fractionalSecs; 378 | if (integerSecsNew != integerSecs) { 379 | integerSecs = integerSecsNew; 380 | if (!onlyEveryFiveSecs || 0 == (integerSecs % 5)) { 381 | canvas.drawLine(i, 0, i, measuredHeight, mGridPaint); 382 | } 383 | } 384 | } 385 | 386 | // Draw waveform 387 | for (i = 0; i < width; i++) { 388 | Paint paint; 389 | if (i + start >= mSelectionStart && 390 | i + start < mSelectionEnd) { 391 | paint = mSelectedLinePaint; 392 | } else { 393 | drawWaveformLine(canvas, i, 0, measuredHeight, 394 | mUnselectedBkgndLinePaint); 395 | paint = mUnselectedLinePaint; 396 | } 397 | drawWaveformLine( 398 | canvas, i, 399 | ctr - mHeightsAtThisZoomLevel[start + i], 400 | ctr + 1 + mHeightsAtThisZoomLevel[start + i], 401 | paint); 402 | 403 | if (i + start == mPlaybackPos) { 404 | canvas.drawLine(i, 0, i, measuredHeight, mPlaybackLinePaint); 405 | } 406 | } 407 | 408 | // If we can see the right edge of the waveform, draw the 409 | // non-waveform area to the right as unselected 410 | for (i = width; i < measuredWidth; i++) { 411 | drawWaveformLine(canvas, i, 0, measuredHeight, 412 | mUnselectedBkgndLinePaint); 413 | } 414 | 415 | // Draw borders 416 | 417 | if (isDrawBorder()) { 418 | canvas.drawLine( 419 | mSelectionStart - mOffset + 0.5f, 0, 420 | mSelectionStart - mOffset + 0.5f, measuredHeight, 421 | mBorderLinePaint); 422 | canvas.drawLine( 423 | mSelectionEnd - mOffset + 0.5f, 0, 424 | mSelectionEnd - mOffset + 0.5f, measuredHeight, 425 | mBorderLinePaint); 426 | } 427 | 428 | /*// Draw timecode 429 | double timecodeIntervalSecs = 1.0; 430 | if (timecodeIntervalSecs / onePixelInSecs < 50) { 431 | timecodeIntervalSecs = 5.0; 432 | } 433 | if (timecodeIntervalSecs / onePixelInSecs < 50) { 434 | timecodeIntervalSecs = 15.0; 435 | } 436 | 437 | // Draw grid 438 | fractionalSecs = mOffset * onePixelInSecs; 439 | int integerTimecode = (int) (fractionalSecs / timecodeIntervalSecs); 440 | i = 0; 441 | while (i < width) { 442 | i++; 443 | fractionalSecs += onePixelInSecs; 444 | integerSecs = (int) fractionalSecs; 445 | int integerTimecodeNew = (int) (fractionalSecs / 446 | timecodeIntervalSecs); 447 | if (integerTimecodeNew != integerTimecode) { 448 | integerTimecode = integerTimecodeNew; 449 | 450 | // Turn, e.g. 67 seconds into "1:07" 451 | String timecodeMinutes = "" + (integerSecs / 60); 452 | String timecodeSeconds = "" + (integerSecs % 60); 453 | if ((integerSecs % 60) < 10) { 454 | timecodeSeconds = "0" + timecodeSeconds; 455 | } 456 | String timecodeStr = timecodeMinutes + ":" + timecodeSeconds; 457 | float offset = (float) ( 458 | 0.5 * mTimecodePaint.measureText(timecodeStr)); 459 | canvas.drawText(timecodeStr, 460 | i - offset, 461 | (int)(12 * mDensity), 462 | mTimecodePaint); 463 | } 464 | }*/ 465 | 466 | if (mListener != null) { 467 | mListener.waveformDraw(); 468 | } 469 | } 470 | 471 | /** 472 | * Called once when a new sound file is added 473 | */ 474 | private void computeDoublesForAllZoomLevels() { 475 | int numFrames = mSoundFile.getNumFrames(); 476 | int[] frameGains = mSoundFile.getFrameGains(); 477 | double[] smoothedGains = new double[numFrames]; 478 | if (numFrames == 1) { 479 | smoothedGains[0] = frameGains[0]; 480 | } else if (numFrames == 2) { 481 | smoothedGains[0] = frameGains[0]; 482 | smoothedGains[1] = frameGains[1]; 483 | } else if (numFrames > 2) { 484 | smoothedGains[0] = (double) ( 485 | (frameGains[0] / 2.0) + 486 | (frameGains[1] / 2.0)); 487 | for (int i = 1; i < numFrames - 1; i++) { 488 | smoothedGains[i] = (double) ( 489 | (frameGains[i - 1] / 3.0) + 490 | (frameGains[i] / 3.0) + 491 | (frameGains[i + 1] / 3.0)); 492 | } 493 | smoothedGains[numFrames - 1] = (double) ( 494 | (frameGains[numFrames - 2] / 2.0) + 495 | (frameGains[numFrames - 1] / 2.0)); 496 | } 497 | 498 | // Make sure the range is no more than 0 - 255 499 | double maxGain = 1.0; 500 | for (int i = 0; i < numFrames; i++) { 501 | if (smoothedGains[i] > maxGain) { 502 | maxGain = smoothedGains[i]; 503 | } 504 | } 505 | double scaleFactor = 1.0; 506 | if (maxGain > 255.0) { 507 | scaleFactor = 255 / maxGain; 508 | } 509 | 510 | // Build histogram of 256 bins and figure out the new scaled max 511 | maxGain = 0; 512 | int gainHist[] = new int[256]; 513 | for (int i = 0; i < numFrames; i++) { 514 | int smoothedGain = (int) (smoothedGains[i] * scaleFactor); 515 | if (smoothedGain < 0) 516 | smoothedGain = 0; 517 | if (smoothedGain > 255) 518 | smoothedGain = 255; 519 | 520 | if (smoothedGain > maxGain) 521 | maxGain = smoothedGain; 522 | 523 | gainHist[smoothedGain]++; 524 | } 525 | 526 | // Re-calibrate the min to be 5% 527 | double minGain = 0; 528 | int sum = 0; 529 | while (minGain < 255 && sum < numFrames / 20) { 530 | sum += gainHist[(int) minGain]; 531 | minGain++; 532 | } 533 | 534 | // Re-calibrate the max to be 99% 535 | sum = 0; 536 | while (maxGain > 2 && sum < numFrames / 100) { 537 | sum += gainHist[(int) maxGain]; 538 | maxGain--; 539 | } 540 | 541 | // Compute the heights 542 | double[] heights = new double[numFrames]; 543 | double range = maxGain - minGain; 544 | for (int i = 0; i < numFrames; i++) { 545 | double value = (smoothedGains[i] * scaleFactor - minGain) / range; 546 | if (value < 0.0) 547 | value = 0.0; 548 | if (value > 1.0) 549 | value = 1.0; 550 | heights[i] = value * value; 551 | } 552 | 553 | mNumZoomLevels = 5; 554 | mLenByZoomLevel = new int[5]; 555 | mZoomFactorByZoomLevel = new double[5]; 556 | mValuesByZoomLevel = new double[5][]; 557 | 558 | // Level 0 is doubled, with interpolated values 559 | mLenByZoomLevel[0] = numFrames * 2; 560 | mZoomFactorByZoomLevel[0] = 2.0; 561 | mValuesByZoomLevel[0] = new double[mLenByZoomLevel[0]]; 562 | if (numFrames > 0) { 563 | mValuesByZoomLevel[0][0] = 0.5 * heights[0]; 564 | mValuesByZoomLevel[0][1] = heights[0]; 565 | } 566 | for (int i = 1; i < numFrames; i++) { 567 | mValuesByZoomLevel[0][2 * i] = 0.5 * (heights[i - 1] + heights[i]); 568 | mValuesByZoomLevel[0][2 * i + 1] = heights[i]; 569 | } 570 | 571 | // Level 1 is normal 572 | mLenByZoomLevel[1] = numFrames; 573 | mValuesByZoomLevel[1] = new double[mLenByZoomLevel[1]]; 574 | mZoomFactorByZoomLevel[1] = 1.0; 575 | for (int i = 0; i < mLenByZoomLevel[1]; i++) { 576 | mValuesByZoomLevel[1][i] = heights[i]; 577 | } 578 | 579 | // 3 more levels are each halved 580 | for (int j = 2; j < 5; j++) { 581 | mLenByZoomLevel[j] = mLenByZoomLevel[j - 1] / 2; 582 | mValuesByZoomLevel[j] = new double[mLenByZoomLevel[j]]; 583 | mZoomFactorByZoomLevel[j] = mZoomFactorByZoomLevel[j - 1] / 2.0; 584 | for (int i = 0; i < mLenByZoomLevel[j]; i++) { 585 | mValuesByZoomLevel[j][i] = 586 | 0.5 * (mValuesByZoomLevel[j - 1][2 * i] + 587 | mValuesByZoomLevel[j - 1][2 * i + 1]); 588 | } 589 | } 590 | 591 | if (numFrames > 5000) { 592 | mZoomLevel = 3; 593 | } else if (numFrames > 1000) { 594 | mZoomLevel = 2; 595 | } else if (numFrames > 300) { 596 | mZoomLevel = 1; 597 | } else { 598 | mZoomLevel = 0; 599 | } 600 | 601 | mInitialized = true; 602 | } 603 | 604 | /** 605 | * Called the first time we need to draw when the zoom level has changed 606 | * or the screen is resized 607 | */ 608 | private void computeIntsForThisZoomLevel() { 609 | int halfHeight = (getMeasuredHeight() / 2) - 1; 610 | mHeightsAtThisZoomLevel = new int[mLenByZoomLevel[mZoomLevel]]; 611 | for (int i = 0; i < mLenByZoomLevel[mZoomLevel]; i++) { 612 | mHeightsAtThisZoomLevel[i] = 613 | (int) (mValuesByZoomLevel[mZoomLevel][i] * halfHeight); 614 | } 615 | } 616 | } 617 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/java/com/demo/audiotrimmer/utils/Utility.java: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2018 Intuz Pvt Ltd. 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 6 | // (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 11 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 12 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | 15 | package com.demo.audiotrimmer.utils; 16 | 17 | public class Utility { 18 | 19 | //audio format in which file after trim will be saved. 20 | public static final String AUDIO_FORMAT = ".wav"; 21 | 22 | //audio mime type in which file after trim will be saved. 23 | public static final String AUDIO_MIME_TYPE = "audio/wav"; 24 | 25 | public static long getCurrentTime() { 26 | return System.nanoTime() / 1000000; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_audiohandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_audiohandle.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_audiostartrecord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_audiostartrecord.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_crop_btn_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_crop_btn_fill.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_edit_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_edit_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_pause_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_pause_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_play_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_play_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_record_btn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_record_btn1.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_refresh_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_refresh_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-hdpi/ic_stop_btn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-hdpi/ic_stop_btn1.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_audiohandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_audiohandle.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_audiostartrecord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_audiostartrecord.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_crop_btn_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_crop_btn_fill.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_edit_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_edit_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_pause_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_pause_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_play_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_play_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_record_btn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_record_btn1.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_refresh_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_refresh_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_stop_btn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xhdpi/ic_stop_btn1.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_audiohandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_audiohandle.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_audiostartrecord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_audiostartrecord.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_crop_btn_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_crop_btn_fill.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_edit_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_edit_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_pause_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_pause_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_play_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_play_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_record_btn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_record_btn1.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_refresh_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_refresh_btn.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_stop_btn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/drawable-xxhdpi/ic_stop_btn1.png -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable/marker_left.xml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/drawable/marker_right.xml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/layout/activity_audio_trim.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 25 | 26 | 36 | 37 | 47 | 48 | 49 | 50 | 54 | 55 | 63 | 64 | 69 | 70 | 79 | 80 | 81 | 86 | 87 | 96 | 97 | 104 | 105 | 106 | 110 | 111 | 119 | 120 | 127 | 128 | 135 | 136 | 144 | 145 | 152 | 153 | 154 | 155 | 156 | 164 | 165 | 166 | 172 | 173 | 176 | 177 | 185 | 186 | 195 | 196 | 197 | 206 | 207 | 212 | 213 | 220 | 221 | 227 | 228 | 235 | 236 | 241 | 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /Audio Trimmer/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 |