diff --git a/app/build.gradle b/app/build.gradle
index 16d603e..f11815b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,8 +8,8 @@ android {
applicationId "com.github.axet.audiorecorder"
minSdkVersion 16
targetSdkVersion 23
- versionCode 19
- versionName "1.0.18"
+ versionCode 20
+ versionName "1.0.19"
}
signingConfigs {
release {
diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java
index 5a025ad..cf90e04 100644
--- a/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java
+++ b/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java
@@ -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();
}
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 0746168..e9b4249 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
@@ -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;
+ }
}
diff --git a/app/src/main/java/com/github/axet/audiorecorder/app/RawSamples.java b/app/src/main/java/com/github/axet/audiorecorder/app/RawSamples.java
new file mode 100644
index 0000000..98fd528
--- /dev/null
+++ b/app/src/main/java/com/github/axet/audiorecorder/app/RawSamples.java
@@ -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);
+ }
+ }
+}
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 3384323..6773f33 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
@@ -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, "");
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 93fc1c1..99a249b 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
@@ -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);
diff --git a/app/src/main/java/com/github/axet/audiorecorder/widgets/PitchView.java b/app/src/main/java/com/github/axet/audiorecorder/widgets/PitchView.java
index c4a5146..d6e2a86 100644
--- a/app/src/main/java/com/github/axet/audiorecorder/widgets/PitchView.java
+++ b/app/src/main/java/com/github/axet/audiorecorder/widgets/PitchView.java
@@ -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();
+ }
+ }
}
diff --git a/app/src/main/res/drawable/ic_content_cut_24dp.xml b/app/src/main/res/drawable/ic_content_cut_24dp.xml
new file mode 100644
index 0000000..0a920da
--- /dev/null
+++ b/app/src/main/res/drawable/ic_content_cut_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml
index c749256..b84a65e 100644
--- a/app/src/main/res/layout/activity_recording.xml
+++ b/app/src/main/res/layout/activity_recording.xml
@@ -1,5 +1,5 @@
-
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:orientation="vertical">
+
+ android:layout_height="120dp"
+ android:layout_centerInParent="true" />
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file