add edit recording druing record
This commit is contained in:
parent
2cc104ae76
commit
b5e24f4207
8 changed files with 653 additions and 118 deletions
|
|
@ -450,6 +450,15 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr
|
|||
}
|
||||
}
|
||||
|
||||
void checkPending() {
|
||||
if(storage.recordingPending()) {
|
||||
Intent intent = new Intent(this, RecordingActivity.class);
|
||||
intent.setAction(RecordingActivity.START_PAUSE);
|
||||
startActivity(intent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// load recordings
|
||||
void load() {
|
||||
recordings.scan(storage.getStoragePath());
|
||||
|
|
@ -501,6 +510,8 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr
|
|||
else
|
||||
load();
|
||||
|
||||
checkPending();
|
||||
|
||||
final int selected = getLastRecording();
|
||||
list.setSelection(selected);
|
||||
if (selected != -1) {
|
||||
|
|
@ -539,6 +550,7 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr
|
|||
if (permitted(permissions)) {
|
||||
storage.migrateLocalStorage();
|
||||
load();
|
||||
checkPending();
|
||||
} else {
|
||||
Toast.makeText(this, "Not permitted", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ import android.content.Intent;
|
|||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Point;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.*;
|
||||
import android.preference.PreferenceManager;
|
||||
|
|
@ -25,15 +27,19 @@ import android.support.v7.app.AppCompatActivity;
|
|||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.axet.audiorecorder.R;
|
||||
import com.github.axet.audiorecorder.app.MainApplication;
|
||||
import com.github.axet.audiorecorder.app.RawSamples;
|
||||
import com.github.axet.audiorecorder.app.Storage;
|
||||
import com.github.axet.audiorecorder.encoders.Encoder;
|
||||
import com.github.axet.audiorecorder.encoders.EncoderInfo;
|
||||
|
|
@ -59,6 +65,7 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
public static final int NOTIFICATION_RECORDING_ICON = 0;
|
||||
public static String SHOW_ACTIVITY = RecordingActivity.class.getCanonicalName() + ".SHOW_ACTIVITY";
|
||||
public static String PAUSE = RecordingActivity.class.getCanonicalName() + ".PAUSE";
|
||||
public static String START_PAUSE = RecordingActivity.class.getCanonicalName() + ".START_PAUSE";
|
||||
|
||||
public static final String PHONE_STATE = "android.intent.action.PHONE_STATE";
|
||||
|
||||
|
|
@ -67,7 +74,9 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
Handler handle = new Handler();
|
||||
FileEncoder encoder;
|
||||
|
||||
boolean start = false;
|
||||
// do we need to start recording immidiatly?
|
||||
boolean start = true;
|
||||
|
||||
Thread thread;
|
||||
// dynamic buffer size. big for backgound recording. small for realtime view updates.
|
||||
Integer bufferSize = 0;
|
||||
|
|
@ -77,6 +86,16 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
int samplesUpdate;
|
||||
// output target file 2016-01-01 01.01.01.wav
|
||||
File targetFile;
|
||||
// how many samples passed for current recording
|
||||
long samplesTime;
|
||||
// current cut position in samples from begining of file
|
||||
long editSample = -1;
|
||||
// current sample index in edit mode while playing;
|
||||
long playIndex;
|
||||
// send ui update every 'playUpdate' samples.
|
||||
int playUpdate;
|
||||
|
||||
AudioTrack play;
|
||||
|
||||
TextView title;
|
||||
TextView time;
|
||||
|
|
@ -84,8 +103,6 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
ImageButton pause;
|
||||
PitchView pitch;
|
||||
|
||||
Runnable progress;
|
||||
|
||||
int soundMode;
|
||||
|
||||
Storage storage;
|
||||
|
|
@ -141,6 +158,8 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
state = (TextView) findViewById(R.id.recording_state);
|
||||
title = (TextView) findViewById(R.id.recording_title);
|
||||
|
||||
edit(false);
|
||||
|
||||
storage = new Storage(this);
|
||||
|
||||
try {
|
||||
|
|
@ -176,13 +195,14 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
sampleRate = Integer.parseInt(shared.getString(MainApplication.PREFERENCE_RATE, ""));
|
||||
|
||||
if (Build.VERSION.SDK_INT < 23 && isEmulator()) {
|
||||
// old emulators are not going to record on high sample rate.
|
||||
Toast.makeText(this, "Emulator Detected. Reducing Sample Rate to 8000 Hz", Toast.LENGTH_SHORT).show();
|
||||
sampleRate = 8000;
|
||||
}
|
||||
|
||||
updateBufferSize(false);
|
||||
|
||||
updateSamples(getSamples(storage.getTempRecording().length()));
|
||||
loadSamples();
|
||||
|
||||
View cancel = findViewById(R.id.recording_cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
|
|
@ -221,6 +241,42 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
String a = getIntent().getAction();
|
||||
if (a != null && a.equals(START_PAUSE)) {
|
||||
// pretend we already start it
|
||||
start = false;
|
||||
stopRecording("pause");
|
||||
}
|
||||
}
|
||||
|
||||
void loadSamples() {
|
||||
if (!storage.getTempRecording().exists())
|
||||
return;
|
||||
|
||||
RawSamples rs = new RawSamples(storage.getTempRecording());
|
||||
samplesTime = rs.getSamples();
|
||||
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
Point size = new Point();
|
||||
display.getSize(size);
|
||||
|
||||
int count = pitch.getMaxPitchCount(size.x);
|
||||
|
||||
short[] buf = new short[count * samplesUpdate];
|
||||
long cut = samplesTime - buf.length;
|
||||
|
||||
if (cut < 0)
|
||||
cut = 0;
|
||||
|
||||
rs.open(cut, buf.length);
|
||||
int len = rs.read(buf);
|
||||
pitch.clear(cut / samplesUpdate);
|
||||
for (int i = 0; i < len; i += samplesUpdate) {
|
||||
pitch.add(getPa(buf, i, samplesUpdate));
|
||||
}
|
||||
rs.close();
|
||||
updateSamples(samplesTime);
|
||||
}
|
||||
|
||||
boolean isEmulator() {
|
||||
|
|
@ -242,6 +298,13 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
if (thread != null) {
|
||||
stopRecording("pause");
|
||||
} else {
|
||||
if (editSample != -1) {
|
||||
RawSamples rs = new RawSamples(storage.getTempRecording());
|
||||
rs.trunk(editSample);
|
||||
loadSamples();
|
||||
edit(false);
|
||||
}
|
||||
|
||||
if (permitted(PERMISSIONS)) {
|
||||
resumeRecording();
|
||||
}
|
||||
|
|
@ -254,8 +317,8 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
Log.d(TAG, "onResume");
|
||||
|
||||
// start once
|
||||
if (start == false) {
|
||||
start = true;
|
||||
if (start) {
|
||||
start = false;
|
||||
if (permitted()) {
|
||||
record();
|
||||
}
|
||||
|
|
@ -282,6 +345,15 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
stopRecording();
|
||||
|
||||
showNotificationAlarm(true);
|
||||
|
||||
pitch.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
edit(true);
|
||||
editSample = pitch.edit(event.getX()) * samplesUpdate;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stopRecording() {
|
||||
|
|
@ -293,6 +365,96 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
unsilent();
|
||||
}
|
||||
|
||||
void edit(boolean b) {
|
||||
if (b) {
|
||||
state.setText("edit");
|
||||
|
||||
editPlay(false);
|
||||
|
||||
View box = findViewById(R.id.recording_edit_box);
|
||||
box.setVisibility(View.VISIBLE);
|
||||
|
||||
View cut = box.findViewById(R.id.recording_cut);
|
||||
cut.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
RawSamples rs = new RawSamples(storage.getTempRecording());
|
||||
rs.trunk(editSample);
|
||||
loadSamples();
|
||||
edit(false);
|
||||
}
|
||||
});
|
||||
|
||||
final ImageView playButton = (ImageView) box.findViewById(R.id.recording_play);
|
||||
playButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (play != null) {
|
||||
editPlay(false);
|
||||
return;
|
||||
}
|
||||
editPlay(true);
|
||||
}
|
||||
});
|
||||
|
||||
View done = box.findViewById(R.id.recording_edit_done);
|
||||
done.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
edit(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
editSample = -1;
|
||||
state.setText("pause");
|
||||
pitch.pause();
|
||||
View box = findViewById(R.id.recording_edit_box);
|
||||
box.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
void editPlay(boolean b) {
|
||||
View box = findViewById(R.id.recording_edit_box);
|
||||
final ImageView playButton = (ImageView) box.findViewById(R.id.recording_play);
|
||||
|
||||
if (b) {
|
||||
playButton.setImageResource(R.drawable.pause);
|
||||
|
||||
playIndex = editSample;
|
||||
|
||||
playUpdate = samplesUpdate;
|
||||
|
||||
RawSamples rs = new RawSamples(storage.getTempRecording());
|
||||
int len = (int) (rs.getSamples() - editSample);
|
||||
short[] buf = new short[len];
|
||||
rs.open(editSample, buf.length);
|
||||
int r = rs.read(buf);
|
||||
play = generateTrack(buf, r);
|
||||
play.play();
|
||||
play.setPositionNotificationPeriod(playUpdate);
|
||||
play.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
|
||||
@Override
|
||||
public void onMarkerReached(AudioTrack track) {
|
||||
editPlay(false);
|
||||
pitch.play(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeriodicNotification(AudioTrack track) {
|
||||
playIndex += playUpdate;
|
||||
long p = playIndex / samplesUpdate;
|
||||
pitch.play(p);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (play != null) {
|
||||
play.release();
|
||||
play = null;
|
||||
}
|
||||
playButton.setImageResource(R.drawable.play);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
cancelDialog(new Runnable() {
|
||||
|
|
@ -371,16 +533,12 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
Log.e(TAG, "Unable to set Thread Priority " + android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
|
||||
}
|
||||
|
||||
// how many samples passed
|
||||
long samplesTime;
|
||||
|
||||
DataOutputStream os = null;
|
||||
RawSamples rs = null;
|
||||
AudioRecord recorder = null;
|
||||
try {
|
||||
File tmp = storage.getTempRecording();
|
||||
samplesTime = getSamples(tmp.length());
|
||||
rs = new RawSamples(storage.getTempRecording());
|
||||
|
||||
os = new DataOutputStream(new BufferedOutputStream(storage.open(tmp)));
|
||||
rs.open(samplesTime);
|
||||
|
||||
int min = AudioRecord.getMinBufferSize(sampleRate, CHANNEL_CONFIG, AUDIO_FORMAT);
|
||||
if (min <= 0) {
|
||||
|
|
@ -417,22 +575,15 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
break;
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
for (int i = 0; i < readSize; i++) {
|
||||
try {
|
||||
os.writeShort(buffer[i]);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
sum += buffer[i] * buffer[i];
|
||||
}
|
||||
rs.write(buffer);
|
||||
|
||||
int pa = getPa(buffer, 0, readSize);
|
||||
|
||||
int amplitude = (int) (Math.sqrt(sum / readSize));
|
||||
int s = CHANNEL_CONFIG == AudioFormat.CHANNEL_IN_MONO ? readSize : readSize / 2;
|
||||
|
||||
samplesUpdateCount += s;
|
||||
if (samplesUpdateCount >= samplesUpdate) {
|
||||
pitch.add((int) (amplitude / (float) MAXIMUM_ALTITUDE * 100) + 1);
|
||||
pitch.add(pa);
|
||||
samplesUpdateCount -= samplesUpdate;
|
||||
}
|
||||
|
||||
|
|
@ -459,11 +610,8 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
} finally {
|
||||
if (os != null) {
|
||||
try {
|
||||
os.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
}
|
||||
if (recorder != null)
|
||||
recorder.release();
|
||||
|
|
@ -475,16 +623,6 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
showNotificationAlarm(true);
|
||||
}
|
||||
|
||||
long getSamples(long len) {
|
||||
if (AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT) {
|
||||
len = len / 2;
|
||||
}
|
||||
if (CHANNEL_CONFIG == AudioFormat.CHANNEL_IN_STEREO) {
|
||||
len = len / 2;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// calcuale buffer length dynamically, this way we can reduce thread cycles when activity in background
|
||||
// or phone screen is off.
|
||||
void updateBufferSize(boolean pause) {
|
||||
|
|
@ -505,6 +643,18 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
time.setText(MainApplication.formatDuration(ms));
|
||||
}
|
||||
|
||||
int getPa(short[] buffer, int offset, int len) {
|
||||
double sum = 0;
|
||||
for (int i = offset; i < offset + len; i++) {
|
||||
sum += buffer[i] * buffer[i];
|
||||
}
|
||||
|
||||
int amplitude = (int) (Math.sqrt(sum / len));
|
||||
int pa = (int) (amplitude / (float) MAXIMUM_ALTITUDE * 100) + 1;
|
||||
|
||||
return pa;
|
||||
}
|
||||
|
||||
// alarm dismiss button
|
||||
public void showNotificationAlarm(boolean show) {
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
|
@ -674,4 +824,27 @@ public class RecordingActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
private AudioTrack generateTrack(short[] buf, int len) {
|
||||
int end = len;
|
||||
|
||||
int c = 0;
|
||||
|
||||
if (CHANNEL_CONFIG == AudioFormat.CHANNEL_IN_MONO)
|
||||
c = AudioFormat.CHANNEL_OUT_MONO;
|
||||
|
||||
if (CHANNEL_CONFIG == AudioFormat.CHANNEL_IN_STEREO)
|
||||
c = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
|
||||
// old phones bug.
|
||||
// http://stackoverflow.com/questions/27602492
|
||||
//
|
||||
// with MODE_STATIC setNotificationMarkerPosition not called
|
||||
AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
|
||||
c, AUDIO_FORMAT,
|
||||
len * (Short.SIZE / 8), AudioTrack.MODE_STREAM);
|
||||
track.write(buf, 0, len);
|
||||
if (track.setNotificationMarkerPosition(end) != AudioTrack.SUCCESS)
|
||||
throw new RuntimeException("unable to set marker");
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
package com.github.axet.audiorecorder.app;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.util.Log;
|
||||
|
||||
import com.github.axet.audiorecorder.activities.RecordingActivity;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class RawSamples {
|
||||
|
||||
File in;
|
||||
|
||||
InputStream is;
|
||||
byte[] readBuffer;
|
||||
|
||||
OutputStream os;
|
||||
|
||||
public RawSamples(File in) {
|
||||
this.in = in;
|
||||
}
|
||||
|
||||
// open for writing with specified offset to truncate file
|
||||
public void open(long writeOffset) {
|
||||
try {
|
||||
os = new BufferedOutputStream(new FileOutputStream(in, true));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// open for reading
|
||||
//
|
||||
// bufReadSize - samples size
|
||||
public void open(int bufReadSize) {
|
||||
try {
|
||||
readBuffer = new byte[(RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1) * bufReadSize];
|
||||
is = new FileInputStream(in);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// open for read with initial offset and buffer read size
|
||||
//
|
||||
// offset - samples offset
|
||||
// bufReadSize - samples size
|
||||
public void open(long offset, int bufReadSize) {
|
||||
try {
|
||||
readBuffer = new byte[(RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1) * bufReadSize];
|
||||
is = new FileInputStream(in);
|
||||
is.skip(offset * (RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int read(short[] buf) {
|
||||
try {
|
||||
int len = is.read(readBuffer);
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
ByteBuffer.wrap(readBuffer, 0, len).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(buf, 0, (int) getSamples(len));
|
||||
return (int) getSamples(len);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(short val) {
|
||||
try {
|
||||
ByteBuffer bb = ByteBuffer.allocate(Short.SIZE / Byte.SIZE);
|
||||
bb.order(ByteOrder.BIG_ENDIAN);
|
||||
bb.putShort(val);
|
||||
os.write(bb.array(), 0, bb.limit());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(short[] buf) {
|
||||
for (int i = 0; i < buf.length; i++) {
|
||||
write(buf[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public long getSamples() {
|
||||
return getSamples(in.length());
|
||||
}
|
||||
|
||||
public long getSamples(long samples) {
|
||||
return samples / (RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1);
|
||||
}
|
||||
|
||||
public void trunk(long pos) {
|
||||
try {
|
||||
FileChannel outChan = new FileOutputStream(in, true).getChannel();
|
||||
outChan.truncate(pos * (RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1));
|
||||
outChan.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (is != null)
|
||||
is.close();
|
||||
is = null;
|
||||
|
||||
if (os != null)
|
||||
os.close();
|
||||
os = null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ public class Storage {
|
|||
return permitted(PERMISSIONS);
|
||||
}
|
||||
|
||||
public boolean recordingPending() {
|
||||
return getTempRecording().exists();
|
||||
}
|
||||
|
||||
public File getStoragePath() {
|
||||
SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String path = shared.getString(MainApplication.PREFERENCE_STORAGE, "");
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
package com.github.axet.audiorecorder.encoders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioFormat;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.axet.audiorecorder.activities.RecordingActivity;
|
||||
import com.github.axet.audiorecorder.app.RawSamples;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class FileEncoder {
|
||||
public static final String TAG = FileEncoder.class.getSimpleName();
|
||||
|
|
@ -39,30 +33,27 @@ public class FileEncoder {
|
|||
thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
samples = getSamples(in.length());
|
||||
|
||||
cur = 0;
|
||||
|
||||
FileInputStream is = null;
|
||||
RawSamples rs = new RawSamples(in);
|
||||
|
||||
samples = rs.getSamples();
|
||||
|
||||
short[] buf = new short[1000];
|
||||
|
||||
rs.open(buf.length);
|
||||
|
||||
try {
|
||||
is = new FileInputStream(in);
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
// temporary recording use global settings for encoding format.
|
||||
// take 1000 samples at once.
|
||||
byte[] buf = new byte[(RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1) * 1000];
|
||||
|
||||
int len = is.read(buf);
|
||||
long len = rs.read(buf);
|
||||
if (len <= 0) {
|
||||
handler.post(done);
|
||||
return;
|
||||
} else {
|
||||
short[] shorts = new short[len / 2];
|
||||
ByteBuffer.wrap(buf, 0, len).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(shorts);
|
||||
encoder.encode(shorts);
|
||||
encoder.encode(buf);
|
||||
handler.post(progress);
|
||||
synchronized (thread) {
|
||||
cur += getSamples(len);
|
||||
cur += len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,17 +61,10 @@ public class FileEncoder {
|
|||
Log.e(TAG, "Exception", e);
|
||||
t = e;
|
||||
handler.post(error);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Exception", e);
|
||||
t = e;
|
||||
handler.post(error);
|
||||
} finally {
|
||||
encoder.close();
|
||||
if (is != null) {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -88,10 +72,6 @@ public class FileEncoder {
|
|||
thread.start();
|
||||
}
|
||||
|
||||
long getSamples(long samples) {
|
||||
return samples / (RecordingActivity.AUDIO_FORMAT == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1);
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
synchronized (thread) {
|
||||
return (int) (cur * 100 / samples);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.content.Context;
|
|||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
|
|
@ -30,6 +32,10 @@ public class PitchView extends ViewGroup {
|
|||
|
||||
// update pitchview in milliseconds
|
||||
public static final int UPDATE_SPEED = 10;
|
||||
|
||||
// edit update time
|
||||
public static final int EDIT_UPDATE_SPEED = 250;
|
||||
|
||||
// 'pitch length' in milliseconds.
|
||||
// in other words how many milliseconds do we need to show whole pitch.
|
||||
int pitchTime;
|
||||
|
|
@ -53,13 +59,26 @@ public class PitchView extends ViewGroup {
|
|||
|
||||
long time = 0;
|
||||
|
||||
// how many samples were cut from 'data' list
|
||||
long samples = 0;
|
||||
|
||||
Runnable edit;
|
||||
// index
|
||||
int editPos = 0;
|
||||
int editCount = 0;
|
||||
int playPos = -1;
|
||||
|
||||
Runnable draw;
|
||||
Thread thread;
|
||||
|
||||
int pitchColor = 0xff0433AE;
|
||||
Paint cutColor = new Paint();
|
||||
int bg;
|
||||
|
||||
public class PitchGraphView extends SurfaceView implements SurfaceHolder.Callback {
|
||||
SurfaceHolder holder;
|
||||
Paint editPaint;
|
||||
Paint playPaint;
|
||||
|
||||
public PitchGraphView(Context context) {
|
||||
this(context, null);
|
||||
|
|
@ -72,12 +91,26 @@ public class PitchView extends ViewGroup {
|
|||
public PitchGraphView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
editPaint = new Paint();
|
||||
editPaint.setColor(Color.BLACK);
|
||||
editPaint.setStrokeWidth(pitchWidth);
|
||||
|
||||
playPaint = new Paint();
|
||||
playPaint.setColor(Color.BLUE);
|
||||
playPaint.setStrokeWidth(pitchWidth / 2);
|
||||
|
||||
getHolder().addCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int w = MeasureSpec.getSize(widthMeasureSpec);
|
||||
|
||||
pitchScreenCount = w / pitchSize + 1;
|
||||
|
||||
pitchMemCount = pitchScreenCount + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -86,11 +119,6 @@ public class PitchView extends ViewGroup {
|
|||
}
|
||||
|
||||
public void draw() {
|
||||
Canvas canvas = holder.lockCanvas(null);
|
||||
canvas.drawColor(bg);
|
||||
|
||||
int m = Math.min(pitchMemCount, data.size());
|
||||
|
||||
float offset = 0;
|
||||
|
||||
if (data.size() >= pitchMemCount) {
|
||||
|
|
@ -105,8 +133,9 @@ public class PitchView extends ViewGroup {
|
|||
if (data.size() > pitchMemCount + 1) {
|
||||
tick = 0;
|
||||
time = cur;
|
||||
data.subList(0, data.size() - pitchMemCount).clear();
|
||||
m = Math.min(pitchMemCount, data.size());
|
||||
int cut = data.size() - pitchMemCount;
|
||||
data.subList(0, cut).clear();
|
||||
samples += cut;
|
||||
}
|
||||
|
||||
if (tick > 1) {
|
||||
|
|
@ -118,12 +147,26 @@ public class PitchView extends ViewGroup {
|
|||
time = cur;
|
||||
}
|
||||
data.subList(0, 1).clear();
|
||||
m = Math.min(pitchMemCount, data.size());
|
||||
samples += 1;
|
||||
}
|
||||
|
||||
offset = pitchSize * tick;
|
||||
}
|
||||
|
||||
draw(offset);
|
||||
}
|
||||
|
||||
void draw(float offset) {
|
||||
Canvas canvas = holder.lockCanvas(null);
|
||||
|
||||
int m = Math.min(pitchMemCount, data.size());
|
||||
canvas.drawColor(bg);
|
||||
|
||||
// if (edit != null) {
|
||||
// float x = editPos * pitchSize + pitchSize / 2f;
|
||||
// canvas.drawRect(x, 0, getWidth(), getHeight(), bg_cut);
|
||||
// }
|
||||
|
||||
for (int i = 0; i < m; i++) {
|
||||
float left = data.get(i);
|
||||
float right = data.get(i);
|
||||
|
|
@ -132,26 +175,53 @@ public class PitchView extends ViewGroup {
|
|||
|
||||
float x = -offset + i * pitchSize + pitchSize / 2f;
|
||||
|
||||
canvas.drawLine(x, mid, x, mid - mid * left, paint);
|
||||
canvas.drawLine(x, mid, x, mid + mid * right, paint);
|
||||
Paint p = paint;
|
||||
|
||||
if (edit != null && i >= editPos)
|
||||
p = cutColor;
|
||||
|
||||
// left channel pitch
|
||||
canvas.drawLine(x, mid, x, mid - mid * left, p);
|
||||
// right channel pitch
|
||||
canvas.drawLine(x, mid, x, mid + mid * right, p);
|
||||
}
|
||||
|
||||
if (edit != null && editCount == 0) {
|
||||
float x = editPos * pitchSize + pitchSize / 2f;
|
||||
canvas.drawLine(x, 0, x, getHeight(), editPaint);
|
||||
}
|
||||
|
||||
if (edit != null && playPos != -1) {
|
||||
float x = playPos * pitchSize + pitchSize / 2f;
|
||||
canvas.drawLine(x, 0, x, getHeight(), playPaint);
|
||||
}
|
||||
|
||||
holder.unlockCanvasAndPost(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
this.holder = holder;
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
synchronized (PitchView.this) {
|
||||
this.holder = holder;
|
||||
fit();
|
||||
draw(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void surfaceCreated(final SurfaceHolder holder) {
|
||||
this.holder = holder;
|
||||
public void surfaceCreated(final SurfaceHolder holder) {
|
||||
synchronized (PitchView.this) {
|
||||
this.holder = holder;
|
||||
fit();
|
||||
draw(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
this.holder = null;
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
synchronized (PitchView.this) {
|
||||
this.holder = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,8 +241,8 @@ public class PitchView extends ViewGroup {
|
|||
super(context, attrs, defStyleAttr);
|
||||
|
||||
paint = new Paint();
|
||||
paint.setColor(0xff0433AE);
|
||||
paint.setStrokeWidth(pitchDlimiter);
|
||||
paint.setColor(pitchColor);
|
||||
paint.setStrokeWidth(pitchWidth);
|
||||
|
||||
getHolder().addCallback(this);
|
||||
}
|
||||
|
|
@ -182,10 +252,6 @@ public class PitchView extends ViewGroup {
|
|||
int w = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int h = Math.min(MeasureSpec.getSize(heightMeasureSpec), dp2px(pitchDlimiter + getPaddingTop() + getPaddingBottom()));
|
||||
|
||||
pitchScreenCount = w / pitchSize + 1;
|
||||
|
||||
pitchMemCount = pitchScreenCount + 1;
|
||||
|
||||
setMeasuredDimension(w, h);
|
||||
}
|
||||
|
||||
|
|
@ -195,39 +261,51 @@ public class PitchView extends ViewGroup {
|
|||
}
|
||||
|
||||
public void draw() {
|
||||
if (data.size() == 0)
|
||||
return;
|
||||
|
||||
Canvas canvas = holder.lockCanvas(null);
|
||||
canvas.drawColor(bg);
|
||||
|
||||
int end = data.size() - 1;
|
||||
if (data.size() > 0) {
|
||||
int end = data.size() - 1;
|
||||
|
||||
float left = data.get(end);
|
||||
float right = data.get(end);
|
||||
if (edit != null) {
|
||||
end = editPos;
|
||||
}
|
||||
|
||||
float mid = getWidth() / 2f;
|
||||
float left = data.get(end);
|
||||
float right = data.get(end);
|
||||
|
||||
float y = getHeight() / 2f;
|
||||
float mid = getWidth() / 2f;
|
||||
|
||||
float y = getHeight() / 2f;
|
||||
|
||||
canvas.drawLine(mid, y, mid - mid * left, y, paint);
|
||||
canvas.drawLine(mid, y, mid + mid * right, y, paint);
|
||||
}
|
||||
|
||||
canvas.drawLine(mid, y, mid - mid * left, y, paint);
|
||||
canvas.drawLine(mid, y, mid + mid * right, y, paint);
|
||||
holder.unlockCanvasAndPost(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void surfaceCreated(SurfaceHolder holder) {
|
||||
this.holder = holder;
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
synchronized (PitchView.this) {
|
||||
this.holder = holder;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
this.holder = holder;
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
synchronized (PitchView.this) {
|
||||
this.holder = holder;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
this.holder = null;
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
synchronized (PitchView.this) {
|
||||
this.holder = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,6 +331,7 @@ public class PitchView extends ViewGroup {
|
|||
pitchTime = pitchSize * UPDATE_SPEED;
|
||||
|
||||
bg = getThemeColor(android.R.attr.windowBackground);
|
||||
cutColor.setColor(0xff0443BE);//getThemeColor(android.R.attr.textColorPrimaryDisableOnly));
|
||||
|
||||
graph = new PitchGraphView(getContext());
|
||||
addView(graph);
|
||||
|
|
@ -274,16 +353,47 @@ public class PitchView extends ViewGroup {
|
|||
time = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getMaxPitchCount(int width) {
|
||||
int pitchScreenCount = width / pitchSize + 1;
|
||||
|
||||
int pitchMemCount = pitchScreenCount + 1;
|
||||
|
||||
return pitchMemCount;
|
||||
}
|
||||
|
||||
public void clear(long s) {
|
||||
data.clear();
|
||||
samples = s;
|
||||
edit = null;
|
||||
draw = null;
|
||||
}
|
||||
|
||||
public void fit() {
|
||||
if (data.size() > pitchMemCount) {
|
||||
int cut = data.size() - pitchMemCount;
|
||||
data.subList(0, cut).clear();
|
||||
samples += cut;
|
||||
}
|
||||
}
|
||||
|
||||
public void add(int a) {
|
||||
data.add(a / 100.0f);
|
||||
}
|
||||
|
||||
public void draw() {
|
||||
synchronized (graph) {
|
||||
synchronized (this) {
|
||||
if (graph.holder != null)
|
||||
graph.draw();
|
||||
if (current.holder != null)
|
||||
current.draw();
|
||||
}
|
||||
synchronized (current) {
|
||||
}
|
||||
|
||||
// draw in edit mode
|
||||
public void drawEdit() {
|
||||
synchronized (this) {
|
||||
if (graph.holder != null)
|
||||
graph.draw(0);
|
||||
if (current.holder != null)
|
||||
current.draw();
|
||||
}
|
||||
|
|
@ -337,9 +447,79 @@ public class PitchView extends ViewGroup {
|
|||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
if (edit != null)
|
||||
edit = null;
|
||||
if (draw != null)
|
||||
draw = null;
|
||||
|
||||
drawEdit();
|
||||
}
|
||||
|
||||
public long edit(float offset) {
|
||||
synchronized (this) {
|
||||
if (offset < 0)
|
||||
offset = 0;
|
||||
editPos = ((int) offset) / pitchSize;
|
||||
|
||||
if (editPos >= pitchScreenCount)
|
||||
editPos = pitchScreenCount - 1;
|
||||
|
||||
if (editPos >= data.size())
|
||||
editPos = data.size() - 1;
|
||||
|
||||
editCount = 0;
|
||||
drawEdit();
|
||||
}
|
||||
|
||||
if (draw != null) {
|
||||
draw = null;
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
if (thread == null) {
|
||||
edit = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
long time = System.currentTimeMillis();
|
||||
drawEdit();
|
||||
|
||||
editCount++;
|
||||
if (editCount > 1)
|
||||
editCount = 0;
|
||||
|
||||
long cur = System.currentTimeMillis();
|
||||
|
||||
long delay = EDIT_UPDATE_SPEED - (cur - time);
|
||||
|
||||
if (delay > 0) {
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
thread = new Thread(edit, TAG);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
return samples + editPos;
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
if (edit != null) {
|
||||
edit = null;
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
if (thread == null) {
|
||||
draw = new Runnable() {
|
||||
@Override
|
||||
|
|
@ -367,4 +547,15 @@ public class PitchView extends ViewGroup {
|
|||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void play(long pos) {
|
||||
synchronized (this) {
|
||||
playPos = (int) (pos - samples);
|
||||
|
||||
if (playPos < 0)
|
||||
playPos = -1;
|
||||
|
||||
drawEdit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_content_cut_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_content_cut_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFffffff"
|
||||
android:pathData="M9.64,7.64c0.23,-0.5 0.36,-1.05 0.36,-1.64 0,-2.21 -1.79,-4 -4,-4S2,3.79 2,6s1.79,4 4,4c0.59,0 1.14,-0.13 1.64,-0.36L10,12l-2.36,2.36C7.14,14.13 6.59,14 6,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4c0,-0.59 -0.13,-1.14 -0.36,-1.64L12,14l7,7h3v-1L9.64,7.64zM6,8c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zm0,12c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zm6,-7.5c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5 0.5,0.22 0.5,0.5 -0.22,0.5 -0.5,0.5zM19,3l-6,6 2,2 7,-7V3z"/>
|
||||
</vector>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.github.axet.audiorecorder.widgets.EqualLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
@ -11,9 +11,10 @@
|
|||
tools:context=".activities.RecordingActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recording_title"
|
||||
|
|
@ -32,14 +33,50 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<com.github.axet.audiorecorder.widgets.PitchView
|
||||
android:id="@+id/recording_pitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp" />
|
||||
android:layout_height="120dp"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<com.github.axet.audiorecorder.widgets.EqualLinearLayout
|
||||
android:id="@+id/recording_edit_box"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@id/recording_pitch"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/recording_cut"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/round"
|
||||
android:gravity="center"
|
||||
android:src="@drawable/ic_content_cut_24dp"
|
||||
android:text="Cancel" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/recording_play"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/round"
|
||||
android:src="@drawable/play" />
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/recording_edit_done"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/round"
|
||||
android:src="@drawable/ic_close_24dp" />
|
||||
|
||||
</com.github.axet.audiorecorder.widgets.EqualLinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
|
@ -82,4 +119,4 @@
|
|||
</com.github.axet.audiorecorder.widgets.EqualLinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</com.github.axet.audiorecorder.widgets.EqualLinearLayout>
|
||||
</RelativeLayout>
|
||||
Loading…
Add table
Add a link
Reference in a new issue