();
74 | for (String line : lines) {
75 | if (!line.contains(separator))
76 | throw new IllegalArgumentException("line: [" + line + "] has no separator:" + separator);
77 | String key = Strings.subStringUntilFirst(line, separator).trim();
78 | String value = Strings.subStringAfterFirst(line, separator).trim();
79 | result.put(key, value);
80 | }
81 | return result;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/jhelper/src/main/java/com/hwangjr/jhelper/LineIterator.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.Closeable;
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.io.Reader;
8 | import java.util.Iterator;
9 | import java.util.NoSuchElementException;
10 |
11 | /**
12 | * An Iterator over the lines in a Reader
.
13 | *
14 | * LineIterator
holds a reference to an open Reader
.
15 | * if there hasNext() returns false, it automatically closes the reader. if somewhat
16 | * an early return is possible iterator or the reader should be closed by calling {@link #close()} method.
17 | *
18 | * The recommended usage pattern is:
19 | *
20 | * LineIterator it = new LineIterator(Files.getReader("filename", "UTF-8"));
21 | * try {
22 | * while (it.hasNext()) {
23 | * String line = it.next();
24 | * /// do something with line
25 | * }
26 | * } finally {
27 | * it.close();
28 | * }
29 | *
30 | *
31 | * This class uses code from Apache commons io LineIterator class. however, it's behavior is slightly different.
32 | */
33 | public class LineIterator implements Iterator, Closeable {
34 |
35 | private final BufferedReader bufferedReader;
36 | /**
37 | * The current line.
38 | */
39 | private String cachedLine;
40 | /**
41 | * A flag indicating if the iterator has been fully read.
42 | */
43 | private boolean finished = false;
44 |
45 | private boolean trim = false;
46 |
47 | private Filter filters[] = new Filter[0];
48 |
49 | public LineIterator(InputStream is) {
50 | Preconditions.checkNotNull(is, "InputStream cannot be null!");
51 | this.bufferedReader = IOs.getReader(is);
52 | }
53 |
54 | public LineIterator(Reader reader) {
55 | Preconditions.checkNotNull(reader, "Reader cannot be null!");
56 | if (reader instanceof BufferedReader)
57 | this.bufferedReader = (BufferedReader) reader;
58 | else
59 | this.bufferedReader = new BufferedReader(reader);
60 | }
61 |
62 | public LineIterator(Reader reader, boolean trim, Filter... filters) {
63 | Preconditions.checkNotNull(reader, "Reader cannot be null!");
64 | if (reader instanceof BufferedReader) {
65 | this.bufferedReader = (BufferedReader) reader;
66 | } else
67 | this.bufferedReader = new BufferedReader(reader);
68 | if (filters != null && filters.length > 0)
69 | this.filters = filters;
70 | this.trim = trim;
71 | }
72 |
73 | public boolean hasNext() {
74 | if (cachedLine != null) {
75 | return true;
76 | } else if (finished) {
77 | close();
78 | return false;
79 | } else {
80 | try {
81 | String line;
82 | do {
83 | line = bufferedReader.readLine();
84 | if (line != null && trim) {
85 | line = line.trim();
86 | }
87 | }
88 | while (line != null && filters.length > 0 && !StringFilters.canPassAll(line, filters));
89 |
90 | if (line == null) {
91 | finished = true;
92 | close();
93 | return false;
94 | } else {
95 | cachedLine = line;
96 | return true;
97 | }
98 | } catch (IOException ioe) {
99 | close();
100 | throw new IllegalStateException(ioe.toString());
101 | }
102 | }
103 | }
104 |
105 | public String next() {
106 | if (!hasNext()) {
107 | close();
108 | throw new NoSuchElementException("No more lines");
109 | }
110 | String currentLine = cachedLine;
111 | cachedLine = null;
112 | return currentLine;
113 | }
114 |
115 | public void remove() {
116 | throw new UnsupportedOperationException("remove() is not implemented in LineIterator class.");
117 | }
118 |
119 | public void close() {
120 | IOs.closeSilently(bufferedReader);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/jhelper/src/main/java/com/hwangjr/jhelper/MultiFileLineIterator.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.ArrayList;
6 | import java.util.Arrays;
7 | import java.util.Iterator;
8 | import java.util.List;
9 |
10 | public class MultiFileLineIterator implements Iterator {
11 |
12 | private int fileCursor = 0;
13 | private LineIterator currentIterator;
14 | private final List files;
15 | private SimpleTextReader.Template template = new SimpleTextReader.Template();
16 |
17 | public MultiFileLineIterator(File... files) throws IOException {
18 | this.files = new ArrayList(Arrays.asList(files));
19 | currentIterator = template.generateReader(files[0]).getLineIterator();
20 | }
21 |
22 | public MultiFileLineIterator(SimpleTextReader.Template template, File... files) throws IOException {
23 | this.files = new ArrayList(Arrays.asList(files));
24 | currentIterator = template.generateReader(files[0]).getLineIterator();
25 | this.template = template;
26 | }
27 |
28 | public MultiFileLineIterator(List files) throws IOException {
29 | this.files = files;
30 | currentIterator = template.generateReader(files.get(0)).getLineIterator();
31 | }
32 |
33 | public MultiFileLineIterator(SimpleTextReader.Template template, List files) throws IOException {
34 | this.files = new ArrayList(files);
35 | this.template = template;
36 | currentIterator = template.generateReader(files.get(0)).getLineIterator();
37 | }
38 |
39 | public boolean hasNext() {
40 | while (!currentIterator.hasNext()) {
41 | fileCursor++;
42 | currentIterator.close();
43 | if (fileCursor < files.size())
44 | try {
45 | currentIterator = template.generateReader(files.get(fileCursor)).getLineIterator();
46 | } catch (IOException e) {
47 | e.printStackTrace();
48 | }
49 | else return false;
50 | }
51 | return true;
52 | }
53 |
54 | public String next() {
55 | return currentIterator.next();
56 | }
57 |
58 | public void remove() {
59 | throw new UnsupportedOperationException("remove ise not supported here.");
60 | }
61 | }
--------------------------------------------------------------------------------
/jhelper/src/main/java/com/hwangjr/jhelper/StringFilters.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import java.util.regex.Pattern;
4 |
5 | import static com.hwangjr.jhelper.Preconditions.checkArgument;
6 | import static com.hwangjr.jhelper.Preconditions.checkNotNull;
7 |
8 | class StringFilters {
9 |
10 | public static final Filter PASS_ALL = new AllPassFilter();
11 | public static final Filter PASS_NON_NULL_OR_EMPTY = new NullOrEmptyFilter();
12 | public static final Filter PASS_ONLY_TEXT = new HasNoTextFilter();
13 |
14 | public static Filter newRegexpFilter(String regexp) {
15 | return new RegexpFilter(regexp, false);
16 | }
17 |
18 | public static Filter newRegexpFilterIgnoreCase(String regexp) {
19 | return new RegexpFilter(regexp, true);
20 | }
21 |
22 | public static Filter newRegexpFilter(Pattern pattern) {
23 | return new RegexpFilter(pattern);
24 | }
25 |
26 | public static Filter newPrefixFilter(String prefix) {
27 | return new PrefixFilter(prefix);
28 | }
29 |
30 | private static class AllPassFilter implements Filter {
31 | public boolean canPass(String str) {
32 | return true;
33 | }
34 | }
35 |
36 | private static class NullOrEmptyFilter implements Filter {
37 | public boolean canPass(String str) {
38 | return !Strings.isNullOrEmpty(str);
39 | }
40 | }
41 |
42 | private static class HasNoTextFilter implements Filter {
43 | public boolean canPass(String str) {
44 | return Strings.hasText(str);
45 | }
46 | }
47 |
48 | private static class PrefixFilter implements Filter {
49 | String token;
50 |
51 | private PrefixFilter(String token) {
52 | checkNotNull(token, "Cannot initialize Filter with null string.");
53 | this.token = token;
54 | }
55 |
56 | public boolean canPass(String s) {
57 | return s != null && s.startsWith(token);
58 | }
59 | }
60 |
61 | private static class RegexpFilter implements Filter {
62 | final Pattern pattern;
63 |
64 | public RegexpFilter(String regExp, boolean ignoreCase) {
65 | checkNotNull(regExp, "regexp String cannot be null.");
66 | checkArgument(!Strings.isNullOrEmpty(regExp), "regexp String cannot be empty");
67 | if (ignoreCase)
68 | this.pattern = Pattern.compile(regExp, Pattern.CASE_INSENSITIVE);
69 | else
70 | this.pattern = Pattern.compile(regExp);
71 | }
72 |
73 | public RegexpFilter(Pattern pattern) {
74 | this.pattern = pattern;
75 | }
76 |
77 | public boolean canPass(String s) {
78 | return s != null && pattern.matcher(s).find();
79 | }
80 | }
81 |
82 | public static boolean canPassAll(String s, Filter... filters) {
83 |
84 | for (Filter filter : filters) {
85 | if (!filter.canPass(s))
86 | return false;
87 | }
88 | return true;
89 | }
90 |
91 | public static boolean canPassAny(String s, Filter... filters) {
92 | for (Filter filter : filters) {
93 | if (filter.canPass(s))
94 | return true;
95 | }
96 | return false;
97 | }
98 |
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/CountingSetTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import junit.framework.Assert;
4 |
5 | import org.junit.Test;
6 |
7 | public class CountingSetTest {
8 |
9 | @Test
10 | public void testGenerate() {
11 | CountingSet histogram = new CountingSet();
12 | histogram.add("Apple", "Pear", "Plum", "Apple", "Apple", "Grape", "Pear");
13 | Assert.assertEquals(3, histogram.getCount("Apple"));
14 | Assert.assertEquals(2, histogram.getCount("Pear"));
15 | Assert.assertEquals(1, histogram.getCount("Plum"));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/FilesTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import org.junit.Ignore;
4 | import org.junit.Test;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 |
9 | import static org.junit.Assert.assertEquals;
10 |
11 | public class FilesTest {
12 | @Test
13 | public void MD5CalculationTest() throws IOException {
14 | assertEquals("873362e429c261e3596ad1d387ad152e",
15 | Bytes.toHex(Files.calculateMD5(new File("test/file_for_md5.txt"))));
16 | }
17 |
18 | @Ignore("Not a unit test")
19 | @Test
20 | public void fileAppendTest() throws IOException {
21 | Files.appendFiles(new File("apended.txt"), new File("test/file_for_md5.txt"), new File("test/multi_line_text_file.txt"));
22 | }
23 |
24 | @Ignore("Not a unit test")
25 | @Test
26 | public void testHexDump() throws IOException {
27 | Files.hexDump(new File("test/multi_line_text_file.txt"), -1);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/IOTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import org.junit.Ignore;
4 | import org.junit.Test;
5 |
6 | import java.io.FileOutputStream;
7 | import java.io.IOException;
8 | import java.net.URL;
9 |
10 |
11 | public class IOTest {
12 |
13 | @Ignore("Not a unit test")
14 | @Test
15 | public void readURI() throws IOException {
16 | URL url = new URL("http://www.google.com");
17 | IOs.copy(url.openStream(), new FileOutputStream("blah.txt"));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/KeyValueReaderTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.util.Map;
9 |
10 | public class KeyValueReaderTest {
11 |
12 | @Test
13 | public void testReader() throws IOException {
14 | Map map = new KeyValueReader(":")
15 | .loadFromFile(new File("test/key-value-colon-separator.txt"));
16 | Assert.assertEquals(map.size(), 4);
17 | Assert.assertTrue(TestUtil.containsAllKeys(map, "1", "2", "3", "4"));
18 | Assert.assertTrue(TestUtil.containsAllValues(map, "bir", "iki", "uc", "dort"));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/SimpleTextReaderTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import org.junit.Test;
4 |
5 | import java.io.File;
6 | import java.io.IOException;
7 | import java.util.List;
8 | import java.util.regex.Pattern;
9 |
10 | import static java.lang.System.getProperty;
11 | import static java.lang.System.out;
12 | import static org.junit.Assert.assertEquals;
13 |
14 | public class SimpleTextReaderTest {
15 |
16 | private static String curDir = getProperty("user.dir");
17 |
18 | @Test
19 | public void testUtf8() throws IOException {
20 | String content = new SimpleTextReader("test/turkish_utf8_with_BOM.txt", "utf-8").asString();
21 | assertEquals(content, "\u015fey");
22 | }
23 |
24 | @Test
25 | public void multilineTest() throws IOException {
26 | List list = new SimpleTextReader("test/multi_line_text_file.txt").asStringList();
27 | assertEquals(list.size(), 17);
28 | assertEquals(list.get(1), "uno");
29 | //test trim
30 | assertEquals(list.get(2), " dos");
31 | }
32 |
33 | @Test
34 | public void multilineConstarintTest() throws IOException {
35 | List list = new SimpleTextReader.Builder("test/multi_line_text_file.txt")
36 | .allowMatchingRegexp("^[^#]")
37 | .ignoreWhiteSpaceLines()
38 | .trim()
39 | .build()
40 | .asStringList();
41 | assertEquals(list.size(), 12);
42 | assertEquals(list.get(0), "uno");
43 | assertEquals(list.get(1), "dos");
44 | }
45 |
46 | public void templateTest() throws IOException {
47 | SimpleTextReader.Template template = new SimpleTextReader.Template()
48 | .allowMatchingRegexp("^[^#]")
49 | .ignoreWhiteSpaceLines()
50 | .trim();
51 | List files = Files.crawlDirectory(new File("blah"));
52 | for (File file : files) {
53 | SimpleTextReader sr = template.generateReader(file);
54 | //....
55 | }
56 |
57 | }
58 |
59 | @Test
60 | public void asStringTest() throws IOException {
61 | String a = new SimpleTextReader("test/multi_line_text_file.txt").asString();
62 | System.out.println(a);
63 | }
64 |
65 | @Test
66 | public void iterableTest() throws IOException {
67 | int i = 0;
68 | for (String s : new SimpleTextReader("test/multi_line_text_file.txt").getIterableReader()) {
69 | if (i == 1) assertEquals(s.trim(), "uno");
70 | if (i == 2) assertEquals(s.trim(), "dos");
71 | if (i == 3) assertEquals(s.trim(), "tres");
72 | i++;
73 | }
74 | assertEquals(i, 17);
75 | }
76 |
77 | @Test
78 | public void lineIteratorTest2() throws IOException {
79 | LineIterator li = new SimpleTextReader("test/multi_line_text_file.txt").getLineIterator();
80 | while (li.hasNext())
81 | out.println(li.next().toUpperCase());
82 | IOs.closeSilently(li);
83 |
84 | }
85 |
86 | @Test
87 | public void lineIteratorWithConstraint() throws IOException {
88 | LineIterator li = new SimpleTextReader
89 | .Builder("test/multi_line_text_file.txt")
90 | .ignoreWhiteSpaceLines()
91 | .trim()
92 | .build().getLineIterator();
93 |
94 | int i = 0;
95 | while (li.hasNext()) {
96 | String s = li.next();
97 | if (i == 0) assertEquals(s, "uno");
98 | if (i == 1) assertEquals(s, "dos");
99 | i++;
100 | }
101 | IOs.closeSilently(li);
102 | }
103 |
104 | public static void main(String[] args) {
105 | Pattern patten = Pattern.compile("^[#]+");
106 |
107 | System.out.println(" ### sdd".replaceAll("^[^#]", "ass"));
108 | }
109 |
110 | }
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/SimpleTextWriterTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import org.junit.After;
4 | import org.junit.Assert;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.util.ArrayList;
11 | import java.util.Arrays;
12 | import java.util.List;
13 |
14 | public class SimpleTextWriterTest {
15 |
16 | File tmpDir;
17 | File tmpFile;
18 |
19 | @Before
20 | public void before() {
21 | tmpDir = new File(Systems.getJavaIoTmpDir() + "/jcaki");
22 | if (!tmpDir.exists())
23 | tmpDir.mkdir();
24 | tmpFile = new File(tmpDir, "jcaki.txt");
25 | }
26 |
27 | @After
28 | public void after() {
29 | tmpFile.delete();
30 | }
31 |
32 | @Test
33 | public void WriteStringTest() throws IOException {
34 | new SimpleTextWriter(tmpFile).write("Hello World!");
35 | Assert.assertEquals(new SimpleTextReader(tmpFile).asString(), "Hello World!");
36 | new SimpleTextWriter(tmpFile).write(null);
37 | Assert.assertEquals(new SimpleTextReader(tmpFile).asString(), "");
38 | new SimpleTextWriter(tmpFile).write("");
39 | Assert.assertEquals(new SimpleTextReader(tmpFile).asString(), "");
40 | }
41 |
42 | @Test
43 | public void WriteStringKeepOpenTest() throws IOException {
44 | SimpleTextWriter sfw = new SimpleTextWriter
45 | .Builder(tmpFile)
46 | .keepOpen()
47 | .build();
48 | sfw.write("Hello");
49 | sfw.write("Merhaba");
50 | sfw.write("");
51 | sfw.write(null);
52 | IOs.closeSilently(sfw);
53 | Assert.assertEquals("HelloMerhaba", new SimpleTextReader(tmpFile).asString());
54 |
55 | }
56 |
57 | @Test(expected = IOException.class)
58 | public void keepOpenExcepionTest() throws IOException {
59 | SimpleTextWriter sfw = new SimpleTextWriter
60 | .Builder(tmpFile)
61 | .build();
62 | sfw.write("Hello");
63 | sfw.write("Now it will throw an exception..");
64 | }
65 |
66 | @Test
67 | public void WriteMultiLineStringTest() throws IOException {
68 | List strs = new ArrayList(Arrays.asList("Merhaba", "Dunya", ""));
69 | new SimpleTextWriter(tmpFile).writeLines(strs);
70 | List read = new SimpleTextReader(tmpFile).asStringList();
71 | for (int i = 0; i < read.size(); i++) {
72 | Assert.assertEquals(read.get(i), strs.get(i));
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/TestSystems.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import org.junit.Test;
4 |
5 | import java.util.Map;
6 |
7 | public class TestSystems {
8 |
9 | @Test
10 | public void testHome() {
11 | System.out.println(Systems.getUserHome());
12 | }
13 |
14 | public static void main(String[] args) {
15 | Map p = System.getenv();
16 | for (String s : p.keySet()) {
17 | System.out.println(s + " : " + p.get(s));
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/jhelper/src/test/java/com/hwangjr/jhelper/TestUtil.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.jhelper;
2 |
3 | import java.util.Map;
4 |
5 | public class TestUtil {
6 |
7 | public static boolean containsAllKeys(Map map, T... keys) {
8 | for (T key : keys) {
9 | if (!map.containsKey(key)) return false;
10 | }
11 | return true;
12 | }
13 |
14 | public static boolean containsAllValues(Map, V> map, V... values) {
15 | for (V value : values) {
16 | if (!map.containsValue(value)) return false;
17 | }
18 | return true;
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/recordhelper/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/recordhelper/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "23.0.2"
6 |
7 | defaultConfig {
8 | minSdkVersion 9
9 | targetSdkVersion 23
10 | versionCode 1
11 | versionName "1.0"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | compile fileTree(dir: 'libs', include: ['*.jar'])
23 | compile project(':soundhelper')
24 | }
25 |
--------------------------------------------------------------------------------
/recordhelper/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 /usr/local/android-sdk-linux/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 |
--------------------------------------------------------------------------------
/recordhelper/src/androidTest/java/com/hwangjr/recordhepler/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhepler;
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 | }
--------------------------------------------------------------------------------
/recordhelper/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/audio/AudioPlaybackManager.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.audio;
2 |
3 | import android.content.Context;
4 | import android.media.MediaPlayer;
5 | import android.media.MediaPlayer.OnCompletionListener;
6 | import android.media.MediaPlayer.OnPreparedListener;
7 | import android.view.ViewTreeObserver.OnGlobalLayoutListener;
8 | import android.widget.MediaController;
9 | import android.widget.MediaController.MediaPlayerControl;
10 |
11 | import com.hwangjr.recordhelper.video.MediaPlayerManager;
12 | import com.hwangjr.recordhelper.video.PlaybackHandler;
13 | import com.hwangjr.recordhelper.visualizer.VisualizerView;
14 |
15 | /*
16 | * Controls audio playback
17 | */
18 | public class AudioPlaybackManager implements OnGlobalLayoutListener, OnPreparedListener, MediaPlayerControl, OnCompletionListener {
19 | private VisualizerView visualizerView;
20 | private MediaPlayerManager playerManager;
21 | private MediaController controller;
22 | private PlaybackHandler playbackHandler;
23 | private boolean isPlayerPrepared, isSurfaceCreated;
24 |
25 | public AudioPlaybackManager(Context ctx, VisualizerView visualizerView, PlaybackHandler playbackHandler) {
26 | this.playerManager = new MediaPlayerManager();
27 | this.playerManager.getPlayer().setOnPreparedListener(this);
28 | this.playerManager.getPlayer().setOnCompletionListener(this);
29 |
30 | this.visualizerView = visualizerView;
31 | this.visualizerView.link(this.playerManager.getPlayer());
32 | this.visualizerView.getViewTreeObserver().addOnGlobalLayoutListener(this);
33 |
34 | this.controller = new MediaController(ctx);
35 | this.controller.setMediaPlayer(this);
36 | this.controller.setAnchorView(visualizerView);
37 |
38 | this.playbackHandler = playbackHandler;
39 | }
40 |
41 | public void setupPlayback(String fileName) {
42 | playerManager.setupPlayback(fileName);
43 | }
44 |
45 | public void showMediaController() {
46 | if (!controller.isEnabled()) {
47 | controller.setEnabled(true);
48 | }
49 | controller.show();
50 | }
51 |
52 | public void hideMediaController() {
53 | controller.hide();
54 | controller.setEnabled(false);
55 | }
56 |
57 | private void releaseVisualizer() {
58 | visualizerView.release();
59 | visualizerView = null;
60 | }
61 |
62 | public void dispose() {
63 | playerManager.releasePlayer();
64 | releaseVisualizer();
65 | controller = null;
66 | playbackHandler = null;
67 | }
68 |
69 | //visualizer setup callback *****************************************************************
70 |
71 | @Override
72 | public void onGlobalLayout() {
73 | isSurfaceCreated = true;
74 | if (isPlayerPrepared && isSurfaceCreated) {
75 | playbackHandler.onPreparePlayback();
76 | }
77 | }
78 |
79 | //media player and controller callbacks *****************************************************
80 |
81 | @Override
82 | public void onPrepared(MediaPlayer mp) {
83 | isPlayerPrepared = true;
84 | if (isPlayerPrepared && isSurfaceCreated) {
85 | playbackHandler.onPreparePlayback();
86 | }
87 | }
88 |
89 | @Override
90 | public void start() {
91 | playerManager.startPlaying();
92 | }
93 |
94 | @Override
95 | public void pause() {
96 | playerManager.pausePlaying();
97 | }
98 |
99 | @Override
100 | public void seekTo(int arg0) {
101 | playerManager.seekTo(arg0);
102 | }
103 |
104 | @Override
105 | public void onCompletion(MediaPlayer mp) {
106 | playerManager.seekTo(0);
107 | }
108 |
109 | @Override
110 | public boolean isPlaying() {
111 | return playerManager.isPlaying();
112 | }
113 |
114 | @Override
115 | public int getCurrentPosition() {
116 | return playerManager.getCurrentPosition();
117 | }
118 |
119 | @Override
120 | public int getDuration() {
121 | return playerManager.getDuration();
122 | }
123 |
124 | @Override
125 | public boolean canPause() {
126 | return true;
127 | }
128 |
129 | @Override
130 | public boolean canSeekBackward() {
131 | return true;
132 | }
133 |
134 | @Override
135 | public boolean canSeekForward() {
136 | return true;
137 | }
138 |
139 | @Override
140 | public int getAudioSessionId() {
141 | return 0;
142 | }
143 |
144 | @Override
145 | public int getBufferPercentage() {
146 | return 0;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/audio/AudioRecordingHandler.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.audio;
2 |
3 | public interface AudioRecordingHandler {
4 | public void onFftDataCapture(byte[] bytes);
5 |
6 | public void onRecordSuccess();
7 |
8 | public void onRecordingError();
9 |
10 | public void onRecordSaveError();
11 | }
12 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/audio/AudioRecordingThread.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.audio;
2 |
3 | import android.media.AudioFormat;
4 | import android.media.AudioRecord;
5 | import android.media.MediaRecorder.AudioSource;
6 |
7 | import com.hwangjr.recordhelper.fft.Complex;
8 | import com.hwangjr.recordhelper.fft.FFT;
9 | import com.hwangjr.soundhelper.pcm.PcmAudioHelper;
10 | import com.hwangjr.soundhelper.pcm.WavAudioFormat;
11 |
12 | import java.io.File;
13 | import java.io.FileNotFoundException;
14 | import java.io.FileOutputStream;
15 | import java.io.IOException;
16 |
17 | /*
18 | * Takes a portion of PCM encoded audio signal (from microphone while recording),
19 | * transforms it using FFT, passes it to a visualizer and saves to a file.
20 | * In the end converts stored audio from a temporary RAW file to WAV.
21 | */
22 | public class AudioRecordingThread extends Thread {
23 | private static final String FILE_NAME = "audiorecordtest.raw";
24 | private static final int SAMPLING_RATE = 44100;
25 | private static final int FFT_POINTS = 1024;
26 | private static final int MAGIC_SCALE = 10;
27 |
28 | private String fileName_wav;
29 | private String fileName_raw;
30 |
31 | private int bufferSize;
32 | private byte[] audioBuffer;
33 |
34 | private boolean isRecording = true;
35 |
36 | private AudioRecordingHandler handler = null;
37 |
38 | public AudioRecordingThread(String fileWavName, AudioRecordingHandler handler) {
39 | this.fileName_wav = fileWavName;
40 | this.fileName_raw = getRawName(fileWavName);
41 | this.handler = handler;
42 |
43 | bufferSize = AudioRecord.getMinBufferSize(SAMPLING_RATE,
44 | AudioFormat.CHANNEL_IN_MONO,
45 | AudioFormat.ENCODING_PCM_16BIT);
46 | audioBuffer = new byte[bufferSize];
47 | }
48 |
49 | @Override
50 | public void run() {
51 | FileOutputStream out = prepareWriting();
52 | if (out == null) {
53 | return;
54 | }
55 |
56 | AudioRecord record = new AudioRecord(AudioSource.VOICE_RECOGNITION, /*AudioSource.MIC*/
57 | SAMPLING_RATE,
58 | AudioFormat.CHANNEL_IN_MONO,
59 | AudioFormat.ENCODING_PCM_16BIT,
60 | bufferSize);
61 | record.startRecording();
62 |
63 | int read = 0;
64 | while (isRecording) {
65 | read = record.read(audioBuffer, 0, bufferSize);
66 |
67 | if ((read == AudioRecord.ERROR_INVALID_OPERATION) ||
68 | (read == AudioRecord.ERROR_BAD_VALUE) ||
69 | (read <= 0)) {
70 | continue;
71 | }
72 |
73 | proceed();
74 | write(out);
75 | }
76 |
77 | record.stop();
78 | record.release();
79 |
80 | finishWriting(out);
81 | convertRawToWav();
82 | }
83 |
84 | private void proceed() {
85 | double temp;
86 | Complex[] y;
87 | Complex[] complexSignal = new Complex[FFT_POINTS];
88 |
89 | for (int i = 0; i < FFT_POINTS; i++) {
90 | temp = (double) ((audioBuffer[2 * i] & 0xFF) | (audioBuffer[2 * i + 1] << 8)) / 32768.0F;
91 | complexSignal[i] = new Complex(temp * MAGIC_SCALE, 0d);
92 | }
93 |
94 | y = FFT.fft(complexSignal);
95 |
96 | /*
97 | * See http://developer.android.com/reference/android/media/audiofx/Visualizer.html#getFft(byte[]) for format explanation
98 | */
99 |
100 | final byte[] y_byte = new byte[y.length * 2];
101 | y_byte[0] = (byte) y[0].re();
102 | y_byte[1] = (byte) y[y.length - 1].re();
103 | for (int i = 1; i < y.length - 1; i++) {
104 | y_byte[i * 2] = (byte) y[i].re();
105 | y_byte[i * 2 + 1] = (byte) y[i].im();
106 | }
107 |
108 | if (handler != null) {
109 | handler.onFftDataCapture(y_byte);
110 | }
111 | }
112 |
113 | private FileOutputStream prepareWriting() {
114 | File file = new File(fileName_raw);
115 | if (file.exists()) {
116 | file.delete();
117 | }
118 |
119 | FileOutputStream out = null;
120 | try {
121 | out = new FileOutputStream(fileName_raw, true);
122 | } catch (FileNotFoundException e) {
123 | e.printStackTrace();
124 | if (handler != null) {
125 | handler.onRecordingError();
126 | }
127 | }
128 | return out;
129 | }
130 |
131 | private void write(FileOutputStream out) {
132 | try {
133 | out.write(audioBuffer);
134 | } catch (IOException e) {
135 | e.printStackTrace();
136 | if (handler != null) {
137 | handler.onRecordingError();
138 | }
139 | }
140 | }
141 |
142 | private void finishWriting(FileOutputStream out) {
143 | try {
144 | out.close();
145 | } catch (IOException e) {
146 | e.printStackTrace();
147 | if (handler != null) {
148 | handler.onRecordingError();
149 | }
150 | }
151 | }
152 |
153 | private String getRawName(String fileWavName) {
154 | return String.format("%s/%s", getFileDir(fileWavName), FILE_NAME);
155 | }
156 |
157 | private String getFileDir(String fileWavName) {
158 | File file = new File(fileWavName);
159 | String dir = file.getParent();
160 | return (dir == null) ? "" : dir;
161 | }
162 |
163 | private void convertRawToWav() {
164 | File file_raw = new File(fileName_raw);
165 | if (!file_raw.exists()) {
166 | return;
167 | }
168 | File file_wav = new File(fileName_wav);
169 | if (file_wav.exists()) {
170 | file_wav.delete();
171 | }
172 | try {
173 | PcmAudioHelper.convertRawToWav(WavAudioFormat.mono16Bit(SAMPLING_RATE), file_raw, file_wav);
174 | file_raw.delete();
175 | if (handler != null) {
176 | handler.onRecordSuccess();
177 | }
178 | } catch (IOException e) {
179 | e.printStackTrace();
180 | if (handler != null) {
181 | handler.onRecordSaveError();
182 | }
183 | }
184 | }
185 |
186 | public synchronized void stopRecording() {
187 | isRecording = false;
188 | }
189 | }
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/fft/Complex.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.fft;
2 |
3 | /*************************************************************************
4 | * Compilation: javac Complex.java
5 | * Execution: java Complex
6 | *
7 | * Data type for complex numbers.
8 | *
9 | * The data type is "immutable" so once you create and initialize
10 | * a Complex object, you cannot change it. The "final" keyword
11 | * when declaring re and im enforces this rule, making it a
12 | * compile-time error to change the .re or .im fields after
13 | * they've been initialized.
14 | *
15 | * % java Complex
16 | * a = 5.0 + 6.0i
17 | * b = -3.0 + 4.0i
18 | * Re(a) = 5.0
19 | * Im(a) = 6.0
20 | * b + a = 2.0 + 10.0i
21 | * a - b = 8.0 + 2.0i
22 | * a * b = -39.0 + 2.0i
23 | * b * a = -39.0 + 2.0i
24 | * a / b = 0.36 - 1.52i
25 | * (a / b) * b = 5.0 + 6.0i
26 | * conj(a) = 5.0 - 6.0i
27 | * |a| = 7.810249675906654
28 | * tan(a) = -6.685231390246571E-6 + 1.0000103108981198i
29 | *************************************************************************/
30 |
31 | public class Complex {
32 | private final double re; // the real part
33 | private final double im; // the imaginary part
34 |
35 | // create a new object with the given real and imaginary parts
36 | public Complex(double real, double imag) {
37 | re = real;
38 | im = imag;
39 | }
40 |
41 | // return a string representation of the invoking Complex object
42 | public String toString() {
43 | if (im == 0) return re + "";
44 | if (re == 0) return im + "i";
45 | if (im < 0) return re + " - " + (-im) + "i";
46 | return re + " + " + im + "i";
47 | }
48 |
49 | // return abs/modulus/magnitude and angle/phase/argument
50 | public double abs() {
51 | return Math.hypot(re, im);
52 | } // Math.sqrt(re*re + im*im)
53 |
54 | public double phase() {
55 | return Math.atan2(im, re);
56 | } // between -pi and pi
57 |
58 | // return a new Complex object whose value is (this + b)
59 | public Complex plus(Complex b) {
60 | Complex a = this; // invoking object
61 | double real = a.re + b.re;
62 | double imag = a.im + b.im;
63 | return new Complex(real, imag);
64 | }
65 |
66 | // return a new Complex object whose value is (this - b)
67 | public Complex minus(Complex b) {
68 | Complex a = this;
69 | double real = a.re - b.re;
70 | double imag = a.im - b.im;
71 | return new Complex(real, imag);
72 | }
73 |
74 | // return a new Complex object whose value is (this * b)
75 | public Complex times(Complex b) {
76 | Complex a = this;
77 | double real = a.re * b.re - a.im * b.im;
78 | double imag = a.re * b.im + a.im * b.re;
79 | return new Complex(real, imag);
80 | }
81 |
82 | // scalar multiplication
83 | // return a new object whose value is (this * alpha)
84 | public Complex times(double alpha) {
85 | return new Complex(alpha * re, alpha * im);
86 | }
87 |
88 | // return a new Complex object whose value is the conjugate of this
89 | public Complex conjugate() {
90 | return new Complex(re, -im);
91 | }
92 |
93 | // return a new Complex object whose value is the reciprocal of this
94 | public Complex reciprocal() {
95 | double scale = re * re + im * im;
96 | return new Complex(re / scale, -im / scale);
97 | }
98 |
99 | // return the real or imaginary part
100 | public double re() {
101 | return re;
102 | }
103 |
104 | public double im() {
105 | return im;
106 | }
107 |
108 | // return a / b
109 | public Complex divides(Complex b) {
110 | Complex a = this;
111 | return a.times(b.reciprocal());
112 | }
113 |
114 | // return a new Complex object whose value is the complex exponential of this
115 | public Complex exp() {
116 | return new Complex(Math.exp(re) * Math.cos(im), Math.exp(re) * Math.sin(im));
117 | }
118 |
119 | // return a new Complex object whose value is the complex sine of this
120 | public Complex sin() {
121 | return new Complex(Math.sin(re) * Math.cosh(im), Math.cos(re) * Math.sinh(im));
122 | }
123 |
124 | // return a new Complex object whose value is the complex cosine of this
125 | public Complex cos() {
126 | return new Complex(Math.cos(re) * Math.cosh(im), -Math.sin(re) * Math.sinh(im));
127 | }
128 |
129 | // return a new Complex object whose value is the complex tangent of this
130 | public Complex tan() {
131 | return sin().divides(cos());
132 | }
133 |
134 |
135 | // a static version of plus
136 | public static Complex plus(Complex a, Complex b) {
137 | double real = a.re + b.re;
138 | double imag = a.im + b.im;
139 | Complex sum = new Complex(real, imag);
140 | return sum;
141 | }
142 |
143 |
144 | // sample client for testing
145 | public static void main(String[] args) {
146 | Complex a = new Complex(5.0, 6.0);
147 | Complex b = new Complex(-3.0, 4.0);
148 |
149 | System.out.println("a = " + a);
150 | System.out.println("b = " + b);
151 | System.out.println("Re(a) = " + a.re());
152 | System.out.println("Im(a) = " + a.im());
153 | System.out.println("b + a = " + b.plus(a));
154 | System.out.println("a - b = " + a.minus(b));
155 | System.out.println("a * b = " + a.times(b));
156 | System.out.println("b * a = " + b.times(a));
157 | System.out.println("a / b = " + a.divides(b));
158 | System.out.println("(a / b) * b = " + a.divides(b).times(b));
159 | System.out.println("conj(a) = " + a.conjugate());
160 | System.out.println("|a| = " + a.abs());
161 | System.out.println("tan(a) = " + a.tan());
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/fft/FFT.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.fft;
2 |
3 | /*************************************************************************
4 | * Compilation: javac FFT.java
5 | * Execution: java FFT N
6 | * Dependencies: Complex.java
7 | *
8 | * Compute the FFT and inverse FFT of a length N complex sequence.
9 | * Bare bones implementation that runs in O(N log N) time. Our goal
10 | * is to optimize the clarity of the code, rather than performance.
11 | *
12 | * Limitations
13 | * -----------
14 | * - assumes N is a power of 2
15 | *
16 | * - not the most memory efficient algorithm (because it uses
17 | * an object type for representing complex numbers and because
18 | * it re-allocates memory for the subarray, instead of doing
19 | * in-place or reusing a single temporary array)
20 | *************************************************************************/
21 |
22 | public class FFT {
23 |
24 | // compute the FFT of x[], assuming its length is a power of 2
25 | public static Complex[] fft(Complex[] x) {
26 | int N = x.length;
27 |
28 | // base case
29 | if (N == 1) return new Complex[]{x[0]};
30 |
31 | // radix 2 Cooley-Tukey FFT
32 | if (N % 2 != 0) {
33 | throw new RuntimeException("N is not a power of 2");
34 | }
35 |
36 | // fft of even terms
37 | Complex[] even = new Complex[N / 2];
38 | for (int k = 0; k < N / 2; k++) {
39 | even[k] = x[2 * k];
40 | }
41 | Complex[] q = fft(even);
42 |
43 | // fft of odd terms
44 | Complex[] odd = even; // reuse the array
45 | for (int k = 0; k < N / 2; k++) {
46 | odd[k] = x[2 * k + 1];
47 | }
48 | Complex[] r = fft(odd);
49 |
50 | // combine
51 | Complex[] y = new Complex[N];
52 | for (int k = 0; k < N / 2; k++) {
53 | double kth = -2 * k * Math.PI / N;
54 | Complex wk = new Complex(Math.cos(kth), Math.sin(kth));
55 | y[k] = q[k].plus(wk.times(r[k]));
56 | y[k + N / 2] = q[k].minus(wk.times(r[k]));
57 | }
58 | return y;
59 | }
60 |
61 |
62 | // compute the inverse FFT of x[], assuming its length is a power of 2
63 | public static Complex[] ifft(Complex[] x) {
64 | int N = x.length;
65 | Complex[] y = new Complex[N];
66 |
67 | // take conjugate
68 | for (int i = 0; i < N; i++) {
69 | y[i] = x[i].conjugate();
70 | }
71 |
72 | // compute forward FFT
73 | y = fft(y);
74 |
75 | // take conjugate again
76 | for (int i = 0; i < N; i++) {
77 | y[i] = y[i].conjugate();
78 | }
79 |
80 | // divide by N
81 | for (int i = 0; i < N; i++) {
82 | y[i] = y[i].times(1.0 / N);
83 | }
84 |
85 | return y;
86 |
87 | }
88 |
89 | // compute the circular convolution of x and y
90 | public static Complex[] cconvolve(Complex[] x, Complex[] y) {
91 |
92 | // should probably pad x and y with 0s so that they have same length
93 | // and are powers of 2
94 | if (x.length != y.length) {
95 | throw new RuntimeException("Dimensions don't agree");
96 | }
97 |
98 | int N = x.length;
99 |
100 | // compute FFT of each sequence
101 | Complex[] a = fft(x);
102 | Complex[] b = fft(y);
103 |
104 | // point-wise multiply
105 | Complex[] c = new Complex[N];
106 | for (int i = 0; i < N; i++) {
107 | c[i] = a[i].times(b[i]);
108 | }
109 |
110 | // compute inverse FFT
111 | return ifft(c);
112 | }
113 |
114 |
115 | // compute the linear convolution of x and y
116 | public static Complex[] convolve(Complex[] x, Complex[] y) {
117 | Complex ZERO = new Complex(0, 0);
118 |
119 | Complex[] a = new Complex[2 * x.length];
120 | for (int i = 0; i < x.length; i++) a[i] = x[i];
121 | for (int i = x.length; i < 2 * x.length; i++) a[i] = ZERO;
122 |
123 | Complex[] b = new Complex[2 * y.length];
124 | for (int i = 0; i < y.length; i++) b[i] = y[i];
125 | for (int i = y.length; i < 2 * y.length; i++) b[i] = ZERO;
126 |
127 | return cconvolve(a, b);
128 | }
129 |
130 | // display an array of Complex numbers to standard output
131 | public static void show(Complex[] x, String title) {
132 | System.out.println(title);
133 | System.out.println("-------------------");
134 | for (int i = 0; i < x.length; i++) {
135 | System.out.println(x[i]);
136 | }
137 | System.out.println();
138 | }
139 |
140 |
141 | /*********************************************************************
142 | * Test client and sample execution
143 | *
144 | * % java FFT 4
145 | * x
146 | * -------------------
147 | * -0.03480425839330703
148 | * 0.07910192950176387
149 | * 0.7233322451735928
150 | * 0.1659819820667019
151 | *
152 | * y = fft(x)
153 | * -------------------
154 | * 0.9336118983487516
155 | * -0.7581365035668999 + 0.08688005256493803i
156 | * 0.44344407521182005
157 | * -0.7581365035668999 - 0.08688005256493803i
158 | *
159 | * z = ifft(y)
160 | * -------------------
161 | * -0.03480425839330703
162 | * 0.07910192950176387 + 2.6599344570851287E-18i
163 | * 0.7233322451735928
164 | * 0.1659819820667019 - 2.6599344570851287E-18i
165 | *
166 | * c = cconvolve(x, x)
167 | * -------------------
168 | * 0.5506798633981853
169 | * 0.23461407150576394 - 4.033186818023279E-18i
170 | * -0.016542951108772352
171 | * 0.10288019294318276 + 4.033186818023279E-18i
172 | *
173 | * d = convolve(x, x)
174 | * -------------------
175 | * 0.001211336402308083 - 3.122502256758253E-17i
176 | * -0.005506167987577068 - 5.058885073636224E-17i
177 | * -0.044092969479563274 + 2.1934338938072244E-18i
178 | * 0.10288019294318276 - 3.6147323062478115E-17i
179 | * 0.5494685269958772 + 3.122502256758253E-17i
180 | * 0.240120239493341 + 4.655566391833896E-17i
181 | * 0.02755001837079092 - 2.1934338938072244E-18i
182 | * 4.01805098805014E-17i
183 | *********************************************************************/
184 |
185 | public static void main(String[] args) {
186 | int N = Integer.parseInt(args[0]);
187 | Complex[] x = new Complex[N];
188 |
189 | // original data
190 | for (int i = 0; i < N; i++) {
191 | x[i] = new Complex(i, 0);
192 | x[i] = new Complex(-2 * Math.random() + 1, 0);
193 | }
194 | show(x, "x");
195 |
196 | // FFT of original data
197 | Complex[] y = fft(x);
198 | show(y, "y = fft(x)");
199 |
200 | // take inverse FFT
201 | Complex[] z = ifft(y);
202 | show(z, "z = ifft(y)");
203 |
204 | // circular convolution of x with itself
205 | Complex[] c = cconvolve(x, x);
206 | show(c, "c = cconvolve(x, x)");
207 |
208 | // linear convolution of x with itself
209 | Complex[] d = convolve(x, x);
210 | show(d, "d = convolve(x, x)");
211 | }
212 |
213 | }
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/AdaptiveSurfaceView.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.content.Context;
4 | import android.hardware.Camera;
5 | import android.util.AttributeSet;
6 | import android.view.SurfaceView;
7 |
8 | /*
9 | * Represents a surface camera preview or media player output is drawn on.
10 | * Surface can adjust its size according to set preview's aspect ratio.
11 | */
12 | public class AdaptiveSurfaceView extends SurfaceView {
13 | private int previewWidth;
14 | private int previewHeight;
15 | private float ratio;
16 |
17 | public AdaptiveSurfaceView(Context context) {
18 | super(context);
19 | }
20 |
21 | public AdaptiveSurfaceView(Context context, AttributeSet attrs) {
22 | super(context, attrs);
23 | }
24 |
25 | public AdaptiveSurfaceView(Context context, AttributeSet attrs, int defStyle) {
26 | super(context, attrs, defStyle);
27 | }
28 |
29 | public void setPreviewSize(Camera.Size size) {
30 | int screenW = getResources().getDisplayMetrics().widthPixels;
31 | int screenH = getResources().getDisplayMetrics().heightPixels;
32 | if (screenW < screenH) {
33 | previewWidth = size.width < size.height ? size.width : size.height;
34 | previewHeight = size.width >= size.height ? size.width : size.height;
35 | } else {
36 | previewWidth = size.width > size.height ? size.width : size.height;
37 | previewHeight = size.width <= size.height ? size.width : size.height;
38 | }
39 | ratio = previewHeight / (float) previewWidth;
40 | requestLayout();
41 | }
42 |
43 | @Override
44 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
45 | int previewW = MeasureSpec.getSize(widthMeasureSpec);
46 | int previewWMode = MeasureSpec.getMode(widthMeasureSpec);
47 | int previewH = MeasureSpec.getSize(heightMeasureSpec);
48 | int previewHMode = MeasureSpec.getMode(heightMeasureSpec);
49 |
50 | int measuredWidth = 0;
51 | int measuredHeight = 0;
52 |
53 | if (previewWidth > 0 && previewHeight > 0) {
54 | measuredWidth = defineWidth(previewW, previewWMode);
55 |
56 | measuredHeight = (int) (measuredWidth * ratio);
57 | if (previewHMode != MeasureSpec.UNSPECIFIED && measuredHeight > previewH) {
58 | measuredWidth = (int) (previewH / ratio);
59 | measuredHeight = previewH;
60 | }
61 |
62 | setMeasuredDimension(measuredWidth, measuredHeight);
63 | } else {
64 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
65 | }
66 | }
67 |
68 | private int defineWidth(int previewW, int previewWMode) {
69 | int measuredWidth;
70 | if (previewWMode == MeasureSpec.UNSPECIFIED) {
71 | measuredWidth = previewWidth;
72 | } else if (previewWMode == MeasureSpec.EXACTLY) {
73 | measuredWidth = previewW;
74 | } else {
75 | measuredWidth = Math.min(previewW, previewWidth);
76 | }
77 | return measuredWidth;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/CameraHelper.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.hardware.Camera;
5 | import android.hardware.Camera.CameraInfo;
6 | import android.hardware.Camera.Size;
7 | import android.os.Build;
8 | import android.view.Surface;
9 |
10 | import java.util.List;
11 |
12 | /*
13 | * Represents camera management helper class.
14 | * Holds method for setting camera display orientation.
15 | */
16 | public class CameraHelper {
17 |
18 | public static int getAvailableCamerasCount() {
19 | return Camera.getNumberOfCameras();
20 | }
21 |
22 | public static int getDefaultCameraID() {
23 | int camerasCnt = getAvailableCamerasCount();
24 | int defaultCameraID = 0;
25 | CameraInfo cameraInfo = new CameraInfo();
26 | for (int i = 0; i < camerasCnt; i++) {
27 | Camera.getCameraInfo(i, cameraInfo);
28 | if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
29 | defaultCameraID = i;
30 | }
31 | }
32 | return defaultCameraID;
33 | }
34 |
35 | public static boolean isCameraFacingBack(int cameraID) {
36 | CameraInfo cameraInfo = new CameraInfo();
37 | Camera.getCameraInfo(cameraID, cameraInfo);
38 | return (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK);
39 | }
40 |
41 | @SuppressLint("NewApi")
42 | public static List getCameraSupportedVideoSizes(Camera camera) {
43 | if ((Build.VERSION.SDK_INT >= 11) && (camera != null)) {
44 | return camera.getParameters().getSupportedVideoSizes();
45 | } else {
46 | return null;
47 | }
48 | }
49 |
50 | public static int setCameraDisplayOrientation(int cameraId, Camera camera, int displayRotation) {
51 | CameraInfo info = new CameraInfo();
52 | Camera.getCameraInfo(cameraId, info);
53 | int degrees = 0;
54 | switch (displayRotation) {
55 | case Surface.ROTATION_0:
56 | degrees = 0;
57 | break;
58 | case Surface.ROTATION_90:
59 | degrees = 90;
60 | break;
61 | case Surface.ROTATION_180:
62 | degrees = 180;
63 | break;
64 | case Surface.ROTATION_270:
65 | degrees = 270;
66 | break;
67 | }
68 |
69 | int camRotationDegree = 0;
70 | if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
71 | camRotationDegree = (info.orientation + degrees) % 360;
72 | camRotationDegree = (360 - camRotationDegree) % 360; // compensate the mirror
73 | } else {
74 | camRotationDegree = (info.orientation - degrees + 360) % 360;
75 | }
76 |
77 | if (camera != null) {
78 | camera.setDisplayOrientation(camRotationDegree);
79 | }
80 | return camRotationDegree;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/CameraManager.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.hardware.Camera;
4 | import android.hardware.Camera.Parameters;
5 | import android.hardware.Camera.Size;
6 | import android.view.SurfaceHolder;
7 |
8 | import java.io.IOException;
9 |
10 | /*
11 | * Manages camera preview
12 | */
13 | public class CameraManager {
14 |
15 | private Camera camera;
16 | private int camerasCount;
17 | private int defaultCameraID;
18 | private int cameraRotationDegree;
19 | private boolean isPreviewStarted = false;
20 |
21 | public CameraManager() {
22 | camerasCount = CameraHelper.getAvailableCamerasCount();
23 | defaultCameraID = CameraHelper.getDefaultCameraID();
24 | }
25 |
26 | public void openCamera() {
27 | if (camera != null) {
28 | releaseCamera();
29 | }
30 | camera = Camera.open(defaultCameraID);
31 | }
32 |
33 | public void releaseCamera() {
34 | if (camera != null) {
35 | camera.release();
36 | camera = null;
37 | }
38 | }
39 |
40 | public void switchCamera() {
41 | stopCameraPreview();
42 |
43 | defaultCameraID = (defaultCameraID + 1) % camerasCount;
44 | openCamera();
45 | }
46 |
47 | public void setupCameraAndStartPreview(SurfaceHolder sf, Size sz, int displayRotation) {
48 | stopCameraPreview();
49 |
50 | cameraRotationDegree = CameraHelper.setCameraDisplayOrientation(defaultCameraID, camera, displayRotation);
51 |
52 | Parameters param = camera.getParameters();
53 | param.setPreviewSize(sz.width, sz.height);
54 | camera.setParameters(param);
55 |
56 | if (setDisplay(sf)) {
57 | startCameraPreview();
58 | }
59 | }
60 |
61 | public boolean setDisplay(SurfaceHolder sf) {
62 | try {
63 | camera.setPreviewDisplay(sf);
64 | return true;
65 | } catch (IOException e) {
66 | e.printStackTrace();
67 | }
68 | return false;
69 | }
70 |
71 | public void startCameraPreview() {
72 | camera.startPreview();
73 | isPreviewStarted = true;
74 | }
75 |
76 | public void stopCameraPreview() {
77 | if (isPreviewStarted && (camera != null)) {
78 | isPreviewStarted = false;
79 | camera.stopPreview();
80 | }
81 | }
82 |
83 | public Camera getCamera() {
84 | return camera;
85 | }
86 |
87 | public int getCameraDisplayOrientation() {
88 | return (CameraHelper.isCameraFacingBack(defaultCameraID)) ? cameraRotationDegree : cameraRotationDegree + 180;
89 | }
90 |
91 | public boolean hasMultipleCameras() {
92 | return (camerasCount > 1);
93 | }
94 |
95 | public boolean isPreviewStarted() {
96 | return isPreviewStarted;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/MediaPlayerManager.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.media.MediaPlayer;
4 | import android.view.SurfaceHolder;
5 |
6 | /*
7 | * Manages media player playback
8 | */
9 | public class MediaPlayerManager {
10 |
11 | private MediaPlayer player;
12 |
13 | public MediaPlayerManager() {
14 | player = new MediaPlayer();
15 | }
16 |
17 | public MediaPlayer getPlayer() {
18 | return player;
19 | }
20 |
21 | public void setupPlayback(String fileName) {
22 | try {
23 | player.setDataSource(fileName);
24 | player.prepare();
25 | } catch (Exception e) {
26 | e.printStackTrace();
27 | }
28 | }
29 |
30 | public void setDisplay(SurfaceHolder sf) {
31 | player.setDisplay(sf);
32 | }
33 |
34 | public void startPlaying() {
35 | player.start();
36 | }
37 |
38 | public void pausePlaying() {
39 | player.pause();
40 | }
41 |
42 | public void seekTo(int pos) {
43 | if (pos < 0) {
44 | player.seekTo(0);
45 | } else if (pos > getDuration()) {
46 | player.seekTo(getDuration());
47 | } else {
48 | player.seekTo(pos);
49 | }
50 | }
51 |
52 | public void stopPlaying() {
53 | if (player.isPlaying()) {
54 | player.stop();
55 | }
56 | }
57 |
58 | public boolean isPlaying() {
59 | return player.isPlaying();
60 | }
61 |
62 | public int getCurrentPosition() {
63 | return player.getCurrentPosition();
64 | }
65 |
66 | public int getDuration() {
67 | return player.getDuration();
68 | }
69 |
70 | public void releasePlayer() {
71 | setDisplay(null);
72 | player.release();
73 | player = null;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/MediaRecorderManager.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.hardware.Camera;
4 | import android.hardware.Camera.Size;
5 | import android.media.MediaRecorder;
6 |
7 | import java.io.IOException;
8 |
9 | /*
10 | * Manages media recorder recording
11 | */
12 | public class MediaRecorderManager {
13 | private static final int VIDEO_W_DEFAULT = 800;
14 | private static final int VIDEO_H_DEFAULT = 480;
15 |
16 | private MediaRecorder recorder;
17 | private boolean isRecording;
18 |
19 | public MediaRecorderManager() {
20 | recorder = new MediaRecorder();
21 | }
22 |
23 | public boolean startRecording(Camera camera, String fileName, Size sz, int cameraRotationDegree) {
24 | if (sz == null) {
25 | sz = camera.new Size(VIDEO_W_DEFAULT, VIDEO_H_DEFAULT);
26 | }
27 |
28 | try {
29 | camera.unlock();
30 | recorder.setCamera(camera);
31 | recorder.setOrientationHint(cameraRotationDegree);
32 | recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
33 | recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
34 | recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
35 | recorder.setVideoSize(sz.width, sz.height);
36 | recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
37 | recorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
38 | recorder.setOutputFile(fileName);
39 | recorder.prepare();
40 | recorder.start();
41 | isRecording = true;
42 | } catch (IllegalStateException e) {
43 | e.printStackTrace();
44 | } catch (IOException e) {
45 | e.printStackTrace();
46 | }
47 |
48 | return isRecording;
49 | }
50 |
51 | public boolean stopRecording() {
52 | if (isRecording) {
53 | isRecording = false;
54 | recorder.stop();
55 | recorder.reset();
56 | return true;
57 | }
58 | return false;
59 | }
60 |
61 | public void releaseRecorder() {
62 | recorder.release();
63 | recorder = null;
64 | }
65 |
66 | public boolean isRecording() {
67 | return isRecording;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/PlaybackHandler.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | public interface PlaybackHandler {
4 | public void onPreparePlayback();
5 | }
6 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/VideoPlaybackManager.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.content.Context;
4 | import android.media.MediaPlayer;
5 | import android.media.MediaPlayer.OnCompletionListener;
6 | import android.media.MediaPlayer.OnPreparedListener;
7 | import android.view.SurfaceHolder;
8 | import android.widget.MediaController;
9 | import android.widget.MediaController.MediaPlayerControl;
10 |
11 | /*
12 | * Controls video playback
13 | */
14 | public class VideoPlaybackManager implements SurfaceHolder.Callback, OnPreparedListener, MediaPlayerControl, OnCompletionListener {
15 | private MediaPlayerManager playerManager;
16 | private MediaController controller;
17 | private PlaybackHandler playbackHandler;
18 | private boolean isPlayerPrepared, isSurfaceCreated;
19 |
20 | public VideoPlaybackManager(Context ctx, AdaptiveSurfaceView videoView, PlaybackHandler playbackHandler) {
21 | videoView.getHolder().addCallback(this);
22 |
23 | this.playerManager = new MediaPlayerManager();
24 | this.playerManager.getPlayer().setOnPreparedListener(this);
25 | this.playerManager.getPlayer().setOnCompletionListener(this);
26 |
27 | this.controller = new MediaController(ctx);
28 | this.controller.setMediaPlayer(this);
29 | this.controller.setAnchorView(videoView);
30 |
31 | this.playbackHandler = playbackHandler;
32 | }
33 |
34 | public void setupPlayback(String fileName) {
35 | playerManager.setupPlayback(fileName);
36 | }
37 |
38 | public void showMediaController() {
39 | if (!controller.isEnabled()) {
40 | controller.setEnabled(true);
41 | }
42 | controller.show();
43 | }
44 |
45 | public void hideMediaController() {
46 | controller.hide();
47 | controller.setEnabled(false);
48 | }
49 |
50 | public MediaPlayerManager getPlayerManager() {
51 | return playerManager;
52 | }
53 |
54 | public void dispose() {
55 | playerManager.releasePlayer();
56 | controller = null;
57 | playbackHandler = null;
58 | }
59 |
60 | //surface holder callbacks ******************************************************************
61 |
62 | @Override
63 | public void surfaceCreated(SurfaceHolder holder) {
64 | isSurfaceCreated = true;
65 | playerManager.setDisplay(holder);
66 | if (isPlayerPrepared && isSurfaceCreated) {
67 | playbackHandler.onPreparePlayback();
68 | }
69 | }
70 |
71 | @Override
72 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
73 | }
74 |
75 | @Override
76 | public void surfaceDestroyed(SurfaceHolder holder) {
77 | playerManager.setDisplay(null);
78 | }
79 |
80 | //media player and controller callbacks *****************************************************
81 |
82 | public void onPrepared(MediaPlayer mp) {
83 | isPlayerPrepared = true;
84 | if (isPlayerPrepared && isSurfaceCreated) {
85 | playbackHandler.onPreparePlayback();
86 | }
87 | }
88 |
89 | @Override
90 | public void start() {
91 | playerManager.startPlaying();
92 | }
93 |
94 | @Override
95 | public void pause() {
96 | playerManager.pausePlaying();
97 | }
98 |
99 | @Override
100 | public void seekTo(int arg0) {
101 | playerManager.seekTo(arg0);
102 | }
103 |
104 | @Override
105 | public void onCompletion(MediaPlayer mp) {
106 | playerManager.seekTo(0);
107 | }
108 |
109 | @Override
110 | public boolean isPlaying() {
111 | return playerManager.isPlaying();
112 | }
113 |
114 | @Override
115 | public int getCurrentPosition() {
116 | return playerManager.getCurrentPosition();
117 | }
118 |
119 | @Override
120 | public int getDuration() {
121 | return playerManager.getDuration();
122 | }
123 |
124 | @Override
125 | public boolean canPause() {
126 | return true;
127 | }
128 |
129 | @Override
130 | public boolean canSeekBackward() {
131 | return true;
132 | }
133 |
134 | @Override
135 | public boolean canSeekForward() {
136 | return true;
137 | }
138 |
139 | @Override
140 | public int getAudioSessionId() {
141 | return 0;
142 | }
143 |
144 | @Override
145 | public int getBufferPercentage() {
146 | return 0;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/VideoRecordingHandler.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.hardware.Camera.Size;
4 |
5 | public interface VideoRecordingHandler {
6 | public boolean onPrepareRecording();
7 |
8 | public Size getVideoSize();
9 |
10 | public int getDisplayRotation();
11 | }
12 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/video/VideoRecordingManager.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.video;
2 |
3 | import android.hardware.Camera.Size;
4 | import android.view.SurfaceHolder;
5 |
6 | /*
7 | * Controls process of previewing and recording video
8 | */
9 | public class VideoRecordingManager implements SurfaceHolder.Callback {
10 |
11 | private AdaptiveSurfaceView videoView;
12 | private CameraManager cameraManager;
13 | private MediaRecorderManager recorderManager;
14 | private VideoRecordingHandler recordingHandler;
15 |
16 | public VideoRecordingManager(AdaptiveSurfaceView videoView, VideoRecordingHandler recordingHandler) {
17 | this.videoView = videoView;
18 | this.videoView.getHolder().addCallback(this);
19 | this.cameraManager = new CameraManager();
20 | this.recorderManager = new MediaRecorderManager();
21 | this.recordingHandler = recordingHandler;
22 | }
23 |
24 | public boolean startRecording(String fileName, Size videoSize) {
25 | int degree = cameraManager.getCameraDisplayOrientation();
26 | return recorderManager.startRecording(cameraManager.getCamera(), fileName, videoSize, degree);
27 | }
28 |
29 | public boolean stopRecording() {
30 | return recorderManager.stopRecording();
31 | }
32 |
33 | public void setPreviewSize(Size videoSize) {
34 | videoView.setPreviewSize(videoSize);
35 | }
36 |
37 | public SurfaceHolder getDisplay() {
38 | return videoView.getHolder();
39 | }
40 |
41 | public CameraManager getCameraManager() {
42 | return cameraManager;
43 | }
44 |
45 | public void dispose() {
46 | videoView = null;
47 | cameraManager.releaseCamera();
48 | recorderManager.releaseRecorder();
49 | recordingHandler = null;
50 | }
51 |
52 | //surface holder callbacks ******************************************************************
53 |
54 | @Override
55 | public void surfaceCreated(SurfaceHolder holder) {
56 | cameraManager.openCamera();
57 | }
58 |
59 | @Override
60 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
61 | if (recordingHandler == null) {
62 | return;
63 | }
64 | if (!recordingHandler.onPrepareRecording()) {
65 | cameraManager.setupCameraAndStartPreview(videoView.getHolder(),
66 | recordingHandler.getVideoSize(),
67 | recordingHandler.getDisplayRotation());
68 | }
69 | }
70 |
71 | @Override
72 | public void surfaceDestroyed(SurfaceHolder holder) {
73 | recorderManager.stopRecording();
74 | cameraManager.releaseCamera();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/visualizer/AudioData.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.visualizer;
2 |
3 | // Data class to explicitly indicate that these bytes are raw audio data
4 | public class AudioData {
5 | public AudioData() {
6 | }
7 |
8 | private byte[] bytes;
9 |
10 | public byte[] getBytes() {
11 | return bytes;
12 | }
13 |
14 | public void setBytes(byte[] bytes) {
15 | this.bytes = bytes;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/visualizer/FFTData.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.visualizer;
2 |
3 | // Data class to explicitly indicate that these bytes are the FFT of audio data
4 | public class FFTData {
5 | public FFTData() {
6 | }
7 |
8 | private byte[] bytes;
9 |
10 | public byte[] getBytes() {
11 | return bytes;
12 | }
13 |
14 | public void setBytes(byte[] bytes) {
15 | this.bytes = bytes;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/visualizer/renderer/BarGraphRenderer.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.visualizer.renderer;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 | import android.graphics.Rect;
6 |
7 | import com.hwangjr.recordhelper.visualizer.AudioData;
8 | import com.hwangjr.recordhelper.visualizer.FFTData;
9 |
10 | public class BarGraphRenderer extends Renderer {
11 | private int mDivisions;
12 | private Paint mPaint;
13 | private boolean mTop;
14 |
15 | /**
16 | * Renders the FFT data as a series of lines, in histogram form
17 | *
18 | * @param divisions - must be a power of 2. Controls how many lines to draw
19 | * @param paint - Paint to draw lines with
20 | * @param top - whether to draw the lines at the top of the canvas, or the bottom
21 | */
22 | public BarGraphRenderer(int divisions,
23 | Paint paint,
24 | boolean top) {
25 | super();
26 | mDivisions = divisions;
27 | mPaint = paint;
28 | mTop = top;
29 | }
30 |
31 | @Override
32 | public void onRender(Canvas canvas, AudioData data, Rect rect) {
33 | // Do nothing, we only display FFT data
34 | }
35 |
36 | @Override
37 | public void onRender(Canvas canvas, FFTData data, Rect rect) {
38 | for (int i = 0; i < data.getBytes().length / mDivisions; i++) {
39 | mFFTPoints[i * 4] = i * 4 * mDivisions;
40 | mFFTPoints[i * 4 + 2] = i * 4 * mDivisions;
41 | byte rfk = data.getBytes()[mDivisions * i];
42 | byte ifk = data.getBytes()[mDivisions * i + 1];
43 | float magnitude = (rfk * rfk + ifk * ifk);
44 | int dbValue = (int) (10 * Math.log10(magnitude));
45 |
46 | if (mTop) {
47 | mFFTPoints[i * 4 + 1] = 0;
48 | mFFTPoints[i * 4 + 3] = (dbValue * 2 - 10);
49 | } else {
50 | mFFTPoints[i * 4 + 1] = rect.height();
51 | mFFTPoints[i * 4 + 3] = rect.height() - (dbValue * 2 - 10);
52 | }
53 | }
54 |
55 | canvas.drawLines(mFFTPoints, mPaint);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/recordhelper/src/main/java/com/hwangjr/recordhelper/visualizer/renderer/Renderer.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhelper.visualizer.renderer;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 |
6 | import com.hwangjr.recordhelper.visualizer.AudioData;
7 | import com.hwangjr.recordhelper.visualizer.FFTData;
8 |
9 | abstract public class Renderer {
10 | // Have these as members, so we don't have to re-create them each time
11 | protected float[] mPoints;
12 | protected float[] mFFTPoints;
13 |
14 | public Renderer() {
15 | }
16 |
17 | // As the display of raw/FFT audio will usually look different, subclasses
18 | // will typically only implement one of the below methods
19 |
20 | /**
21 | * Implement this method to render the audio data onto the canvas
22 | *
23 | * @param canvas - Canvas to draw on
24 | * @param data - Data to render
25 | * @param rect - Rect to render into
26 | */
27 | abstract public void onRender(Canvas canvas, AudioData data, Rect rect);
28 |
29 | /**
30 | * Implement this method to render the FFT audio data onto the canvas
31 | *
32 | * @param canvas - Canvas to draw on
33 | * @param data - Data to render
34 | * @param rect - Rect to render into
35 | */
36 | abstract public void onRender(Canvas canvas, FFTData data, Rect rect);
37 |
38 |
39 | // These methods should actually be called for rendering
40 |
41 | /**
42 | * Render the audio data onto the canvas
43 | *
44 | * @param canvas - Canvas to draw on
45 | * @param data - Data to render
46 | * @param rect - Rect to render into
47 | */
48 | final public void render(Canvas canvas, AudioData data, Rect rect) {
49 | if (mPoints == null || mPoints.length < data.getBytes().length * 4) {
50 | mPoints = new float[data.getBytes().length * 4];
51 | }
52 |
53 | onRender(canvas, data, rect);
54 | }
55 |
56 | /**
57 | * Render the FFT data onto the canvas
58 | *
59 | * @param canvas - Canvas to draw on
60 | * @param data - Data to render
61 | * @param rect - Rect to render into
62 | */
63 | final public void render(Canvas canvas, FFTData data, Rect rect) {
64 | if (mFFTPoints == null || mFFTPoints.length < data.getBytes().length * 4) {
65 | mFFTPoints = new float[data.getBytes().length * 4];
66 | }
67 |
68 | onRender(canvas, data, rect);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/recordhelper/src/test/java/com/hwangjr/recordhepler/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.recordhepler;
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 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':recordhelper', ':soundhelper', ':jhelper'
2 |
--------------------------------------------------------------------------------
/soundhelper/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/soundhelper/README.md:
--------------------------------------------------------------------------------
1 | SoundHelper
2 | ------
3 | Clone from [simplesound](https://code.google.com/p/simplesound/source/browse/).
4 |
5 | # Introduction
6 | Simple raw-wav audio I/O and processing helper tool for java.
--------------------------------------------------------------------------------
/soundhelper/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 |
3 | dependencies {
4 | compile fileTree(dir: 'libs', include: ['*.jar'])
5 | compile project(':jhelper')
6 |
7 | // test
8 | testCompile 'junit:junit:4.12'
9 | }
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/Complex.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | public final class Complex {
4 |
5 | public final double real;
6 | public final double imaginary;
7 |
8 | public Complex(double real, double imaginary) {
9 | this.real = real;
10 | this.imaginary = imaginary;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/DoubleVector.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | import java.util.Arrays;
4 |
5 | /**
6 | * a vector containing a double numbers.
7 | */
8 | public class DoubleVector {
9 |
10 | final double[] data;
11 |
12 | public DoubleVector(double[] data) {
13 | if (data == null)
14 | throw new IllegalArgumentException("Data cannot be null!");
15 | this.data = data;
16 | }
17 |
18 | public int size() {
19 | return data.length;
20 | }
21 |
22 | public double[] getData() {
23 | return data;
24 | }
25 |
26 |
27 | @Override
28 | public String toString() {
29 | return Arrays.toString(data);
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/DoubleVectorFrameSource.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | import com.hwangjr.soundhelper.pcm.PcmMonoInputStream;
4 |
5 | import java.util.Iterator;
6 |
7 | public class DoubleVectorFrameSource {
8 |
9 | private final PcmMonoInputStream pmis;
10 | private final int frameSize;
11 | private final int shiftAmount;
12 | private final boolean paddingApplied;
13 |
14 | private DoubleVectorFrameSource(PcmMonoInputStream pmis, int frameSize, int shiftAmount, boolean paddingApplied) {
15 | this.pmis = pmis;
16 | this.frameSize = frameSize;
17 | this.shiftAmount = shiftAmount;
18 | this.paddingApplied = paddingApplied;
19 | }
20 |
21 | public static DoubleVectorFrameSource fromSampleAmount(
22 | PcmMonoInputStream pmis, int frameSize, int shiftAmount) {
23 | return new DoubleVectorFrameSource(pmis, frameSize, shiftAmount, false);
24 | }
25 |
26 | public static DoubleVectorFrameSource fromSampleAmountWithPadding(
27 | PcmMonoInputStream pmis, int frameSize, int shiftAmount) {
28 | return new DoubleVectorFrameSource(pmis, frameSize, shiftAmount, true);
29 | }
30 |
31 | public static DoubleVectorFrameSource fromSizeInMiliseconds(
32 | PcmMonoInputStream pmis, double frameSizeInMilis, double shiftAmountInMilis) {
33 | return new DoubleVectorFrameSource(pmis,
34 | pmis.getFormat().sampleCountForMiliseconds(frameSizeInMilis),
35 | pmis.getFormat().sampleCountForMiliseconds(shiftAmountInMilis),
36 | false);
37 | }
38 |
39 | public static DoubleVectorFrameSource fromSizeInMilisecondsWithPadding(
40 | PcmMonoInputStream pmis, double frameSizeInMilis, double shiftAmountInMilis) {
41 | return new DoubleVectorFrameSource(pmis,
42 | pmis.getFormat().sampleCountForMiliseconds(frameSizeInMilis),
43 | pmis.getFormat().sampleCountForMiliseconds(shiftAmountInMilis),
44 | true);
45 | }
46 |
47 | public Iterable getIterableFrameReader() {
48 | return new Iterable() {
49 | public Iterator iterator() {
50 | return new NormalizedFrameIterator(pmis, frameSize, shiftAmount, paddingApplied);
51 | }
52 | };
53 | }
54 |
55 | public Iterator getNormalizedFrameIterator() {
56 | return new NormalizedFrameIterator(pmis, frameSize, shiftAmount, paddingApplied);
57 | }
58 |
59 | public PcmMonoInputStream getPmis() {
60 | return pmis;
61 | }
62 |
63 | public int getFrameSize() {
64 | return frameSize;
65 | }
66 |
67 | public int getShiftAmount() {
68 | return shiftAmount;
69 | }
70 |
71 | public boolean isPaddingApplied() {
72 | return paddingApplied;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/DoubleVectorProcessingPipeline.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | import java.util.Iterator;
4 | import java.util.List;
5 |
6 | public class DoubleVectorProcessingPipeline {
7 |
8 | List processors;
9 | Iterator vectorSource;
10 |
11 | public DoubleVectorProcessingPipeline(Iterator vectorSource,
12 | List processors) {
13 | this.vectorSource = vectorSource;
14 | this.processors = processors;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/DoubleVectorProcessor.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | public interface DoubleVectorProcessor {
4 |
5 | DoubleVector process(DoubleVector input);
6 |
7 | void processInPlace(DoubleVector input);
8 | }
9 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/MutableComplex.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | public class MutableComplex {
4 | public double real;
5 | public double imaginary;
6 |
7 | public MutableComplex(double real, double imaginary) {
8 | this.real = real;
9 | this.imaginary = imaginary;
10 | }
11 |
12 | public MutableComplex(Complex complex) {
13 | this.real = complex.real;
14 | this.imaginary = complex.imaginary;
15 | }
16 |
17 | public Complex getImmutableComplex() {
18 | return new Complex(real, imaginary);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/NormalizedFrameIterator.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | import com.hwangjr.soundhelper.pcm.PcmMonoInputStream;
4 |
5 | import java.io.IOException;
6 | import java.util.Iterator;
7 |
8 | public class NormalizedFrameIterator implements Iterator {
9 |
10 | private final PcmMonoInputStream pmis;
11 | private final int frameSize;
12 | private final int shiftAmount;
13 | //TODO: not applied yet
14 | private final boolean applyPadding;
15 |
16 | public NormalizedFrameIterator(PcmMonoInputStream pmis, int frameSize, int shiftAmount, boolean applyPadding) {
17 | if (frameSize < 1)
18 | throw new IllegalArgumentException("Frame size must be larger than zero.");
19 | if (shiftAmount < 1)
20 | throw new IllegalArgumentException("Shift size must be larger than zero.");
21 | this.pmis = pmis;
22 | this.frameSize = frameSize;
23 | this.shiftAmount = shiftAmount;
24 | this.applyPadding = applyPadding;
25 | }
26 |
27 | public NormalizedFrameIterator(PcmMonoInputStream pmis, int frameSize, boolean applyPadding) {
28 | this(pmis, frameSize, frameSize, applyPadding);
29 | }
30 |
31 | public NormalizedFrameIterator(PcmMonoInputStream pmis, int frameSize) {
32 | this(pmis, frameSize, frameSize, false);
33 | }
34 |
35 | private DoubleVector currentFrame;
36 | private int frameCounter;
37 |
38 | public boolean hasNext() {
39 | double[] data;
40 | try {
41 | if (frameCounter == 0) {
42 | data = pmis.readSamplesNormalized(frameSize);
43 | if (data.length < frameSize)
44 | return false;
45 | currentFrame = new DoubleVector(data);
46 | } else {
47 | data = pmis.readSamplesNormalized(shiftAmount);
48 | if (data.length < shiftAmount)
49 | return false;
50 | double[] frameData = currentFrame.data.clone();
51 | System.arraycopy(data, 0, frameData, frameData.length - shiftAmount, shiftAmount);
52 | currentFrame = new DoubleVector(frameData);
53 | }
54 | } catch (IOException e) {
55 | return false;
56 | }
57 | frameCounter++;
58 | return true;
59 | }
60 |
61 | public DoubleVector next() {
62 | return currentFrame;
63 | }
64 |
65 | public void remove() {
66 | throw new UnsupportedOperationException("Remove is not supported.");
67 | }
68 |
69 | public int getFrameSize() {
70 | return frameSize;
71 | }
72 |
73 | public int getShiftAmount() {
74 | return shiftAmount;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/dsp/WindowerFactory.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.dsp;
2 |
3 | import com.hwangjr.jhelper.Doubles;
4 |
5 | import static java.lang.Math.PI;
6 | import static java.lang.Math.cos;
7 |
8 | public class WindowerFactory {
9 |
10 | private static class RaisedCosineWindower implements DoubleVectorProcessor {
11 | double alpha;
12 | double cosineWindow[];
13 |
14 | RaisedCosineWindower(double alpha, int length) {
15 | if (length <= 0)
16 | throw new IllegalArgumentException("Window length cannot be smaller than 1");
17 | this.alpha = alpha;
18 | cosineWindow = new double[length];
19 | for (int i = 0; i < length; i++) {
20 | cosineWindow[i] = (1 - alpha) - alpha * cos(2 * PI * i / ((double) length - 1.0));
21 | }
22 | }
23 |
24 | public DoubleVector process(DoubleVector input) {
25 | return new DoubleVector(Doubles.multiply(input.data, cosineWindow));
26 | }
27 |
28 | public void processInPlace(DoubleVector input) {
29 | Doubles.multiplyInPlace(input.data, cosineWindow);
30 | }
31 | }
32 |
33 | public static DoubleVectorProcessor newHammingWindower(int length) {
34 | return new RaisedCosineWindower(0.46d, length);
35 | }
36 |
37 | public static DoubleVectorProcessor newHanningWindower(int length) {
38 | return new RaisedCosineWindower(0.5d, length);
39 | }
40 |
41 | public static DoubleVectorProcessor newTriangularWindower(int length) {
42 | return new RaisedCosineWindower(0.0d, length);
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/JavaAudioFormatBuilder.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import javax.sound.sampled.AudioFormat;
4 | import javax.sound.sampled.AudioSystem;
5 |
6 | /**
7 | * this is a builder class for easily generating javax.sound.sampled.AudioFormat object.
8 | */
9 | public class JavaAudioFormatBuilder {
10 |
11 | private enum Channel {
12 | MONO(1), STEREO(2), NOT_SPECIFIED(0);
13 |
14 | public int audioChannels;
15 |
16 | Channel(int i) {
17 | this.audioChannels = i;
18 | }
19 | }
20 |
21 | public static final AudioFormat PCM_SIGNED_8_KHZ_16_BIT_MONO_BIG_ENDIAN =
22 | new JavaAudioFormatBuilder().pcmSigned().sampleSizeInBits(16).sampleRate(8000.0f).mono().bigEndian().build();
23 |
24 | public static final AudioFormat PCM_SIGNED_8_KHZ_16_BIT_MONO_LITTLE_ENDIAN =
25 | new JavaAudioFormatBuilder().pcmSigned().sampleSizeInBits(16).sampleRate(8000.0f).mono().littleEndian().build();
26 |
27 | public static final AudioFormat PCM_SIGNED_44_KHZ_16_BIT_STEREO_LITTLE_ENDIAN =
28 | new JavaAudioFormatBuilder().pcmSigned().sampleSizeInBits(16).sampleRate(44100.0f).stereo().littleEndian().build();
29 |
30 | /**
31 | * The audio encoding technique used by this format.
32 | */
33 | private AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED;
34 |
35 | /**
36 | * The number of samples played or recorded per second, for sounds that have this format.
37 | */
38 | private float sampleRate = 8000.0f;
39 |
40 | /**
41 | * The number of bits in each sample of a sound that has this format.
42 | */
43 | private int sampleSizeInBits = AudioSystem.NOT_SPECIFIED;
44 |
45 | /**
46 | * The number of audio channels in this format (1 for mono, 2 for stereo).
47 | */
48 | private Channel channel = Channel.NOT_SPECIFIED;
49 |
50 | /**
51 | * Indicates whether the audio data is stored in big-endian or little-endian order.
52 | */
53 | private boolean bigEndian = true;
54 |
55 | public JavaAudioFormatBuilder pcmSigned() {
56 | this.encoding = AudioFormat.Encoding.PCM_SIGNED;
57 | return this;
58 | }
59 |
60 | public JavaAudioFormatBuilder pcmUnsigned() {
61 | this.encoding = AudioFormat.Encoding.PCM_UNSIGNED;
62 | return this;
63 | }
64 |
65 | public JavaAudioFormatBuilder aLaw() {
66 | this.encoding = AudioFormat.Encoding.ALAW;
67 | return this;
68 | }
69 |
70 | public JavaAudioFormatBuilder uLaw() {
71 | this.encoding = AudioFormat.Encoding.ULAW;
72 | return this;
73 | }
74 |
75 | public JavaAudioFormatBuilder sampleRate(float sampleRate) {
76 | this.sampleRate = sampleRate;
77 | return this;
78 | }
79 |
80 | public JavaAudioFormatBuilder sampleSizeInBits(int sampleSizeInBits) {
81 | this.sampleSizeInBits = sampleSizeInBits;
82 | return this;
83 | }
84 |
85 | public JavaAudioFormatBuilder mono() {
86 | this.channel = Channel.MONO;
87 | return this;
88 | }
89 |
90 | public JavaAudioFormatBuilder stereo() {
91 | this.channel = Channel.STEREO;
92 | return this;
93 | }
94 |
95 | public JavaAudioFormatBuilder bigEndian() {
96 | this.bigEndian = true;
97 | return this;
98 | }
99 |
100 | public JavaAudioFormatBuilder littleEndian() {
101 | this.bigEndian = false;
102 | return this;
103 | }
104 |
105 | public AudioFormat build() {
106 | boolean signed = encoding.equals(AudioFormat.Encoding.PCM_SIGNED);
107 | return new AudioFormat(sampleRate, sampleSizeInBits, channel.audioChannels, signed, bigEndian);
108 | }
109 |
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/MonoWavFileReader.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 | import java.io.IOException;
6 |
7 | public class MonoWavFileReader {
8 |
9 | private final File file;
10 | private final RiffHeaderData riffHeaderData;
11 |
12 | public MonoWavFileReader(String fileName) throws IOException {
13 | this(new File(fileName));
14 | }
15 |
16 | public MonoWavFileReader(File file) throws IOException {
17 | this.file = file;
18 | riffHeaderData = new RiffHeaderData(file);
19 | if (riffHeaderData.getFormat().getChannels() != 1)
20 | throw new IllegalArgumentException("Wav file is not Mono.");
21 | }
22 |
23 | public PcmMonoInputStream getNewStream() throws IOException {
24 | PcmMonoInputStream asis = new PcmMonoInputStream(
25 | riffHeaderData.getFormat(),
26 | new FileInputStream(file));
27 | long amount = asis.skip(RiffHeaderData.PCM_RIFF_HEADER_SIZE);
28 | if (amount < RiffHeaderData.PCM_RIFF_HEADER_SIZE)
29 | throw new IllegalArgumentException("cannot skip necessary amount of bytes from underlying stream.");
30 | return asis;
31 | }
32 |
33 | private void validateFrameBoundaries(int frameStart, int frameEnd) {
34 | if (frameStart < 0)
35 | throw new IllegalArgumentException("Start Frame cannot be negative:" + frameStart);
36 | if (frameEnd < frameStart)
37 | throw new IllegalArgumentException("Start Frame cannot be after end frame. Start:"
38 | + frameStart + ", end:" + frameEnd);
39 | if (frameEnd > riffHeaderData.getSampleCount())
40 | throw new IllegalArgumentException("Frame count out of bounds. Max sample count:"
41 | + riffHeaderData.getSampleCount() + " but frame is:" + frameEnd);
42 | }
43 |
44 | public int[] getAllSamples() throws IOException {
45 | PcmMonoInputStream stream = getNewStream();
46 | try {
47 | return stream.readAll();
48 | } finally {
49 | stream.close();
50 | }
51 | }
52 |
53 | public int[] getSamplesAsInts(int frameStart, int frameEnd) throws IOException {
54 | validateFrameBoundaries(frameStart, frameEnd);
55 | PcmMonoInputStream stream = getNewStream();
56 | try {
57 | stream.skipSamples(frameStart);
58 | return stream.readSamplesAsIntArray(frameEnd - frameStart);
59 | } finally {
60 | stream.close();
61 | }
62 | }
63 |
64 |
65 | public PcmAudioFormat getFormat() {
66 | return riffHeaderData.getFormat();
67 | }
68 |
69 | public int getSampleCount() {
70 | return riffHeaderData.getSampleCount();
71 | }
72 |
73 | public File getFile() {
74 | return file;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/PcmAudioFormat.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | /**
4 | * Represents paramters for raw pcm audio sample data.
5 | * Channels represents mono or stereo data. mono=1, stereo=2
6 | */
7 | public class PcmAudioFormat {
8 |
9 | /**
10 | * Sample frequency in sample/sec.
11 | */
12 | private final int sampleRate;
13 | /**
14 | * the amount of bits representing samples.
15 | */
16 | private final int sampleSizeInBits;
17 | /**
18 | * How many bytes are required for representing samples
19 | */
20 | private final int bytesRequiredPerSample;
21 | /**
22 | * channels. For now only 1 or two channels are allowed.
23 | */
24 | private final int channels;
25 | /**
26 | * if data is represented as big endian or little endian.
27 | */
28 | protected final boolean bigEndian;
29 | /**
30 | * if data is signed or unsigned.
31 | */
32 | private final boolean signed;
33 |
34 | protected PcmAudioFormat(int sampleRate, int sampleSizeInBits, int channels, boolean bigEndian, boolean signed) {
35 |
36 | if (sampleRate < 1)
37 | throw new IllegalArgumentException("sampleRate cannot be less than one. But it is:" + sampleRate);
38 | this.sampleRate = sampleRate;
39 |
40 | if (sampleSizeInBits < 2 || sampleSizeInBits > 31) {
41 | throw new IllegalArgumentException("sampleSizeInBits must be between (including) 2-31. But it is:" + sampleSizeInBits);
42 | }
43 | this.sampleSizeInBits = sampleSizeInBits;
44 |
45 | if (channels < 1 || channels > 2) {
46 | throw new IllegalArgumentException("channels must be 1 or 2. But it is:" + channels);
47 | }
48 | this.channels = channels;
49 |
50 | this.bigEndian = bigEndian;
51 | this.signed = signed;
52 | if (sampleSizeInBits % 8 == 0)
53 | bytesRequiredPerSample = sampleSizeInBits / 8;
54 | else
55 | bytesRequiredPerSample = sampleSizeInBits / 8 + 1;
56 | }
57 |
58 | /**
59 | * This is a builder class. By default it generates little endian, mono, signed, 16 bits per sample.
60 | */
61 | public static class Builder {
62 | private int _sampleRate;
63 | private int _sampleSizeInBits = 16;
64 | private int _channels = 1;
65 | private boolean _bigEndian = false;
66 | private boolean _signed = true;
67 |
68 | public Builder(int sampleRate) {
69 | this._sampleRate = sampleRate;
70 | }
71 |
72 | public Builder channels(int channels) {
73 | this._channels = channels;
74 | return this;
75 | }
76 |
77 | public Builder bigEndian() {
78 | this._bigEndian = true;
79 | return this;
80 | }
81 |
82 | public Builder unsigned() {
83 | this._signed = false;
84 | return this;
85 | }
86 |
87 | public Builder sampleSizeInBits(int sampleSizeInBits) {
88 | this._sampleSizeInBits = sampleSizeInBits;
89 | return this;
90 | }
91 |
92 | public PcmAudioFormat build() {
93 | return new PcmAudioFormat(_sampleRate, _sampleSizeInBits, _channels, _bigEndian, _signed);
94 | }
95 | }
96 |
97 | PcmAudioFormat mono16BitSignedLittleEndian(int sampleRate) {
98 | return new PcmAudioFormat(sampleRate, 16, 1, false, true);
99 | }
100 |
101 | public int getSampleRate() {
102 | return sampleRate;
103 | }
104 |
105 | public int getChannels() {
106 | return channels;
107 | }
108 |
109 | public int getSampleSizeInBits() {
110 | return sampleSizeInBits;
111 | }
112 |
113 | /**
114 | * returns the required bytes for the sample bit size. Such that, if 4 or 8 bit samples are used.
115 | * it returns 1, if 12 bit used 2 returns.
116 | *
117 | * @return required byte amount for the sample size in bits.
118 | */
119 | public int getBytePerSample() {
120 | return bytesRequiredPerSample;
121 | }
122 |
123 | public boolean isBigEndian() {
124 | return bigEndian;
125 | }
126 |
127 | public boolean isSigned() {
128 | return signed;
129 | }
130 |
131 | public int sampleCountForMiliseconds(double miliseconds) {
132 | return (int) ((double) sampleRate * miliseconds / 1000d);
133 | }
134 |
135 | public String toString() {
136 | return "[ Sample Rate:" + sampleRate + " , SampleSizeInBits:" + sampleSizeInBits +
137 | ", channels:" + channels + ", signed:" + signed + ", bigEndian:" + bigEndian + " ]";
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/PcmAudioHelper.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import com.hwangjr.jhelper.IOs;
4 |
5 | import java.io.DataInputStream;
6 | import java.io.DataOutputStream;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 | import java.io.RandomAccessFile;
12 |
13 | import static com.hwangjr.jhelper.Bytes.toByteArray;
14 |
15 | public class PcmAudioHelper {
16 |
17 | /**
18 | * Converts a pcm encoded raw audio stream to a wav file.
19 | *
20 | * @param af format
21 | * @param rawSource raw source file
22 | * @param wavTarget raw file target
23 | * @throws IOException thrown if an error occurs during file operations.
24 | */
25 | public static void convertRawToWav(WavAudioFormat af, File rawSource, File wavTarget) throws IOException {
26 | DataOutputStream dos = new DataOutputStream(new FileOutputStream(wavTarget));
27 | dos.write(new RiffHeaderData(af, 0).asByteArray());
28 | DataInputStream dis = new DataInputStream(new FileInputStream(rawSource));
29 | byte[] buffer = new byte[4096];
30 | int i;
31 | int total = 0;
32 | while ((i = dis.read(buffer)) != -1) {
33 | total += i;
34 | dos.write(buffer, 0, i);
35 | }
36 | dos.close();
37 | modifyRiffSizeData(wavTarget, total);
38 | }
39 |
40 | public static void convertWavToRaw(File wavSource, File rawTarget) throws IOException {
41 | IOs.copy(new MonoWavFileReader(wavSource).getNewStream(), new FileOutputStream(rawTarget));
42 | }
43 |
44 | public static double[] readAllFromWavNormalized(String fileName) throws IOException {
45 | return new MonoWavFileReader(new File(fileName)).getNewStream().readSamplesNormalized();
46 | }
47 |
48 | /**
49 | * Modifies the size information in a wav file header.
50 | *
51 | * @param wavFile a wav file
52 | * @param size size to replace the header.
53 | * @throws IOException if an error occurs whule accesing the data.
54 | */
55 | static void modifyRiffSizeData(File wavFile, int size) throws IOException {
56 | RandomAccessFile raf = new RandomAccessFile(wavFile, "rw");
57 | raf.seek(RiffHeaderData.RIFF_CHUNK_SIZE_INDEX);
58 | raf.write(toByteArray(size + 36, false));
59 | raf.seek(RiffHeaderData.RIFF_SUBCHUNK2_SIZE_INDEX);
60 | raf.write(toByteArray(size, false));
61 | raf.close();
62 | }
63 |
64 | public static void generateSilenceWavFile(WavAudioFormat wavAudioFormat, File file, double sec) throws IOException {
65 | WavFileWriter wfr = new WavFileWriter(wavAudioFormat, file);
66 | int[] empty = new int[(int) (sec * wavAudioFormat.getSampleRate())];
67 | try {
68 | wfr.write(empty);
69 | } finally {
70 | wfr.close();
71 | }
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/PcmMonoInputStream.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import com.hwangjr.jhelper.Bytes;
4 | import com.hwangjr.jhelper.IOs;
5 |
6 | import java.io.Closeable;
7 | import java.io.DataInputStream;
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 |
11 | public class PcmMonoInputStream extends InputStream implements Closeable {
12 |
13 | private final PcmAudioFormat format;
14 | private final DataInputStream dis;
15 | /**
16 | * this is used for normalization.
17 | */
18 | private final int maxPositiveIntegerForSampleSize;
19 |
20 |
21 | public PcmMonoInputStream(PcmAudioFormat format, InputStream is) {
22 | if (format.getChannels() != 1)
23 | throw new IllegalArgumentException("Only mono streams are supported.");
24 | this.format = format;
25 | this.dis = new DataInputStream(is);
26 | this.maxPositiveIntegerForSampleSize = 0x7fffffff >>> (32 - format.getSampleSizeInBits());
27 | }
28 |
29 | public int read() throws IOException {
30 | return dis.read();
31 | }
32 |
33 | public int[] readSamplesAsIntArray(int amount) throws IOException {
34 | byte[] bytez = new byte[amount * format.getBytePerSample()];
35 | int readAmount = dis.read(bytez);
36 | if (readAmount == -1)
37 | return new int[0];
38 | return Bytes.toReducedBitIntArray(
39 | bytez,
40 | readAmount,
41 | format.getBytePerSample(),
42 | format.getSampleSizeInBits(),
43 | format.isBigEndian());
44 | }
45 |
46 | public int[] readAll() throws IOException {
47 | byte[] all = IOs.readAsByteArray(dis);
48 | return Bytes.toReducedBitIntArray(
49 | all,
50 | all.length,
51 | format.getBytePerSample(),
52 | format.getSampleSizeInBits(),
53 | format.isBigEndian());
54 | }
55 |
56 | private static final int BYTE_BUFFER_SIZE = 4096;
57 |
58 | /**
59 | * reads samples as byte array. if there is not enough data for the amount of samples, remaining data is returned
60 | * anyway. if the byte amount is not an order of bytes required for sample (such as 51 bytes left but 16 bit samples)
61 | * an IllegalStateException is thrown.
62 | *
63 | * @param amount amount of samples to read.
64 | * @return byte array.
65 | * @throws IOException if there is an IO error.
66 | * @throws IllegalStateException if the amount of bytes read is not an order of correct.
67 | */
68 | public byte[] readSamplesAsByteArray(int amount) throws IOException {
69 |
70 | byte[] bytez = new byte[amount * format.getBytePerSample()];
71 | int readCount = dis.read(bytez);
72 | if (readCount != bytez.length) {
73 | validateReadCount(readCount);
74 | byte[] result = new byte[readCount];
75 | System.arraycopy(bytez, 0, result, 0, readCount);
76 | return result;
77 | } else
78 | return bytez;
79 | }
80 |
81 | private void validateReadCount(int readCount) {
82 | if (readCount % format.getBytePerSample() != 0)
83 | throw new IllegalStateException("unexpected amounts of bytes read from the input stream. " +
84 | "Byte count must be an order of:" + format.getBytePerSample());
85 | }
86 |
87 | public int[] readSamplesAsIntArray(int frameStart, int frameEnd) throws IOException {
88 | skipSamples(frameStart * format.getBytePerSample());
89 | return readSamplesAsIntArray(frameEnd - frameStart);
90 | }
91 |
92 | /**
93 | * skips samples from the stream. if end of file is reached, it returns the amount that is actually skipped.
94 | *
95 | * @param skipAmount amount of samples to skip
96 | * @return actual skipped sample count.
97 | * @throws IOException if there is a problem while skipping.
98 | */
99 | public int skipSamples(int skipAmount) throws IOException {
100 | long actualSkipped = dis.skip(skipAmount * format.getBytePerSample());
101 | return (int) actualSkipped / format.getBytePerSample();
102 | }
103 |
104 | public double[] readSamplesNormalized(int amount) throws IOException {
105 | return normalize(readSamplesAsIntArray(amount));
106 | }
107 |
108 | public double[] readSamplesNormalized() throws IOException {
109 | return normalize(readAll());
110 | }
111 |
112 | private double[] normalize(int[] original) {
113 | if (original.length == 0)
114 | return new double[0];
115 | double[] normalized = new double[original.length];
116 | for (int i = 0; i < normalized.length; i++) {
117 | normalized[i] = (double) original[i] / maxPositiveIntegerForSampleSize;
118 | }
119 | return normalized;
120 | }
121 |
122 | public void close() throws IOException {
123 | dis.close();
124 | }
125 |
126 | /**
127 | * finds the byte location of a given time. if time is negative, exception is thrown.
128 | *
129 | * @param second second information
130 | * @return the byte location in the samples.
131 | */
132 | public int calculateSampleByteIndex(double second) {
133 |
134 | if (second < 0)
135 | throw new IllegalArgumentException("Time information cannot be negative.");
136 |
137 | int loc = (int) (second * format.getSampleRate() * format.getBytePerSample());
138 |
139 | //byte alignment.
140 | if (loc % format.getBytePerSample() != 0) {
141 | loc += (format.getBytePerSample() - loc % format.getBytePerSample());
142 | }
143 | return loc;
144 | }
145 |
146 | /**
147 | * calcualates the time informationn for a given sample.
148 | *
149 | * @param sampleIndex sample index.
150 | * @return approximate seconds information for the given sample.
151 | */
152 | public double calculateSampleTime(int sampleIndex) {
153 | if (sampleIndex < 0)
154 | throw new IllegalArgumentException("sampleIndex information cannot be negative:" + sampleIndex);
155 |
156 | return (double) sampleIndex / format.getSampleRate();
157 | }
158 |
159 | public PcmAudioFormat getFormat() {
160 | return format;
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/PcmMonoOutputStream.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import com.hwangjr.jhelper.Bytes;
4 | import com.hwangjr.jhelper.IOs;
5 |
6 | import java.io.Closeable;
7 | import java.io.DataOutputStream;
8 | import java.io.File;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 | import java.io.OutputStream;
12 |
13 | public class PcmMonoOutputStream extends OutputStream implements Closeable {
14 |
15 | final PcmAudioFormat format;
16 | final DataOutputStream dos;
17 |
18 | public PcmMonoOutputStream(PcmAudioFormat format, DataOutputStream dos) {
19 | this.format = format;
20 | this.dos = dos;
21 | }
22 |
23 | public PcmMonoOutputStream(PcmAudioFormat format, File file) throws IOException {
24 | this.format = format;
25 | this.dos = new DataOutputStream(new FileOutputStream(file));
26 | }
27 |
28 | public void write(int b) throws IOException {
29 | dos.write(b);
30 | }
31 |
32 | public void write(short[] shorts) throws IOException {
33 | dos.write(Bytes.toByteArray(shorts, shorts.length, format.isBigEndian()));
34 | }
35 |
36 | public void write(int[] ints) throws IOException {
37 | dos.write(Bytes.toByteArray(ints, ints.length, format.getBytePerSample(), format.isBigEndian()));
38 | }
39 |
40 | public void close() {
41 | IOs.closeSilently(dos);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/RiffHeaderData.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import com.hwangjr.jhelper.IOs;
4 |
5 | import java.io.ByteArrayOutputStream;
6 | import java.io.DataInputStream;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 |
11 | import static com.hwangjr.jhelper.Bytes.toByteArray;
12 | import static com.hwangjr.jhelper.Bytes.toInt;
13 |
14 | class RiffHeaderData {
15 |
16 | public static final int PCM_RIFF_HEADER_SIZE = 44;
17 | public static final int RIFF_CHUNK_SIZE_INDEX = 4;
18 | public static final int RIFF_SUBCHUNK2_SIZE_INDEX = 40;
19 |
20 | private final PcmAudioFormat format;
21 | private final int totalSamplesInByte;
22 |
23 | public RiffHeaderData(PcmAudioFormat format, int totalSamplesInByte) {
24 | this.format = format;
25 | this.totalSamplesInByte = totalSamplesInByte;
26 | }
27 |
28 | public double timeSeconds() {
29 | return (double) totalSamplesInByte / format.getBytePerSample() / format.getSampleRate();
30 | }
31 |
32 | public RiffHeaderData(DataInputStream dis) throws IOException {
33 |
34 | try {
35 | byte[] buf4 = new byte[4];
36 | byte[] buf2 = new byte[2];
37 |
38 | dis.skipBytes(4 + 4 + 4 + 4 + 4 + 2);
39 |
40 | dis.readFully(buf2);
41 | final int channels = toInt(buf2, false);
42 |
43 | dis.readFully(buf4);
44 | final int sampleRate = toInt(buf4, false);
45 |
46 | dis.skipBytes(4 + 2);
47 |
48 | dis.readFully(buf2);
49 | final int sampleSizeInBits = toInt(buf2, false);
50 |
51 | dis.skipBytes(4);
52 |
53 | dis.readFully(buf4);
54 | totalSamplesInByte = toInt(buf4, false);
55 |
56 | format = new WavAudioFormat.Builder().
57 | channels(channels).
58 | sampleRate(sampleRate).
59 | sampleSizeInBits(sampleSizeInBits).
60 | build();
61 | } finally {
62 | IOs.closeSilently(dis);
63 | }
64 | }
65 |
66 | public RiffHeaderData(File file) throws IOException {
67 | this(new DataInputStream(new FileInputStream(file)));
68 | }
69 |
70 | public byte[] asByteArray() {
71 | ByteArrayOutputStream baos = null;
72 | try {
73 | baos = new ByteArrayOutputStream();
74 | // ChunkID (the String "RIFF") 4 Bytes
75 | baos.write(toByteArray(0x52494646, true));
76 | // ChunkSize (Whole file size in byte minus 8 bytes ) , or (4 + (8 + SubChunk1Size) + (8 + SubChunk2Size))
77 | // little endian 4 Bytes.
78 | baos.write(toByteArray(36 + totalSamplesInByte, false));
79 | // Format (the String "WAVE") 4 Bytes big endian
80 | baos.write(toByteArray(0x57415645, true));
81 |
82 | // Subchunk1
83 | // Subchunk1ID (the String "fmt ") 4 bytes big endian.
84 | baos.write(toByteArray(0x666d7420, true));
85 | // Subchunk1Size. 16 for the PCM. little endian 4 bytes.
86 | baos.write(toByteArray(16, false));
87 | // AudioFormat , for PCM = 1, Little endian 2 Bytes.
88 | baos.write(toByteArray((short) 1, false));
89 | // Number of channels Mono = 1, Stereo = 2 Little Endian , 2 bytes.
90 | int channels = format.getChannels();
91 | baos.write(toByteArray((short) channels, false));
92 | // SampleRate (8000, 44100 etc.) little endian, 4 bytes
93 | int sampleRate = format.getSampleRate();
94 | baos.write(toByteArray(sampleRate, false));
95 | // byte rate (SampleRate * NumChannels * BitsPerSample/8) little endian, 4 bytes.
96 | baos.write(toByteArray(channels * sampleRate * format.getBytePerSample(), false));
97 | // Block Allign == NumChannels * BitsPerSample/8 The number of bytes for one sample including all channels. LE, 2 bytes
98 | baos.write(toByteArray((short) (channels * format.getBytePerSample()), false));
99 | // BitsPerSample (8, 16 etc.) LE, 2 bytes
100 | baos.write(toByteArray((short) format.getSampleSizeInBits(), false));
101 |
102 | // Subchunk2
103 | // SubChunk2ID (String "data") 4 bytes.
104 | baos.write(toByteArray(0x64617461, true));
105 | // Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8. This is the number of bytes in the data.
106 | // You can also think of this as the size of the read of the subchunk following this number. LE, 4 bytes.
107 | baos.write(toByteArray(totalSamplesInByte, false));
108 |
109 | return baos.toByteArray();
110 | } catch (IOException e) {
111 | e.printStackTrace();
112 | return new byte[0];
113 | } finally {
114 | IOs.closeSilently(baos);
115 | }
116 | }
117 |
118 | public PcmAudioFormat getFormat() {
119 | return format;
120 | }
121 |
122 | public int getTotalSamplesInByte() {
123 | return totalSamplesInByte;
124 | }
125 |
126 | public int getSampleCount() {
127 | return totalSamplesInByte / format.getBytePerSample();
128 | }
129 |
130 | public String toString() {
131 | return "[ Format: " + format.toString() + " , totalSamplesInByte:" + totalSamplesInByte + "]";
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/WavAudioFormat.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | public class WavAudioFormat extends PcmAudioFormat {
4 |
5 | /**
6 | * if data is represented as big endian or little endian.
7 | */
8 | protected final boolean bigEndian = false;
9 |
10 | private WavAudioFormat(int sampleRate, int sampleSizeInBits, int channels, boolean signed) {
11 | super(sampleRate, sampleSizeInBits, channels, false, signed);
12 | }
13 |
14 | /**
15 | * a builder class for generating PCM Audio format for wav files.
16 | */
17 | public static class Builder {
18 | private int _sampleRate;
19 | private int _sampleSizeInBits = 16;
20 | private int _channels = 1;
21 |
22 | public Builder sampleRate(int sampleRate) {
23 | this._sampleRate = sampleRate;
24 | return this;
25 | }
26 |
27 | public Builder channels(int channels) {
28 | this._channels = channels;
29 | return this;
30 | }
31 |
32 | public Builder sampleSizeInBits(int sampleSizeInBits) {
33 | this._sampleSizeInBits = sampleSizeInBits;
34 | return this;
35 | }
36 |
37 | public WavAudioFormat build() {
38 | if (_sampleSizeInBits == 8)
39 | return new WavAudioFormat(_sampleRate, _sampleSizeInBits, _channels, false);
40 | else
41 | return new WavAudioFormat(_sampleRate, _sampleSizeInBits, _channels, true);
42 | }
43 | }
44 |
45 | /**
46 | * generates a PcmAudioFormat for wav files for 16 bits signed mono data.
47 | *
48 | * @param sampleRate sampling rate.
49 | * @return new PcmAudioFormat object for given wav header values. .
50 | */
51 | public static WavAudioFormat mono16Bit(int sampleRate) {
52 | return new WavAudioFormat(sampleRate, 16, 1, true);
53 | }
54 |
55 | /**
56 | * Generates audio format data for Wav audio format. returning PCM format is little endian.
57 | *
58 | * @param sampleRate sample rate
59 | * @param sampleSizeInBits bit amount per sample
60 | * @param channels channel count. can be 1 or 2
61 | * @return a RawAudioFormat suitable for wav format.
62 | */
63 | public static WavAudioFormat wavFormat(int sampleRate, int sampleSizeInBits, int channels) {
64 | if (sampleSizeInBits == 8)
65 | return new WavAudioFormat(sampleRate, sampleSizeInBits, channels, false);
66 | else
67 | return new WavAudioFormat(sampleRate, sampleSizeInBits, channels, true);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/pcm/WavFileWriter.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import java.io.Closeable;
4 | import java.io.File;
5 | import java.io.IOException;
6 |
7 | /**
8 | * Writes a wav file. Careful that it writes the total amount of the bytes information once the close method
9 | * is called. It has a counter in it to calculate the samle size.
10 | */
11 | public class WavFileWriter implements Closeable {
12 |
13 | private final WavAudioFormat pcmAudioFormat;
14 | private final PcmMonoOutputStream pos;
15 | private int totalSampleBytesWritten = 0;
16 | private final File file;
17 |
18 | public WavFileWriter(WavAudioFormat wavAudioFormat, File file) throws IOException {
19 | if (wavAudioFormat.isBigEndian())
20 | throw new IllegalArgumentException("Wav file cannot contain bigEndian sample data.");
21 | if (wavAudioFormat.getSampleSizeInBits() > 8 && !wavAudioFormat.isSigned())
22 | throw new IllegalArgumentException("Wav file cannot contain unsigned data for this sampleSize:"
23 | + wavAudioFormat.getSampleSizeInBits());
24 | this.pcmAudioFormat = wavAudioFormat;
25 | this.file = file;
26 | this.pos = new PcmMonoOutputStream(wavAudioFormat, file);
27 | pos.write(new RiffHeaderData(wavAudioFormat, 0).asByteArray());
28 | }
29 |
30 | public WavFileWriter write(byte[] bytes) throws IOException {
31 | checkLimit(totalSampleBytesWritten, bytes.length);
32 | pos.write(bytes);
33 | totalSampleBytesWritten += bytes.length;
34 | return this;
35 | }
36 |
37 | private void checkLimit(int total, int toAdd) {
38 | final long result = total + toAdd;
39 | if (result >= Integer.MAX_VALUE) {
40 | throw new IllegalStateException("Size of bytes is too big:" + result);
41 | }
42 | }
43 |
44 | public WavFileWriter write(int[] samples) throws IOException {
45 | final int bytePerSample = pcmAudioFormat.getBytePerSample();
46 | checkLimit(totalSampleBytesWritten, samples.length * bytePerSample);
47 | pos.write(samples);
48 | totalSampleBytesWritten += samples.length * bytePerSample;
49 | return this;
50 | }
51 |
52 | WavFileWriter writeNormalized(double[] samples) throws IOException {
53 | return this;
54 | }
55 |
56 | public void close() throws IOException {
57 | pos.close();
58 | PcmAudioHelper.modifyRiffSizeData(file, totalSampleBytesWritten);
59 | }
60 |
61 | public PcmAudioFormat getWavFormat() {
62 | return pcmAudioFormat;
63 | }
64 |
65 |
66 | public int getTotalSampleBytesWritten() {
67 | return totalSampleBytesWritten;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/soundhelper/src/main/java/com/hwangjr/soundhelper/system/WavPlayer.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.system;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | import javax.sound.sampled.AudioFormat;
7 | import javax.sound.sampled.AudioInputStream;
8 | import javax.sound.sampled.AudioSystem;
9 | import javax.sound.sampled.DataLine;
10 | import javax.sound.sampled.SourceDataLine;
11 | import javax.sound.sampled.UnsupportedAudioFileException;
12 |
13 | public class WavPlayer extends Thread {
14 |
15 | private final String filename;
16 |
17 |
18 | private final int EXTERNAL_BUFFER_SIZE = 524288; // 128Kb
19 |
20 | public WavPlayer(String wavfile) {
21 | filename = wavfile;
22 | }
23 |
24 | public WavPlayer(File wavfile) {
25 | filename = wavfile.getAbsolutePath();
26 | }
27 |
28 | public void run() {
29 |
30 | File soundFile = new File(filename);
31 | if (!soundFile.exists()) {
32 | System.err.println("Wave file not found: " + filename);
33 | return;
34 | }
35 |
36 | AudioInputStream audioInputStream;
37 | try {
38 | audioInputStream = AudioSystem.getAudioInputStream(soundFile);
39 | } catch (UnsupportedAudioFileException e1) {
40 | e1.printStackTrace();
41 | return;
42 | } catch (IOException e1) {
43 | e1.printStackTrace();
44 | return;
45 | }
46 |
47 | AudioFormat format = audioInputStream.getFormat();
48 | SourceDataLine auline;
49 | DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
50 |
51 | try {
52 | auline = (SourceDataLine) AudioSystem.getLine(info);
53 | auline.open(format);
54 | } catch (Exception e) {
55 | e.printStackTrace();
56 | return;
57 | }
58 |
59 | auline.start();
60 | int nBytesRead = 0;
61 | byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
62 |
63 | try {
64 | while (nBytesRead != -1) {
65 | nBytesRead = audioInputStream.read(abData, 0, abData.length);
66 | if (nBytesRead >= 0)
67 | auline.write(abData, 0, nBytesRead);
68 | }
69 | } catch (IOException e) {
70 | e.printStackTrace();
71 | } finally {
72 | auline.drain();
73 | auline.close();
74 | }
75 |
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/soundhelper/src/test/java/com/hwangjr/soundhelper/pcm/IterableFrameReaderTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import com.hwangjr.soundhelper.dsp.DoubleVector;
4 | import com.hwangjr.soundhelper.dsp.DoubleVectorFrameSource;
5 | import com.hwangjr.soundhelper.dsp.DoubleVectorProcessor;
6 | import com.hwangjr.soundhelper.dsp.WindowerFactory;
7 |
8 | import junit.framework.Assert;
9 |
10 | import org.junit.Test;
11 |
12 | import java.io.IOException;
13 |
14 | public class IterableFrameReaderTest {
15 |
16 | public static final double EPSILON = 0.01;
17 |
18 | @Test
19 | public void testIterableFrameReader() throws IOException {
20 | PcmMonoInputStream pmis = new MonoWavFileReader("wav-samples/square-0_8amp-440hz-1000sample-16bit-mono.wav").getNewStream();
21 | DoubleVectorFrameSource dvfs = DoubleVectorFrameSource.fromSampleAmount(pmis, 10, 50);
22 | int frameCounter = 0;
23 | for (DoubleVector ddf : dvfs.getIterableFrameReader()) {
24 | frameCounter++;
25 | Assert.assertEquals(ddf.size(), 100);
26 | for (double d : ddf.getData()) {
27 | Assert.assertTrue("oops:" + d, d <= (0.8d + EPSILON) && d > (-0.8d - EPSILON));
28 | }
29 | }
30 | Assert.assertEquals(frameCounter, (1000 - 100) / 50 + 1);
31 | pmis.close();
32 | }
33 |
34 | public static void main(String[] args) throws IOException {
35 | PcmMonoInputStream pmis = new MonoWavFileReader("wav-samples/square-0_8amp-440hz-1000sample-16bit-mono.wav").getNewStream();
36 | DoubleVectorFrameSource dvfs = DoubleVectorFrameSource.fromSampleAmount(pmis, 10, 50);
37 | DoubleVectorProcessor windower = WindowerFactory.newHammingWindower(dvfs.getFrameSize());
38 | for (DoubleVector ddf : dvfs.getIterableFrameReader()) {
39 | System.out.println("orig:" + ddf);
40 | System.out.println("proc:" + windower.process(ddf));
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/soundhelper/src/test/java/com/hwangjr/soundhelper/pcm/PcmAudioFormatTest.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import junit.framework.Assert;
4 |
5 | import org.junit.Test;
6 |
7 | public class PcmAudioFormatTest {
8 |
9 | @Test
10 | public void testBuilders() {
11 |
12 | PcmAudioFormat paf = new PcmAudioFormat.Builder(16000).build();
13 | Assert.assertTrue(paf.getChannels() == 1 &&
14 | paf.getSampleRate() == 16000 &&
15 | paf.getSampleSizeInBits() == 16 &&
16 | paf.getBytePerSample() == 2 &&
17 | !paf.isBigEndian());
18 |
19 | paf = new PcmAudioFormat.Builder(8000).
20 | bigEndian().
21 | channels(2).
22 | sampleSizeInBits(12).
23 | unsigned().
24 | build();
25 |
26 | Assert.assertTrue(paf.getChannels() == 2 &&
27 | paf.getSampleRate() == 8000 &&
28 | paf.getSampleSizeInBits() == 12 &&
29 | paf.getBytePerSample() == 2 &&
30 | paf.isBigEndian());
31 | }
32 |
33 | @Test(expected = IllegalArgumentException.class)
34 | public void testBuilderException() {
35 | new PcmAudioFormat.Builder(0).build();
36 | }
37 |
38 | @Test(expected = IllegalArgumentException.class)
39 | public void testBuilderException2() {
40 | new PcmAudioFormat.Builder(1000).channels(0).build();
41 | }
42 |
43 | @Test(expected = IllegalArgumentException.class)
44 | public void testBuilderException3() {
45 | new PcmAudioFormat.Builder(1000).channels(3).build();
46 | }
47 |
48 | @Test(expected = IllegalArgumentException.class)
49 | public void testBuilderException4() {
50 | new PcmAudioFormat.Builder(-1).build();
51 | }
52 |
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/soundhelper/src/test/java/com/hwangjr/soundhelper/pcm/PcmAudioInputStreamTests.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import org.junit.Test;
4 |
5 | public class PcmAudioInputStreamTests {
6 |
7 | @Test
8 | public void testReadInt() {
9 |
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/soundhelper/src/test/java/com/hwangjr/soundhelper/pcm/WavAuidoFormatTests.java:
--------------------------------------------------------------------------------
1 | package com.hwangjr.soundhelper.pcm;
2 |
3 | import junit.framework.Assert;
4 |
5 | import org.junit.Test;
6 |
7 | public class WavAuidoFormatTests {
8 |
9 | @Test
10 | public void testWavBuilders() {
11 | PcmAudioFormat wavFormat = new WavAudioFormat.Builder().
12 | sampleRate(8000).
13 | channels(1).
14 | sampleSizeInBits(16).
15 | build();
16 |
17 | Assert.assertTrue(wavFormat.getChannels() == 1 &&
18 | wavFormat.getSampleRate() == 8000 &&
19 | wavFormat.getSampleSizeInBits() == 16 &&
20 | !wavFormat.isBigEndian() &&
21 | wavFormat.isSigned() &&
22 | wavFormat.getBytePerSample() == 2);
23 |
24 | wavFormat = WavAudioFormat.wavFormat(8000, 8, 1);
25 |
26 | // notice the !isSigned() because 8 bit data is unsigned in wav
27 | Assert.assertTrue(wavFormat.getChannels() == 1 &&
28 | wavFormat.getSampleRate() == 8000 &&
29 | wavFormat.getSampleSizeInBits() == 8 &&
30 | !wavFormat.isBigEndian() &&
31 | !wavFormat.isSigned() &&
32 | wavFormat.getBytePerSample() == 1);
33 |
34 | wavFormat = WavAudioFormat.mono16Bit(44100);
35 |
36 | Assert.assertTrue(wavFormat.getChannels() == 1 &&
37 | wavFormat.getSampleRate() == 44100 &&
38 | wavFormat.getSampleSizeInBits() == 16 &&
39 | !wavFormat.isBigEndian() &&
40 | wavFormat.getBytePerSample() == 2);
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------