From b5e24f42071ea2212d760effc5231469ec658440 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 25 Mar 2016 14:04:30 +0300 Subject: [PATCH 1/2] add edit recording druing record --- .../activities/MainActivity.java | 12 + .../activities/RecordingActivity.java | 251 +++++++++++++--- .../axet/audiorecorder/app/RawSamples.java | 129 +++++++++ .../axet/audiorecorder/app/Storage.java | 4 + .../audiorecorder/encoders/FileEncoder.java | 48 +--- .../axet/audiorecorder/widgets/PitchView.java | 271 +++++++++++++++--- .../main/res/drawable/ic_content_cut_24dp.xml | 9 + .../main/res/layout/activity_recording.xml | 47 ++- 8 files changed, 653 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/github/axet/audiorecorder/app/RawSamples.java create mode 100644 app/src/main/res/drawable/ic_content_cut_24dp.xml 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 From ccdaf087d6181f1f54ef65069f643e8f73f05f77 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 25 Mar 2016 14:04:33 +0300 Subject: [PATCH 2/2] Bump version 1.0.19 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 {