add edit recording druing record

This commit is contained in:
Alexey Kuznetsov 2016-03-25 14:04:30 +03:00
commit b5e24f4207
8 changed files with 653 additions and 118 deletions

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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, "");

View file

@ -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);

View file

@ -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();
}
}
}

View 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>

View file

@ -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>