diff --git a/app/build.gradle b/app/build.gradle index a206af3..dea87b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,4 +44,5 @@ dependencies { compile 'com.google.android.gms:play-services-appindexing:9.8.0' compile 'org.apache.commons:commons-math3:3.6.1' compile 'com.github.axet:android-library:1.8.2' // compile project(':android-library') + compile 'com.github.axet:jebml:0.0.1' // compile project(':jebml') } diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java index d5b98b0..78e83c4 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java @@ -47,6 +47,7 @@ import com.github.axet.audiorecorder.encoders.EncoderInfo; import com.github.axet.audiorecorder.encoders.FileEncoder; import com.github.axet.audiorecorder.encoders.Format3GP; import com.github.axet.audiorecorder.encoders.FormatM4A; +import com.github.axet.audiorecorder.encoders.FormatMKA; import com.github.axet.audiorecorder.encoders.FormatWAV; import com.github.axet.audiorecorder.services.RecordingService; import com.github.axet.audiorecorder.widgets.PitchView; @@ -794,6 +795,9 @@ public class RecordingActivity extends AppCompatActivity { if (ext.equals("3gp")) { e = new Format3GP(info, out); } + if (ext.equals("mka")) { + e = new FormatMKA(info, out); + } encoder = new FileEncoder(this, in, e); diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java index 600d5b5..4eb885c 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java @@ -8,7 +8,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.graphics.drawable.ColorDrawable; import android.media.MediaCodecInfo; import android.media.Ringtone; import android.media.RingtoneManager; @@ -30,6 +29,9 @@ import com.github.axet.audiorecorder.R; import com.github.axet.audiorecorder.app.MainApplication; import com.github.axet.audiorecorder.encoders.MuxerMP4; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -46,6 +48,13 @@ import java.util.Map; */ public class SettingsActivity extends AppCompatPreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + public static T[] removeElement(Class c, T[] aa, int i) { + List ll = Arrays.asList(aa); + ll = new ArrayList<>(ll); + ll.remove(i); + return ll.toArray((T[]) Array.newInstance(c, ll.size())); + } + /** * A preference value change listener that updates the preference's summary * to reflect its new value. @@ -262,16 +271,37 @@ public class SettingsActivity extends AppCompatPreferenceActivity implements Sha bindPreferenceSummaryToValue(findPreference(MainApplication.PREFERENCE_STORAGE)); } - Preference enc = findPreference(MainApplication.PREFERENCE_ENCODING); + ListPreference enc = (ListPreference) findPreference(MainApplication.PREFERENCE_ENCODING); - if (Build.VERSION.SDK_INT < 18) { // depends on MediaMuxer + if (Build.VERSION.SDK_INT < 16) { // Android 4.1 getPreferenceScreen().removePreference(enc); } else { + if (Build.VERSION.SDK_INT < 18) { // MediaMuxer + String v = enc.getValue(); + int i = enc.findIndexOfValue("m4a"); + CharSequence[] ee = enc.getEntries(); + CharSequence[] vv = enc.getEntryValues(); + ee = removeElement(CharSequence.class, ee, i); + vv = removeElement(CharSequence.class, vv, i); + enc.setEntries(ee); + enc.setEntryValues(vv); + enc.setValueIndex(0); + i = enc.findIndexOfValue(v); + if (i == -1) { + enc.setValue("wav"); + enc.setValueIndex(0); + } else { + enc.setValueIndex(i); + enc.setValue(v); + } + } + Map mime = MuxerMP4.findEncoder("audio/mp4"); if (mime.isEmpty()) getPreferenceScreen().removePreference(enc); - else - bindPreferenceSummaryToValue(enc); + else { + //bindPreferenceSummaryToValue(enc); + } } bindPreferenceSummaryToValue(findPreference(MainApplication.PREFERENCE_RATE)); diff --git a/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java b/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java index a9b8c84..9c35bac 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java +++ b/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java @@ -197,7 +197,20 @@ public class Storage { int rate = Integer.parseInt(shared.getString(MainApplication.PREFERENCE_RATE, "")); String ext = shared.getString(MainApplication.PREFERENCE_ENCODING, ""); - if (ext.equals("m4a")) { + if (ext.equals("m4a") || ext.equals("mka")) { + long y1 = 365723; // one minute sample 16000Hz + long x1 = 16000; // at 16000 + long y2 = 493743; // one minute sample + long x2 = 44000; // at 44000 + long x = rate; + long y = (x - x1) * (y2 - y1) / (x2 - x1) + y1; + + int m = MainApplication.getChannels(context); + long perSec = (y / 60) * m; + return free / perSec * 1000; + } + + if (ext.equals("mka")) { long y1 = 365723; // one minute sample 16000Hz long x1 = 16000; // at 16000 long y2 = 493743; // one minute sample diff --git a/app/src/main/java/com/github/axet/audiorecorder/encoders/Encoder.java b/app/src/main/java/com/github/axet/audiorecorder/encoders/Encoder.java index 6659fa2..d3f2678 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/encoders/Encoder.java +++ b/app/src/main/java/com/github/axet/audiorecorder/encoders/Encoder.java @@ -3,7 +3,5 @@ package com.github.axet.audiorecorder.encoders; public interface Encoder { void encode(short[] buf, int len); - void end(); // flush stream. may throw state exceptions - - void close(); // release native resources, sholud not throw exceptions + void close(); } diff --git a/app/src/main/java/com/github/axet/audiorecorder/encoders/EncoderInfo.java b/app/src/main/java/com/github/axet/audiorecorder/encoders/EncoderInfo.java index 84a2094..a5636b6 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/encoders/EncoderInfo.java +++ b/app/src/main/java/com/github/axet/audiorecorder/encoders/EncoderInfo.java @@ -1,8 +1,5 @@ package com.github.axet.audiorecorder.encoders; -/** - * Created by axet on 11/03/16. - */ public class EncoderInfo { public int channels; public int sampleRate; diff --git a/app/src/main/java/com/github/axet/audiorecorder/encoders/FileEncoder.java b/app/src/main/java/com/github/axet/audiorecorder/encoders/FileEncoder.java index 22c5af2..8002f65 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/encoders/FileEncoder.java +++ b/app/src/main/java/com/github/axet/audiorecorder/encoders/FileEncoder.java @@ -47,9 +47,7 @@ public class FileEncoder { while (!Thread.currentThread().isInterrupted()) { int len = rs.read(buf); if (len <= 0) { - encoder.end(); - handler.post(done); - return; + break; } else { encoder.encode(buf, len); handler.post(progress); @@ -58,15 +56,15 @@ public class FileEncoder { } } } - } catch (RuntimeException e) { - Log.e(TAG, "Exception", e); - t = e; - handler.post(error); - } finally { encoder.close(); if (rs != null) { rs.close(); } + handler.post(done); + } catch (RuntimeException e) { + Log.e(TAG, "Exception", e); + t = e; + handler.post(error); } } }); diff --git a/app/src/main/java/com/github/axet/audiorecorder/encoders/FormatMKA.java b/app/src/main/java/com/github/axet/audiorecorder/encoders/FormatMKA.java new file mode 100755 index 0000000..565a438 --- /dev/null +++ b/app/src/main/java/com/github/axet/audiorecorder/encoders/FormatMKA.java @@ -0,0 +1,183 @@ +package com.github.axet.audiorecorder.encoders; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Build; + +import org.ebml.io.FileDataWriter; +import org.ebml.matroska.MatroskaFileFrame; +import org.ebml.matroska.MatroskaFileTrack; +import org.ebml.matroska.MatroskaFileWriter; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +@TargetApi(16) // mp4/aac codec +public class FormatMKA implements Encoder { + EncoderInfo info; + MediaCodec encoder; + long NumSamples; + ByteBuffer input; + int inputIndex; + MatroskaFileWriter writer; + MatroskaFileTrack track; + MatroskaFileTrack.MatroskaAudioTrack audio; + + MatroskaFileFrame old; + + public FormatMKA(EncoderInfo info, File out) { + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, info.sampleRate); + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, info.channels); + format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1024); + format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectHE); + format.setInteger(MediaFormat.KEY_AAC_SBR_MODE, 0); + create(info, format, out); + } + + public void create(EncoderInfo info, MediaFormat format, File out) { + this.info = info; + try { + encoder = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)); + encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + encoder.start(); + writer = new MatroskaFileWriter(new FileDataWriter(out.getAbsolutePath())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void encode(short[] buf, int len) { + for (int offset = 0; offset < len; offset++) { + if (input == null) { + inputIndex = encoder.dequeueInputBuffer(-1); + if (inputIndex < 0) + throw new RuntimeException("unable to open encoder input buffer"); + if (Build.VERSION.SDK_INT >= 21) + input = encoder.getInputBuffer(inputIndex); + else + input = encoder.getInputBuffers()[inputIndex]; + input.clear(); + } + input.putShort(buf[offset]); + if (!input.hasRemaining()) { + queue(); + } + } + } + + void queue() { + if (input == null) + return; + encoder.queueInputBuffer(inputIndex, 0, input.position(), getCurrentTimeStamp(), 0); + NumSamples += input.position() / info.channels / (Short.SIZE / 8); + input = null; + while (encode()) + ;// do encode() + } + + public static ByteBuffer clone(ByteBuffer original) { + ByteBuffer clone = ByteBuffer.allocate(original.capacity()); + original.rewind();//copy from the beginning + clone.put(original); + original.rewind(); + clone.flip(); + return clone; + } + + boolean encode() { + MediaCodec.BufferInfo outputInfo = new MediaCodec.BufferInfo(); + int outputIndex = encoder.dequeueOutputBuffer(outputInfo, 0); + if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) + return false; + + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + audio = new MatroskaFileTrack.MatroskaAudioTrack(); + audio.setSamplingFrequency(info.sampleRate); + audio.setOutputSamplingFrequency(info.sampleRate); + audio.setBitDepth(info.bps); + audio.setChannels((short) info.channels); + track = new MatroskaFileTrack(); + track.setCodecID("A_AAC"); + track.setAudio(audio); + track.setTrackType(MatroskaFileTrack.TrackType.AUDIO); + writer.addTrack(track); + return true; + } + + if (outputIndex >= 0) { + ByteBuffer output; + if (Build.VERSION.SDK_INT >= 21) + output = encoder.getOutputBuffer(outputIndex); + else + output = encoder.getOutputBuffers()[outputIndex]; + output.position(outputInfo.offset); + output.limit(outputInfo.offset + outputInfo.size); + old(outputInfo.presentationTimeUs / 1000); + if ((outputInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { + track.setCodecPrivate(clone(output)); + writer.flush(); + encoder.releaseOutputBuffer(outputIndex, false); + } else { + MatroskaFileFrame frame = new MatroskaFileFrame(); + frame.setKeyFrame((outputInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) == MediaCodec.BUFFER_FLAG_KEY_FRAME); + frame.setTimecode(outputInfo.presentationTimeUs / 1000); + frame.setTrackNo(track.getTrackNo()); + frame.setData(clone(output)); + encoder.releaseOutputBuffer(outputIndex, false); + old = frame; + } + } + + return true; + } + + void old(long cur) { + if (old != null) { + old.setDuration(cur - old.getTimecode()); + writer.addFrame(old); + writer.flush(); + old = null; + } + } + + public void close() { + end(); + encoder.release(); + writer.close(); + } + + long getCurrentTimeStamp() { + return NumSamples * 1000 * 1000 / info.sampleRate; + } + + public void end() { + if (input != null) { + queue(); + } + int inputIndex = encoder.dequeueInputBuffer(-1); + if (inputIndex >= 0) { + ByteBuffer input; + if (Build.VERSION.SDK_INT >= 21) + input = encoder.getInputBuffer(inputIndex); + else + input = encoder.getInputBuffers()[inputIndex]; + input.clear(); + encoder.queueInputBuffer(inputIndex, 0, 0, getCurrentTimeStamp(), MediaCodec.BUFFER_FLAG_END_OF_STREAM); + } + while (encode()) + ;// do encode() + old(getCurrentTimeStamp() / 1000); + writer.setDuration(getCurrentTimeStamp() / 1000); + encoder.stop(); + } + + public EncoderInfo getInfo() { + return info; + } +} diff --git a/app/src/main/java/com/github/axet/audiorecorder/encoders/MuxerMP4.java b/app/src/main/java/com/github/axet/audiorecorder/encoders/MuxerMP4.java index a0c2e2f..fc09bfd 100755 --- a/app/src/main/java/com/github/axet/audiorecorder/encoders/MuxerMP4.java +++ b/app/src/main/java/com/github/axet/audiorecorder/encoders/MuxerMP4.java @@ -113,7 +113,7 @@ public class MuxerMP4 implements Encoder { if (input == null) return; encoder.queueInputBuffer(inputIndex, 0, input.position(), getCurrentTimeStamp(), 0); - NumSamples += input.position() / info.channels; + NumSamples += input.position() / info.channels / (Short.SIZE / 8); input = null; while (encode()) ;// do encode() @@ -147,6 +147,7 @@ public class MuxerMP4 implements Encoder { } public void close() { + end(); encoder.release(); muxer.release(); } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7887f7d..f5421c6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -19,6 +19,7 @@ .wav (по умолчанию) .m4a + .mka diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 258dbd4..54f5883 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,11 +24,13 @@ .wav (default) .m4a + .mka wav m4a + mka diff --git a/settings.gradle b/settings.gradle index 817831f..c4f97a6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':android-library' +include ':app', ':android-library', ':jebml'