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 |
18 |
19 |
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | #80000000
8 | #0e1f2f
9 | #ADADAD
10 | #3be3e3
11 | #000
12 | #00ed1b24
13 | #9c41bc
14 | #253571
15 | #b2253571
16 | #dd253571
17 | #aa253571
18 | #adb5cc
19 | #adb5cc
20 | #e8e6e7
21 |
22 |
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AudioTrimmer
3 | Cancel
4 | Upload
5 |
6 |
--------------------------------------------------------------------------------
/Audio Trimmer/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Audio Trimmer/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/Audio Trimmer/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/Audio Trimmer/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Audio Trimmer/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Audio Trimmer/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/Audio Trimmer/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/Audio Trimmer/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/Audio Trimmer/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Intuz-production
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Introduction
2 |
3 | INTUZ is presenting a Audio Trimmer component, which lets you trim your audio files on the fly in Android. You can record your audio and get start to trim!
4 | Please follow below steps to integrate this control in your next project.
5 |
6 |
7 |
Features
8 |
9 | - Ability to record your audio and trim it.
10 | - Ability to play selected range of audio before trimming.
11 | - After trimming if you don’t like the audio then you can get the original file again by pressing “Reset”.
12 |
13 |
14 |
15 |
16 |
17 |
88 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
89 |
90 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/Screenshots/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Screenshots/.DS_Store
--------------------------------------------------------------------------------
/Screenshots/audio_trimmer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Screenshots/audio_trimmer.png
--------------------------------------------------------------------------------
/Screenshots/audiotrimmer_.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Screenshots/audiotrimmer_.gif
--------------------------------------------------------------------------------
/Screenshots/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Intuz-production/Audio-Trimmer-Android/a88a28294aa44f062aae4a66e17a8b2a929297a8/Screenshots/logo.jpg
--------------------------------------------------------------------------------