├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── styles.xml
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── bg_clips.jpg
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_end_clips.png
│ │ │ │ ├── ic_start_clips.png
│ │ │ │ └── img_graduation.png
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── luowei
│ │ │ └── audioclips
│ │ │ └── MainActivity.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── luowei
│ │ │ └── audioclips
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── luowei
│ │ └── audioclips
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── audioclip
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── attrs.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── luowei
│ │ │ └── audioclip
│ │ │ ├── ClipsMarkerTextView.java
│ │ │ ├── MusicEditor.java
│ │ │ ├── soundfile
│ │ │ ├── WAVHeader.java
│ │ │ ├── MP4Header.java
│ │ │ └── SoundFile.java
│ │ │ └── ClipsFrameLayout.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── luowei
│ │ │ └── audioclip
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── luowei
│ │ └── audioclip
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── picture
└── demo.jpg
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/audioclip/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':audioclip'
2 |
--------------------------------------------------------------------------------
/picture/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/picture/demo.jpg
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AudioClips
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/audioclip/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AudioClip
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/bg_clips.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/drawable-xhdpi/bg_clips.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_end_clips.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/drawable-xxhdpi/ic_end_clips.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_start_clips.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/drawable-xxhdpi/ic_start_clips.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/img_graduation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luoweii/AudioClips/HEAD/app/src/main/res/drawable-xxhdpi/img_graduation.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Apr 26 16:08:02 CST 2016
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.10-all.zip
7 |
--------------------------------------------------------------------------------
/audioclip/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/test/java/com/luowei/audioclips/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclips;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/audioclip/src/test/java/com/luowei/audioclip/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclip;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/luowei/audioclips/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclips;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Android generated
2 | bin/
3 | gen/
4 |
5 | # Ant
6 | build.xml
7 | local.properties
8 |
9 | # Maven
10 | target/
11 | pom.xml.*
12 | release.properties
13 |
14 | # Eclipse
15 | .classpath
16 | .project
17 | .externalToolBuilders/
18 |
19 | # IntelliJ
20 | *.iml
21 | *.ipr
22 | *.iws
23 | .idea/
24 | out/
25 |
26 | # Mac
27 | .DS_Store
28 |
29 | # Ignore gradle files
30 | .gradle/
31 | build/
32 |
33 | # Private
34 | signing.properties
35 |
36 | captures/
--------------------------------------------------------------------------------
/audioclip/src/androidTest/java/com/luowei/audioclip/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclip;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/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 E:\software\android_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 |
--------------------------------------------------------------------------------
/audioclip/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 E:\software\android_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 |
--------------------------------------------------------------------------------
/audioclip/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "23.0.1"
6 |
7 | defaultConfig {
8 | applicationId "com.luowei.audioclips"
9 | minSdkVersion 16
10 | targetSdkVersion 23
11 | versionCode 1
12 | versionName "1.0"
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 | testCompile 'junit:junit:4.12'
25 | compile 'com.android.support:appcompat-v7:23.1.1'
26 | compile project(':audioclip')
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/audioclip/src/main/java/com/luowei/audioclip/ClipsMarkerTextView.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclip;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.graphics.Color;
6 | import android.graphics.Paint;
7 | import android.util.AttributeSet;
8 | import android.widget.TextView;
9 |
10 | import java.text.SimpleDateFormat;
11 | import java.util.Date;
12 |
13 | /**
14 | * 自定义剪辑的marker
15 | * Created by 骆巍 on 2015/10/27.
16 | */
17 | public class ClipsMarkerTextView extends TextView {
18 | private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
19 | private int second;
20 | private float pointWidth;
21 |
22 | public ClipsMarkerTextView(Context context) {
23 | this(context, null);
24 | }
25 |
26 | public ClipsMarkerTextView(Context context, AttributeSet attrs) {
27 | super(context, attrs);
28 | paint.setColor(Color.WHITE);
29 | }
30 |
31 | @Override
32 | protected void onDraw(Canvas canvas) {
33 | canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getCompoundPaddingTop(), paint);
34 | super.onDraw(canvas);
35 | }
36 |
37 | public void setSecond(int second) {
38 | this.second = second;
39 | SimpleDateFormat sdf = new SimpleDateFormat("m:ss");
40 | String str = sdf.format(new Date(second * 1000));
41 | setText(str);
42 | }
43 |
44 | public int getSecond() {
45 | return second;
46 | }
47 |
48 | public void setPointWidth(float pointWidth) {
49 | this.pointWidth = pointWidth;
50 | paint.setStrokeWidth(pointWidth);
51 | invalidate();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/audioclip/src/main/java/com/luowei/audioclip/MusicEditor.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclip;
2 |
3 | import android.media.MediaExtractor;
4 | import android.media.MediaFormat;
5 |
6 | import java.io.File;
7 | import java.io.FileInputStream;
8 | import java.io.FileNotFoundException;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.nio.ByteBuffer;
13 |
14 | /**
15 | * Created by 骆巍 on 2015/11/26.
16 | */
17 | public class MusicEditor {
18 | public static int REQUEST_FINISH = 1000;
19 |
20 | public static void editorMusic(InputStream is, File targetFile, int start, int end, int allTime) throws IOException {
21 | byte[] byteMusic = getMusicByte(is);
22 | editorMusic(byteMusic, targetFile, start, end, allTime);
23 | }
24 |
25 | public static void editorMusic(File file, File targetFile, int start, int end, int allTime) throws IOException {
26 | byte[] byteMusic = getMusicByte(file);
27 | editorMusic(byteMusic, targetFile, start, end, allTime);
28 | }
29 |
30 | public static void editorMusic(byte[] bytes, File targetFile, int start, int end, int allTime) throws IOException {
31 | if (bytes == null) return;
32 | int editorStart = (start * REQUEST_FINISH) / allTime;
33 | int editorStop = (end * REQUEST_FINISH) / allTime;
34 |
35 | int num = bytes.length;
36 | int editorStart2 = editorStart * (num / REQUEST_FINISH);
37 | int count = (editorStop * (num / REQUEST_FINISH)) - editorStart2;
38 | FileOutputStream fout = new FileOutputStream(targetFile);
39 | fout.write(bytes, editorStart2, count);
40 | fout.close();
41 | }
42 |
43 | public static byte[] getMusicByte(File file) throws IOException {
44 | if (!file.exists()) {
45 | return null;
46 | }
47 | return getMusicByte(new FileInputStream(file));
48 | }
49 |
50 | public static byte[] getMusicByte(InputStream is) throws IOException {
51 | if (is == null) return null;
52 | int num = is.available();
53 | byte[] buffer = new byte[num];
54 | is.read(buffer);
55 | is.close();
56 | return buffer;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AudioClips
2 | 音频剪辑工具,能自定义许多元素
3 |
4 |
5 |
6 |
7 | 使用
8 | -----------------------------
9 | dependencies {
10 | compile 'com.luowei.warmheart:audioclip:+'
11 | }
12 |
13 | 配置
14 | -------------
15 | ```xml
16 |
26 |
27 |
32 |
33 |
40 |
41 |
50 |
51 |
62 |
63 |
74 |
75 | ```
76 | ####标签属性说明
77 | * clip_progressHeight: 播放进度条高度
78 | * clip_startId: 剪辑开始标记的资源Id
79 | * clip_endId: 剪辑结束标记的资源Id
80 | * clip_background: 剪辑的背景图标
81 | * clip_clipsOverColor: 剪辑的覆盖颜色
82 | * clip_clipsMinSecond: 剪辑的最小时间
83 | * clip_point_width: 播放进度条的指定的宽度
84 | * clip_point_color: 播放进度条的颜色
85 |
--------------------------------------------------------------------------------
/audioclip/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'com.github.dcendents.android-maven'
3 | apply plugin: 'com.jfrog.bintray'
4 |
5 | def siteUrl = 'https://github.com/luoweii/AudioClips' // project homepage
6 | def gitUrl = 'https://github.com/luoweii/AudioClips.git' // project git
7 | version = "1.1.0"
8 | group = "com.luowei.warmheart"
9 |
10 | android {
11 | compileSdkVersion 23
12 | buildToolsVersion "23.0.1"
13 |
14 | defaultConfig {
15 | minSdkVersion 16
16 | targetSdkVersion 23
17 | versionCode 1
18 | versionName "1.0.0"
19 | }
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | }
27 |
28 | install {
29 | repositories.mavenInstaller {
30 | // This generates POM.xml with proper parameters
31 | pom {
32 | project {
33 | packaging 'aar'
34 | name 'audioclips'
35 | url siteUrl
36 | licenses {
37 | license {
38 | name 'The Apache Software License, Version 2.0'
39 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
40 | }
41 | }
42 | developers {
43 | developer {
44 | id 'luowei'
45 | name 'luowei'
46 | email 'luowei58@qq.com'
47 | }
48 | }
49 | scm {
50 | connection gitUrl
51 | developerConnection gitUrl
52 | url siteUrl
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | task sourcesJar(type: Jar) {
60 | from android.sourceSets.main.java.srcDirs
61 | classifier = 'sources'
62 | }
63 |
64 | task javadoc(type: Javadoc) {
65 | options.encoding = "UTF-8"
66 | source = android.sourceSets.main.java.srcDirs
67 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
68 | }
69 |
70 | task javadocJar(type: Jar, dependsOn: javadoc) {
71 | classifier = 'javadoc'
72 | from javadoc.destinationDir
73 | }
74 |
75 | artifacts {
76 | archives javadocJar
77 | archives sourcesJar
78 | }
79 |
80 | Properties properties = new Properties()
81 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
82 | bintray {
83 | user = properties.getProperty("bintray.user")
84 | key = properties.getProperty("bintray.apikey")
85 | configurations = ['archives']
86 | pkg {
87 | repo = "maven"
88 | name = "audioclips" // project name in jcenter
89 | websiteUrl = siteUrl
90 | vcsUrl = gitUrl
91 | licenses = ["Apache-2.0"]
92 | publish = true
93 | }
94 | }
95 |
96 | dependencies {
97 | compile fileTree(dir: 'libs', include: ['*.jar'])
98 | testCompile 'junit:junit:4.12'
99 | compile 'com.android.support:appcompat-v7:23.1.1'
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
26 |
27 |
34 |
35 |
44 |
45 |
56 |
57 |
68 |
69 |
70 |
76 |
77 |
83 |
84 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/audioclip/src/main/java/com/luowei/audioclip/soundfile/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.luowei.audioclip.soundfile;
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/luowei/audioclips/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclips;
2 |
3 | import android.app.ProgressDialog;
4 | import android.content.res.AssetFileDescriptor;
5 | import android.media.MediaPlayer;
6 | import android.os.AsyncTask;
7 | import android.os.Environment;
8 | import android.support.v7.app.AppCompatActivity;
9 | import android.os.Bundle;
10 | import android.util.Log;
11 | import android.view.MotionEvent;
12 | import android.view.View;
13 | import android.widget.Button;
14 | import android.widget.TextView;
15 | import android.widget.Toast;
16 |
17 | import com.luowei.audioclip.ClipsFrameLayout;
18 | import com.luowei.audioclip.MusicEditor;
19 | import com.luowei.audioclip.soundfile.SoundFile;
20 |
21 | import java.io.File;
22 | import java.text.SimpleDateFormat;
23 | import java.util.Date;
24 |
25 | public class MainActivity extends AppCompatActivity {
26 | ClipsFrameLayout clipsFrameLayout;
27 | TextView tvDuration;
28 | Button btnPlay;
29 | MediaPlayer mp;
30 | //填写自己的音乐文件路径,这里是我的音乐文件路径
31 | File file = new File(Environment.getExternalStorageDirectory() + "/Music/Download", "nsn.mp3");
32 | SoundFile soundFile;
33 |
34 |
35 | @Override
36 | protected void onCreate(Bundle savedInstanceState) {
37 | super.onCreate(savedInstanceState);
38 | setContentView(R.layout.activity_main);
39 | clipsFrameLayout = (ClipsFrameLayout) findViewById(R.id.clipsFrameLayout);
40 | tvDuration = (TextView) findViewById(R.id.tvDuration);
41 | btnPlay = (Button) findViewById(R.id.btnPlay);
42 | mp = new MediaPlayer();
43 | if (!file.exists()) {
44 | Toast.makeText(MainActivity.this, "文件不存在 " + file, Toast.LENGTH_LONG).show();
45 | return;
46 | }
47 | final ProgressDialog pd = new ProgressDialog(MainActivity.this);
48 | pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
49 | pd.setTitle("Loading...");
50 | pd.setCancelable(false);
51 | new AsyncTask() {
52 | @Override
53 | protected void onPreExecute() {
54 | pd.show();
55 | }
56 |
57 | @Override
58 | protected Void doInBackground(Void... params) {
59 | try {
60 | SoundFile.ProgressListener listener = new SoundFile.ProgressListener() {
61 | public boolean reportProgress(double fractionComplete) {
62 | pd.setProgress((int) (pd.getMax() * fractionComplete));
63 | return true;
64 | }
65 | };
66 | soundFile = SoundFile.create(file.getAbsolutePath(), listener);
67 | } catch (Exception e) {
68 | e.printStackTrace();
69 | }
70 | return null;
71 | }
72 |
73 | @Override
74 | protected void onPostExecute(Void aVoid) {
75 | try {
76 | pd.dismiss();
77 | mp.setDataSource(file.getAbsolutePath());
78 | mp.prepare();
79 | int second = (mp.getDuration() / 1000);
80 | clipsFrameLayout.setMaxProgress(second);
81 | clipsFrameLayout.setProgress(clipsFrameLayout.getStartClips());
82 | tvDuration.setText(getFormatTime(second));
83 | mp.seekTo(clipsFrameLayout.getStartClips() * 1000);
84 |
85 | //监听剪辑开始位置的触摸
86 | clipsFrameLayout.setStartClipsTouchListener(new View.OnTouchListener() {
87 | @Override
88 | public boolean onTouch(View v, MotionEvent event) {
89 | switch (event.getAction()) {
90 | case MotionEvent.ACTION_UP:
91 | if (mp.isPlaying()) mp.pause();
92 | btnPlay.setText("播放");
93 | clipsFrameLayout.setProgress(clipsFrameLayout.getStartClips());
94 | mp.seekTo(clipsFrameLayout.getProgress() * 1000);
95 | break;
96 | }
97 | return false;
98 | }
99 | });
100 | clipsFrameLayout.setSoundFile(soundFile);
101 | } catch (Exception e) {
102 | e.printStackTrace();
103 | }
104 | }
105 | }.execute();
106 | }
107 |
108 | public String getFormatTime(int second) {
109 | SimpleDateFormat sdf = new SimpleDateFormat("m:ss");
110 | return sdf.format(new Date(second * 1000));
111 | }
112 |
113 | public void playOnClick(View view) {
114 | if ("播放".equals(btnPlay.getText())) {
115 | if (clipsFrameLayout.getProgress() < clipsFrameLayout.getStartClips() || clipsFrameLayout.getProgress() >= clipsFrameLayout.getEndClips()) {
116 | mp.seekTo(clipsFrameLayout.getStartClips() * 1000);
117 | }
118 | mp.start();
119 | btnPlay.setText("暂停");
120 | btnPlay.postDelayed(new Runnable() {
121 | @Override
122 | public void run() {
123 | if ("暂停".equals(btnPlay.getText())) {
124 | int p = mp.getCurrentPosition() / 1000;
125 | if (p < clipsFrameLayout.getStartClips() || p > clipsFrameLayout.getEndClips() ||
126 | !hasWindowFocus()) {
127 | mp.pause();
128 | btnPlay.setText("播放");
129 | return;
130 | }
131 | clipsFrameLayout.setProgress(p);
132 | btnPlay.postDelayed(this, 250);
133 | }
134 | }
135 | }, 250);
136 | } else {
137 | mp.pause();
138 | btnPlay.setText("播放");
139 | }
140 | }
141 |
142 | public void clipsOnClick(View view) {
143 | try {
144 | String filename = file.getName();
145 | File targetFile = new File(Environment.getExternalStorageDirectory() + "/audioclips", filename.substring(0, filename.lastIndexOf(".")) + System.currentTimeMillis() + filename.substring(filename.lastIndexOf(".")));
146 | if (!targetFile.getParentFile().exists()) targetFile.getParentFile().mkdirs();
147 | MusicEditor.editorMusic(file, targetFile, clipsFrameLayout.getStartClips(),
148 | clipsFrameLayout.getEndClips(), clipsFrameLayout.getMaxProgress());
149 | Toast.makeText(this, "剪辑成功,文件保存在: " + targetFile, Toast.LENGTH_LONG).show();
150 | } catch (Exception e) {
151 | e.printStackTrace();
152 | Toast.makeText(this, "剪辑失败,请查看错误日志", Toast.LENGTH_LONG).show();
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/audioclip/src/main/java/com/luowei/audioclip/ClipsFrameLayout.java:
--------------------------------------------------------------------------------
1 | package com.luowei.audioclip;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Bitmap;
6 | import android.graphics.BitmapFactory;
7 | import android.graphics.Canvas;
8 | import android.graphics.Color;
9 | import android.graphics.Paint;
10 | import android.graphics.Rect;
11 | import android.util.AttributeSet;
12 | import android.util.Log;
13 | import android.view.MotionEvent;
14 | import android.view.View;
15 | import android.widget.FrameLayout;
16 |
17 | import com.luowei.audioclip.soundfile.SoundFile;
18 |
19 | /**
20 | * 剪辑布局
21 | * Created by 骆巍 on 2015/10/26.
22 | */
23 | public class ClipsFrameLayout extends FrameLayout {
24 | private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
25 | private int progress;
26 | private int progressHeight;
27 | private int maxProgress;
28 | private ClipsMarkerTextView cmtvStart;
29 | private ClipsMarkerTextView cmtvEnd;
30 | private Bitmap cfBackground;
31 | private int clipsOverColor;
32 | private int minSecond;//最小的剪辑时间
33 | private OnTouchListener startClipsTouchListener;
34 | private int pointColor;
35 | private float pointWidth;
36 |
37 | private Paint wavePaint;
38 | private SoundFile soundFile;
39 | private float[] smoothedGains;
40 |
41 | public ClipsFrameLayout(Context context) {
42 | this(context, null);
43 | }
44 |
45 | public ClipsFrameLayout(Context context, AttributeSet attrs) {
46 | super(context, attrs);
47 |
48 | setWillNotDraw(false);//使该组件有绘图能力
49 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ClipsFrameLayout);
50 | progressHeight = typedArray.getDimensionPixelSize(R.styleable.ClipsFrameLayout_clip_progressHeight, 0);
51 | final int startView = typedArray.getResourceId(R.styleable.ClipsFrameLayout_clip_startId, 0);
52 | final int endView = typedArray.getResourceId(R.styleable.ClipsFrameLayout_clip_endId, 0);
53 | int bgId = typedArray.getResourceId(R.styleable.ClipsFrameLayout_clip_background, 0);
54 | clipsOverColor = typedArray.getColor(R.styleable.ClipsFrameLayout_clip_clipsOverColor, Color.parseColor("#87654321"));
55 | pointWidth = typedArray.getDimension(R.styleable.ClipsFrameLayout_clip_point_width, 1);
56 | paint.setStrokeWidth(pointWidth);
57 | pointColor = typedArray.getColor(R.styleable.ClipsFrameLayout_clip_point_color, Color.WHITE);
58 | minSecond = typedArray.getInteger(R.styleable.ClipsFrameLayout_clip_clipsMinSecond, 10);
59 | if (minSecond < 1 || minSecond > maxProgress/2) {
60 | minSecond = 10;
61 | }
62 | int waveformColor = typedArray.getColor(R.styleable.ClipsFrameLayout_clip_waveformColor, Color.GREEN);
63 | typedArray.recycle();
64 |
65 | cfBackground = BitmapFactory.decodeResource(getResources(), bgId);
66 | wavePaint = new Paint();
67 | wavePaint.setColor(waveformColor);
68 | wavePaint.setStrokeWidth(0);
69 | wavePaint.setAntiAlias(false);
70 |
71 | post(new Runnable() {
72 | @Override
73 | public void run() {
74 | cmtvStart = (ClipsMarkerTextView) findViewById(startView);
75 | cmtvStart.setPointWidth(pointWidth);
76 | cmtvStart.setTranslationX(getWidth() / 3f);
77 | cmtvStart.setSecond(getSecondByPosition(getWidth() / 3f + cmtvStart.getWidth() / 2f));
78 | cmtvStart.setOnTouchListener(new OnTouchListener() {
79 | @Override
80 | public boolean onTouch(View v, MotionEvent event) {
81 | switch (event.getAction()) {
82 | case MotionEvent.ACTION_MOVE:
83 | float x = event.getRawX() - cmtvStart.getWidth() / 2f;
84 | if (cmtvEnd.getTranslationX() - x < getPositionBySecond(minSecond)) {
85 | return false;
86 | }
87 | cmtvStart.setTranslationX(x);
88 | cmtvStart.setSecond(getSecondByPosition(x + cmtvStart.getWidth() / 2f));
89 | // invalidate();
90 | break;
91 | }
92 | if (startClipsTouchListener != null)
93 | startClipsTouchListener.onTouch(v, event);
94 | return true;
95 | }
96 | });
97 | cmtvEnd = (ClipsMarkerTextView) findViewById(endView);
98 | cmtvEnd.setPointWidth(pointWidth);
99 | cmtvEnd.setTranslationX(getWidth() * 2f / 3f);
100 | cmtvEnd.setSecond(getSecondByPosition(getWidth() * 2f / 3f + cmtvEnd.getWidth() / 2f));
101 | cmtvEnd.setOnTouchListener(new OnTouchListener() {
102 | @Override
103 | public boolean onTouch(View v, MotionEvent event) {
104 | switch (event.getAction()) {
105 | case MotionEvent.ACTION_MOVE:
106 | float x = event.getRawX() - cmtvStart.getWidth() / 2f;
107 | if (x - cmtvStart.getTranslationX() < getPositionBySecond(minSecond)) {
108 | return false;
109 | }
110 | cmtvEnd.setTranslationX(x);
111 | cmtvEnd.setSecond(getSecondByPosition(x + cmtvEnd.getWidth() / 2f));
112 | // invalidate();
113 | break;
114 | }
115 | return true;
116 | }
117 | });
118 | }
119 | });
120 | }
121 |
122 | @Override
123 | protected void onAttachedToWindow() {
124 | super.onAttachedToWindow();
125 | // CommonUtil.showToast("onAttachedToWindow");
126 | }
127 |
128 | @Override
129 | protected void onDraw(Canvas canvas) {
130 | super.onDraw(canvas);
131 | if (cfBackground != null) {
132 | Rect r = new Rect(0, 0, getWidth(), progressHeight);
133 | canvas.drawBitmap(cfBackground, null, r, null);
134 | }
135 |
136 | if (soundFile != null) {
137 | int width = getWidth();
138 | int height = getHeight();
139 | float ctr = progressHeight / 2f;
140 | for (int i = 0; i < width; i+=2) {
141 | canvas.drawLine(i, ctr-smoothedGains[i]/2, i, ctr+smoothedGains[i]/2, wavePaint);
142 | }
143 | }
144 |
145 | float x = (float) (progress * getWidth()) / (float) maxProgress;
146 | paint.setColor(pointColor);
147 | canvas.drawLine(x, 0, x, progressHeight, paint);
148 |
149 | if (cmtvStart != null && cmtvEnd != null) {
150 | paint.setColor(clipsOverColor);
151 | float left = cmtvStart.getTranslationX() + cmtvStart.getWidth() / 2f;
152 | float right = cmtvEnd.getTranslationX() + cmtvEnd.getWidth() / 2f;
153 | canvas.drawRect(left, 0, right, progressHeight, paint);
154 | }
155 | }
156 |
157 | @Override
158 | public void draw(Canvas canvas) {
159 | super.draw(canvas);
160 | }
161 |
162 | public void setMaxProgress(int maxProgress) {
163 | this.maxProgress = maxProgress;
164 | cmtvStart.setTranslationX(getWidth() / 3f);
165 | cmtvStart.setSecond(getSecondByPosition(getWidth() / 3f + cmtvStart.getWidth() / 2f));
166 | cmtvEnd.setTranslationX(getWidth() * 2f / 3f);
167 | cmtvEnd.setSecond(getSecondByPosition(getWidth() * 2f / 3f + cmtvEnd.getWidth() / 2f));
168 | }
169 |
170 | public void setProgress(int progress) {
171 | this.progress = progress;
172 | invalidate();
173 | }
174 |
175 | public int getProgress() {
176 | return progress;
177 | }
178 |
179 | public int getMaxProgress() {
180 | return maxProgress;
181 | }
182 |
183 | public int getStartClips() {
184 | return cmtvStart.getSecond();
185 | }
186 |
187 | public int getEndClips() {
188 | return cmtvEnd.getSecond();
189 | }
190 |
191 | private int getSecondByPosition(float position) {
192 | return (int) (position * maxProgress / getWidth());
193 | }
194 |
195 | private float getPositionBySecond(int second) {
196 | return (float) (second * getWidth()) / (float) maxProgress;
197 | }
198 |
199 | public void setStartClipsTouchListener(OnTouchListener startClipsTouchListener) {
200 | this.startClipsTouchListener = startClipsTouchListener;
201 | }
202 |
203 | public void setSoundFile(SoundFile soundFile) {
204 | this.soundFile = soundFile;
205 | computeSmoothedGains();
206 | }
207 |
208 | private void computeSmoothedGains() {
209 | int[] frameGains = soundFile.getFrameGains();
210 | smoothedGains = new float[getWidth()];
211 | int countPerWidth = frameGains.length / getWidth();
212 | if (countPerWidth < 1) countPerWidth = 1;
213 | for (int i = 0; i < smoothedGains.length; i++) {
214 | float sum = 0;
215 | for (int j = 0; j < countPerWidth; j++) {
216 | sum += frameGains[j + i * countPerWidth];
217 | }
218 | smoothedGains[i] = sum/countPerWidth;
219 | }
220 | float maxGains = 0;
221 | for (float a : smoothedGains) {
222 | if (a > maxGains) maxGains = a;
223 | }
224 | float factor = (float) (progressHeight/Math.pow(maxGains,3));
225 | for (int i = 0; i < smoothedGains.length;i++) {
226 | smoothedGains[i] = (float) (Math.pow(smoothedGains[i],3) * factor);
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/audioclip/src/main/java/com/luowei/audioclip/soundfile/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.luowei.audioclip.soundfile;
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> 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> 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 |
--------------------------------------------------------------------------------
/audioclip/src/main/java/com/luowei/audioclip/soundfile/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.luowei.audioclip.soundfile;
18 |
19 | import java.io.BufferedWriter;
20 | import java.io.File;
21 | import java.io.FileWriter;
22 | import java.io.FileOutputStream;
23 | import java.io.IOException;
24 | import java.io.PrintWriter;
25 | import java.io.StringWriter;
26 | import java.nio.ByteBuffer;
27 | import java.nio.ByteOrder;
28 | import java.nio.ShortBuffer;
29 | import java.util.Arrays;
30 |
31 | import android.media.AudioFormat;
32 | import android.media.AudioRecord;
33 | import android.media.MediaCodec;
34 | import android.media.MediaExtractor;
35 | import android.media.MediaFormat;
36 | import android.media.MediaRecorder;
37 | import android.os.Environment;
38 | import android.util.Log;
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 | public InvalidInputException(String message) {
79 | super(message);
80 | }
81 | }
82 |
83 | // TODO(nfaralli): what is the real list of supported extensions? Is it device dependent?
84 | public static String[] getSupportedExtensions() {
85 | return new String[] {"mp3", "wav", "3gpp", "3gp", "amr", "aac", "m4a", "ogg"};
86 | }
87 |
88 | public static boolean isFilenameSupported(String filename) {
89 | String[] extensions = getSupportedExtensions();
90 | for (int i=0; i= 0) {
243 | sample_size = extractor.readSampleData(inputBuffers[inputBufferIndex], 0);
244 | if (firstSampleData
245 | && format.getString(MediaFormat.KEY_MIME).equals("audio/mp4a-latm")
246 | && sample_size == 2) {
247 | // For some reasons on some devices (e.g. the Samsung S3) you should not
248 | // provide the first two bytes of an AAC stream, otherwise the MediaCodec will
249 | // crash. These two bytes do not contain music data but basic info on the
250 | // stream (e.g. channel configuration and sampling frequency), and skipping them
251 | // seems OK with other devices (MediaCodec has already been configured and
252 | // already knows these parameters).
253 | extractor.advance();
254 | tot_size_read += sample_size;
255 | } else if (sample_size < 0) {
256 | // All samples have been read.
257 | codec.queueInputBuffer(
258 | inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
259 | done_reading = true;
260 | } else {
261 | presentation_time = extractor.getSampleTime();
262 | codec.queueInputBuffer(inputBufferIndex, 0, sample_size, presentation_time, 0);
263 | extractor.advance();
264 | tot_size_read += sample_size;
265 | if (mProgressListener != null) {
266 | if (!mProgressListener.reportProgress((float)(tot_size_read) / mFileSize)) {
267 | // We are asked to stop reading the file. Returning immediately. The
268 | // SoundFile object is invalid and should NOT be used afterward!
269 | extractor.release();
270 | extractor = null;
271 | codec.stop();
272 | codec.release();
273 | codec = null;
274 | return;
275 | }
276 | }
277 | }
278 | firstSampleData = false;
279 | }
280 |
281 | // Get decoded stream from the decoder output buffers.
282 | int outputBufferIndex = codec.dequeueOutputBuffer(info, 100);
283 | if (outputBufferIndex >= 0 && info.size > 0) {
284 | if (decodedSamplesSize < info.size) {
285 | decodedSamplesSize = info.size;
286 | decodedSamples = new byte[decodedSamplesSize];
287 | }
288 | outputBuffers[outputBufferIndex].get(decodedSamples, 0, info.size);
289 | outputBuffers[outputBufferIndex].clear();
290 | // Check if buffer is big enough. Resize it if it's too small.
291 | if (mDecodedBytes.remaining() < info.size) {
292 | // Getting a rough estimate of the total size, allocate 20% more, and
293 | // make sure to allocate at least 5MB more than the initial size.
294 | int position = mDecodedBytes.position();
295 | int newSize = (int)((position * (1.0 * mFileSize / tot_size_read)) * 1.2);
296 | if (newSize - position < info.size + 5 * (1<<20)) {
297 | newSize = position + info.size + 5 * (1<<20);
298 | }
299 | ByteBuffer newDecodedBytes = null;
300 | // Try to allocate memory. If we are OOM, try to run the garbage collector.
301 | int retry = 10;
302 | while(retry > 0) {
303 | try {
304 | newDecodedBytes = ByteBuffer.allocate(newSize);
305 | break;
306 | } catch (OutOfMemoryError oome) {
307 | // setting android:largeHeap="true" in seem to help not
308 | // reaching this section.
309 | retry--;
310 | }
311 | }
312 | if (retry == 0) {
313 | // Failed to allocate memory... Stop reading more data and finalize the
314 | // instance with the data decoded so far.
315 | break;
316 | }
317 | //ByteBuffer newDecodedBytes = ByteBuffer.allocate(newSize);
318 | mDecodedBytes.rewind();
319 | newDecodedBytes.put(mDecodedBytes);
320 | mDecodedBytes = newDecodedBytes;
321 | mDecodedBytes.position(position);
322 | }
323 | mDecodedBytes.put(decodedSamples, 0, info.size);
324 | codec.releaseOutputBuffer(outputBufferIndex, false);
325 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
326 | outputBuffers = codec.getOutputBuffers();
327 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
328 | // Subsequent data will conform to new format.
329 | // We could check that codec.getOutputFormat(), which is the new output format,
330 | // is what we expect.
331 | }
332 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
333 | || (mDecodedBytes.position() / (2 * mChannels)) >= expectedNumSamples) {
334 | // We got all the decoded data from the decoder. Stop here.
335 | // Theoretically dequeueOutputBuffer(info, ...) should have set info.flags to
336 | // MediaCodec.BUFFER_FLAG_END_OF_STREAM. However some phones (e.g. Samsung S3)
337 | // won't do that for some files (e.g. with mono AAC files), in which case subsequent
338 | // calls to dequeueOutputBuffer may result in the application crashing, without
339 | // even an exception being thrown... Hence the second check.
340 | // (for mono AAC files, the S3 will actually double each sample, as if the stream
341 | // was stereo. The resulting stream is half what it's supposed to be and with a much
342 | // lower pitch.)
343 | break;
344 | }
345 | }
346 | mNumSamples = mDecodedBytes.position() / (mChannels * 2); // One sample = 2 bytes.
347 | mDecodedBytes.rewind();
348 | mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
349 | mDecodedSamples = mDecodedBytes.asShortBuffer();
350 | mAvgBitRate = (int)((mFileSize * 8) * ((float)mSampleRate / mNumSamples) / 1000);
351 |
352 | extractor.release();
353 | extractor = null;
354 | codec.stop();
355 | codec.release();
356 | codec = null;
357 |
358 | // Temporary hack to make it work with the old version.
359 | mNumFrames = mNumSamples / getSamplesPerFrame();
360 | if (mNumSamples % getSamplesPerFrame() != 0){
361 | mNumFrames++;
362 | }
363 | mFrameGains = new int[mNumFrames];
364 | mFrameLens = new int[mNumFrames];
365 | mFrameOffsets = new int[mNumFrames];
366 | int j;
367 | int gain, value;
368 | int frameLens = (int)((1000 * mAvgBitRate / 8) *
369 | ((float)getSamplesPerFrame() / mSampleRate));
370 | for (i=0; i 0) {
376 | value += Math.abs(mDecodedSamples.get());
377 | }
378 | }
379 | value /= mChannels;
380 | if (gain < value) {
381 | gain = value;
382 | }
383 | }
384 | mFrameGains[i] = (int)Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)...
385 | mFrameLens[i] = frameLens; // totally not accurate...
386 | mFrameOffsets[i] = (int)(i * (1000 * mAvgBitRate / 8) * // = i * frameLens
387 | ((float)getSamplesPerFrame() / mSampleRate));
388 | }
389 | mDecodedSamples.rewind();
390 | // DumpSamples(); // Uncomment this line to dump the samples in a TSV file.
391 | }
392 |
393 | private void RecordAudio() {
394 | if (mProgressListener == null) {
395 | // A progress listener is mandatory here, as it will let us know when to stop recording.
396 | return;
397 | }
398 | mInputFile = null;
399 | mFileType = "raw";
400 | mFileSize = 0;
401 | mSampleRate = 44100;
402 | mChannels = 1; // record mono audio.
403 | short[] buffer = new short[1024]; // buffer contains 1 mono frame of 1024 16 bits samples
404 | int minBufferSize = AudioRecord.getMinBufferSize(
405 | mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
406 | // make sure minBufferSize can contain at least 1 second of audio (16 bits sample).
407 | if (minBufferSize < mSampleRate * 2) {
408 | minBufferSize = mSampleRate * 2;
409 | }
410 | AudioRecord audioRecord = new AudioRecord(
411 | MediaRecorder.AudioSource.DEFAULT,
412 | mSampleRate,
413 | AudioFormat.CHANNEL_IN_MONO,
414 | AudioFormat.ENCODING_PCM_16BIT,
415 | minBufferSize
416 | );
417 |
418 | // Allocate memory for 20 seconds first. Reallocate later if more is needed.
419 | mDecodedBytes = ByteBuffer.allocate(20 * mSampleRate * 2);
420 | mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
421 | mDecodedSamples = mDecodedBytes.asShortBuffer();
422 | audioRecord.startRecording();
423 | while (true) {
424 | // check if mDecodedSamples can contain 1024 additional samples.
425 | if (mDecodedSamples.remaining() < 1024) {
426 | // Try to allocate memory for 10 additional seconds.
427 | int newCapacity = mDecodedBytes.capacity() + 10 * mSampleRate * 2;
428 | ByteBuffer newDecodedBytes = null;
429 | try {
430 | newDecodedBytes = ByteBuffer.allocate(newCapacity);
431 | } catch (OutOfMemoryError oome) {
432 | break;
433 | }
434 | int position = mDecodedSamples.position();
435 | mDecodedBytes.rewind();
436 | newDecodedBytes.put(mDecodedBytes);
437 | mDecodedBytes = newDecodedBytes;
438 | mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
439 | mDecodedBytes.rewind();
440 | mDecodedSamples = mDecodedBytes.asShortBuffer();
441 | mDecodedSamples.position(position);
442 | }
443 | // TODO(nfaralli): maybe use the read method that takes a direct ByteBuffer argument.
444 | audioRecord.read(buffer, 0, buffer.length);
445 | mDecodedSamples.put(buffer);
446 | // Let the progress listener know how many seconds have been recorded.
447 | // The returned value tells us if we should keep recording or stop.
448 | if (!mProgressListener.reportProgress(
449 | (float)(mDecodedSamples.position()) / mSampleRate)) {
450 | break;
451 | }
452 | }
453 | audioRecord.stop();
454 | audioRecord.release();
455 | mNumSamples = mDecodedSamples.position();
456 | mDecodedSamples.rewind();
457 | mDecodedBytes.rewind();
458 | mAvgBitRate = mSampleRate * 16 / 1000;
459 |
460 | // Temporary hack to make it work with the old version.
461 | mNumFrames = mNumSamples / getSamplesPerFrame();
462 | if (mNumSamples % getSamplesPerFrame() != 0){
463 | mNumFrames++;
464 | }
465 | mFrameGains = new int[mNumFrames];
466 | mFrameLens = null; // not needed for recorded audio
467 | mFrameOffsets = null; // not needed for recorded audio
468 | int i, j;
469 | int gain, value;
470 | for (i=0; i 0) {
474 | value = Math.abs(mDecodedSamples.get());
475 | } else {
476 | value = 0;
477 | }
478 | if (gain < value) {
479 | gain = value;
480 | }
481 | }
482 | mFrameGains[i] = (int)Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)...
483 | }
484 | mDecodedSamples.rewind();
485 | // DumpSamples(); // Uncomment this line to dump the samples in a TSV file.
486 | }
487 |
488 | // should be removed in the near future...
489 | public void WriteFile(File outputFile, int startFrame, int numFrames)
490 | throws IOException {
491 | float startTime = (float)startFrame * getSamplesPerFrame() / mSampleRate;
492 | float endTime = (float)(startFrame + numFrames) * getSamplesPerFrame() / mSampleRate;
493 | WriteFile(outputFile, startTime, endTime);
494 | }
495 |
496 | public void WriteFile(File outputFile, float startTime, float endTime)
497 | throws IOException {
498 | int startOffset = (int)(startTime * mSampleRate) * 2 * mChannels;
499 | int numSamples = (int)((endTime - startTime) * mSampleRate);
500 | // Some devices have problems reading mono AAC files (e.g. Samsung S3). Making it stereo.
501 | int numChannels = (mChannels == 1) ? 2 : mChannels;
502 |
503 | String mimeType = "audio/mp4a-latm";
504 | int bitrate = 64000 * numChannels; // rule of thumb for a good quality: 64kbps per channel.
505 | MediaCodec codec = MediaCodec.createEncoderByType(mimeType);
506 | MediaFormat format = MediaFormat.createAudioFormat(mimeType, mSampleRate, numChannels);
507 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
508 | codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
509 | codec.start();
510 |
511 | // Get an estimation of the encoded data based on the bitrate. Add 10% to it.
512 | int estimatedEncodedSize = (int)((endTime - startTime) * (bitrate / 8) * 1.1);
513 | ByteBuffer encodedBytes = ByteBuffer.allocate(estimatedEncodedSize);
514 | ByteBuffer[] inputBuffers = codec.getInputBuffers();
515 | ByteBuffer[] outputBuffers = codec.getOutputBuffers();
516 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
517 | boolean done_reading = false;
518 | long presentation_time = 0;
519 |
520 | int frame_size = 1024; // number of samples per frame per channel for an mp4 (AAC) stream.
521 | byte buffer[] = new byte[frame_size * numChannels * 2]; // a sample is coded with a short.
522 | mDecodedBytes.position(startOffset);
523 | numSamples += (2 * frame_size); // Adding 2 frames, Cf. priming frames for AAC.
524 | int tot_num_frames = 1 + (numSamples / frame_size); // first AAC frame = 2 bytes
525 | if (numSamples % frame_size != 0) {
526 | tot_num_frames++;
527 | }
528 | int[] frame_sizes = new int[tot_num_frames];
529 | int num_out_frames = 0;
530 | int num_frames=0;
531 | int num_samples_left = numSamples;
532 | int encodedSamplesSize = 0; // size of the output buffer containing the encoded samples.
533 | byte[] encodedSamples = null;
534 | while (true) {
535 | // Feed the samples to the encoder.
536 | int inputBufferIndex = codec.dequeueInputBuffer(100);
537 | if (!done_reading && inputBufferIndex >= 0) {
538 | if (num_samples_left <= 0) {
539 | // All samples have been read.
540 | codec.queueInputBuffer(
541 | inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
542 | done_reading = true;
543 | } else {
544 | inputBuffers[inputBufferIndex].clear();
545 | if (buffer.length > inputBuffers[inputBufferIndex].remaining()) {
546 | // Input buffer is smaller than one frame. This should never happen.
547 | continue;
548 | }
549 | // bufferSize is a hack to create a stereo file from a mono stream.
550 | int bufferSize = (mChannels == 1) ? (buffer.length / 2) : buffer.length;
551 | if (mDecodedBytes.remaining() < bufferSize) {
552 | for (int i=mDecodedBytes.remaining(); i < bufferSize; i++) {
553 | buffer[i] = 0; // pad with extra 0s to make a full frame.
554 | }
555 | mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining());
556 | } else {
557 | mDecodedBytes.get(buffer, 0, bufferSize);
558 | }
559 | if (mChannels == 1) {
560 | for (int i=bufferSize - 1; i >= 1; i -= 2) {
561 | buffer[2*i + 1] = buffer[i];
562 | buffer[2*i] = buffer[i-1];
563 | buffer[2*i - 1] = buffer[2*i + 1];
564 | buffer[2*i - 2] = buffer[2*i];
565 | }
566 | }
567 | num_samples_left -= frame_size;
568 | inputBuffers[inputBufferIndex].put(buffer);
569 | presentation_time = (long) (((num_frames++) * frame_size * 1e6) / mSampleRate);
570 | codec.queueInputBuffer(
571 | inputBufferIndex, 0, buffer.length, presentation_time, 0);
572 | }
573 | }
574 |
575 | // Get the encoded samples from the encoder.
576 | int outputBufferIndex = codec.dequeueOutputBuffer(info, 100);
577 | if (outputBufferIndex >= 0 && info.size > 0 && info.presentationTimeUs >=0) {
578 | if (num_out_frames < frame_sizes.length) {
579 | frame_sizes[num_out_frames++] = info.size;
580 | }
581 | if (encodedSamplesSize < info.size) {
582 | encodedSamplesSize = info.size;
583 | encodedSamples = new byte[encodedSamplesSize];
584 | }
585 | outputBuffers[outputBufferIndex].get(encodedSamples, 0, info.size);
586 | outputBuffers[outputBufferIndex].clear();
587 | codec.releaseOutputBuffer(outputBufferIndex, false);
588 | if (encodedBytes.remaining() < info.size) { // Hopefully this should not happen.
589 | estimatedEncodedSize = (int)(estimatedEncodedSize * 1.2); // Add 20%.
590 | ByteBuffer newEncodedBytes = ByteBuffer.allocate(estimatedEncodedSize);
591 | int position = encodedBytes.position();
592 | encodedBytes.rewind();
593 | newEncodedBytes.put(encodedBytes);
594 | encodedBytes = newEncodedBytes;
595 | encodedBytes.position(position);
596 | }
597 | encodedBytes.put(encodedSamples, 0, info.size);
598 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
599 | outputBuffers = codec.getOutputBuffers();
600 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
601 | // Subsequent data will conform to new format.
602 | // We could check that codec.getOutputFormat(), which is the new output format,
603 | // is what we expect.
604 | }
605 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
606 | // We got all the encoded data from the encoder.
607 | break;
608 | }
609 | }
610 | int encoded_size = encodedBytes.position();
611 | encodedBytes.rewind();
612 | codec.stop();
613 | codec.release();
614 | codec = null;
615 |
616 | // Write the encoded stream to the file, 4kB at a time.
617 | buffer = new byte[4096];
618 | try {
619 | FileOutputStream outputStream = new FileOutputStream(outputFile);
620 | outputStream.write(
621 | MP4Header.getMP4Header(mSampleRate, numChannels, frame_sizes, bitrate));
622 | while (encoded_size - encodedBytes.position() > buffer.length) {
623 | encodedBytes.get(buffer);
624 | outputStream.write(buffer);
625 | }
626 | int remaining = encoded_size - encodedBytes.position();
627 | if (remaining > 0) {
628 | encodedBytes.get(buffer, 0, remaining);
629 | outputStream.write(buffer, 0, remaining);
630 | }
631 | outputStream.close();
632 | } catch (IOException e) {
633 | Log.e("Ringdroid", "Failed to create the .m4a file.");
634 | Log.e("Ringdroid", getStackTrace(e));
635 | }
636 | }
637 |
638 | // Method used to swap the left and right channels (needed for stereo WAV files).
639 | // buffer contains the PCM data: {sample 1 right, sample 1 left, sample 2 right, etc.}
640 | // The size of a sample is assumed to be 16 bits (for a single channel).
641 | // When done, buffer will contain {sample 1 left, sample 1 right, sample 2 left, etc.}
642 | private void swapLeftRightChannels(byte[] buffer) {
643 | byte left[] = new byte[2];
644 | byte right[] = new byte[2];
645 | if (buffer.length % 4 != 0) { // 2 channels, 2 bytes per sample (for one channel).
646 | // Invalid buffer size.
647 | return;
648 | }
649 | for (int offset = 0; offset < buffer.length; offset += 4) {
650 | left[0] = buffer[offset];
651 | left[1] = buffer[offset + 1];
652 | right[0] = buffer[offset + 2];
653 | right[1] = buffer[offset + 3];
654 | buffer[offset] = right[0];
655 | buffer[offset + 1] = right[1];
656 | buffer[offset + 2] = left[0];
657 | buffer[offset + 3] = left[1];
658 | }
659 | }
660 |
661 | // should be removed in the near future...
662 | public void WriteWAVFile(File outputFile, int startFrame, int numFrames)
663 | throws IOException {
664 | float startTime = (float)startFrame * getSamplesPerFrame() / mSampleRate;
665 | float endTime = (float)(startFrame + numFrames) * getSamplesPerFrame() / mSampleRate;
666 | WriteWAVFile(outputFile, startTime, endTime);
667 | }
668 |
669 | public void WriteWAVFile(File outputFile, float startTime, float endTime)
670 | throws IOException {
671 | int startOffset = (int)(startTime * mSampleRate) * 2 * mChannels;
672 | int numSamples = (int)((endTime - startTime) * mSampleRate);
673 |
674 | // Start by writing the RIFF header.
675 | FileOutputStream outputStream = new FileOutputStream(outputFile);
676 | outputStream.write(WAVHeader.getWAVHeader(mSampleRate, mChannels, numSamples));
677 |
678 | // Write the samples to the file, 1024 at a time.
679 | byte buffer[] = new byte[1024 * mChannels * 2]; // Each sample is coded with a short.
680 | mDecodedBytes.position(startOffset);
681 | int numBytesLeft = numSamples * mChannels * 2;
682 | while (numBytesLeft >= buffer.length) {
683 | if (mDecodedBytes.remaining() < buffer.length) {
684 | // This should not happen.
685 | for (int i = mDecodedBytes.remaining(); i < buffer.length; i++) {
686 | buffer[i] = 0; // pad with extra 0s to make a full frame.
687 | }
688 | mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining());
689 | } else {
690 | mDecodedBytes.get(buffer);
691 | }
692 | if (mChannels == 2) {
693 | swapLeftRightChannels(buffer);
694 | }
695 | outputStream.write(buffer);
696 | numBytesLeft -= buffer.length;
697 | }
698 | if (numBytesLeft > 0) {
699 | if (mDecodedBytes.remaining() < numBytesLeft) {
700 | // This should not happen.
701 | for (int i = mDecodedBytes.remaining(); i < numBytesLeft; i++) {
702 | buffer[i] = 0; // pad with extra 0s to make a full frame.
703 | }
704 | mDecodedBytes.get(buffer, 0, mDecodedBytes.remaining());
705 | } else {
706 | mDecodedBytes.get(buffer, 0, numBytesLeft);
707 | }
708 | if (mChannels == 2) {
709 | swapLeftRightChannels(buffer);
710 | }
711 | outputStream.write(buffer, 0, numBytesLeft);
712 | }
713 | outputStream.close();
714 | }
715 |
716 | // Debugging method dumping all the samples in mDecodedSamples in a TSV file.
717 | // Each row describes one sample and has the following format:
718 | // "\t\t...\t\n"
719 | // File will be written on the SDCard under media/audio/debug/
720 | // If fileName is null or empty, then the default file name (samples.tsv) is used.
721 | private void DumpSamples(String fileName) {
722 | String externalRootDir = Environment.getExternalStorageDirectory().getPath();
723 | if (!externalRootDir.endsWith("/")) {
724 | externalRootDir += "/";
725 | }
726 | String parentDir = externalRootDir + "media/audio/debug/";
727 | // Create the parent directory
728 | File parentDirFile = new File(parentDir);
729 | parentDirFile.mkdirs();
730 | // If we can't write to that special path, try just writing directly to the SDCard.
731 | if (!parentDirFile.isDirectory()) {
732 | parentDir = externalRootDir;
733 | }
734 | if (fileName == null || fileName.isEmpty()) {
735 | fileName = "samples.tsv";
736 | }
737 | File outFile = new File(parentDir + fileName);
738 |
739 | // Start dumping the samples.
740 | BufferedWriter writer = null;
741 | float presentationTime = 0;
742 | mDecodedSamples.rewind();
743 | String row;
744 | try {
745 | writer = new BufferedWriter(new FileWriter(outFile));
746 | for (int sampleIndex = 0; sampleIndex < mNumSamples; sampleIndex++) {
747 | presentationTime = (float)(sampleIndex) / mSampleRate;
748 | row = Float.toString(presentationTime);
749 | for (int channelIndex = 0; channelIndex < mChannels; channelIndex++) {
750 | row += "\t" + mDecodedSamples.get();
751 | }
752 | row += "\n";
753 | writer.write(row);
754 | }
755 | } catch (IOException e) {
756 | Log.w("Ringdroid", "Failed to create the sample TSV file.");
757 | Log.w("Ringdroid", getStackTrace(e));
758 | }
759 | // We are done here. Close the file and rewind the buffer.
760 | try {
761 | writer.close();
762 | } catch (Exception e) {
763 | Log.w("Ringdroid", "Failed to close sample TSV file.");
764 | Log.w("Ringdroid", getStackTrace(e));
765 | }
766 | mDecodedSamples.rewind();
767 | }
768 |
769 | // Helper method (samples will be dumped in media/audio/debug/samples.tsv).
770 | private void DumpSamples() {
771 | DumpSamples(null);
772 | }
773 |
774 | // Return the stack trace of a given exception.
775 | private String getStackTrace(Exception e) {
776 | StringWriter writer = new StringWriter();
777 | e.printStackTrace(new PrintWriter(writer));
778 | return writer.toString();
779 | }
780 | }
781 |
--------------------------------------------------------------------------------