diff --git a/app/build.gradle b/app/build.gradle index 5e657ba..053dafb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.github.axet.audiorecorder" minSdkVersion 16 targetSdkVersion 23 - versionCode 54 - versionName "1.1.33" + versionCode 55 + versionName "1.1.34" } signingConfigs { release { @@ -36,5 +36,6 @@ dependencies { compile 'com.android.support:preference-v14:23.2.1' compile 'com.android.support:design:23.2.1' compile 'com.google.android.gms:play-services-appindexing:8.1.0' + compile 'org.apache.commons:commons-math3:3.6.1' compile project(":android-library") } diff --git a/app/src/androidTest/java/com/github/axet/audiorecorder/ApplicationTest.java b/app/src/androidTest/java/com/github/axet/audiorecorder/ApplicationTest.java index 9e55e4f..0e33c79 100644 --- a/app/src/androidTest/java/com/github/axet/audiorecorder/ApplicationTest.java +++ b/app/src/androidTest/java/com/github/axet/audiorecorder/ApplicationTest.java @@ -2,6 +2,9 @@ package com.github.axet.audiorecorder; import android.app.Application; import android.test.ApplicationTestCase; +import android.util.Log; + +import com.github.axet.audiorecorder.app.RawSamples; /** * Testing Fundamentals @@ -10,4 +13,7 @@ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } -} \ No newline at end of file + + public void testFFT() { + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d94f30..a753a6d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppThemeLight"> + android:theme="@style/AppThemeLight.NoActionBar"> diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/AppCompatPreferenceActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/AppCompatPreferenceActivity.java index 9b0052d..bc6ae27 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/AppCompatPreferenceActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/AppCompatPreferenceActivity.java @@ -12,6 +12,8 @@ import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import com.github.axet.audiorecorder.app.MainApplication; + /** * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls * to be used with AppCompat. @@ -22,6 +24,7 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity { @Override protected void onCreate(Bundle savedInstanceState) { + setTheme(((MainApplication) getApplication()).getUserTheme()); getDelegate().installViewFactory(); getDelegate().onCreate(savedInstanceState); super.onCreate(savedInstanceState); 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 896a5d7..03082ce 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 @@ -74,6 +74,7 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr Storage storage; ListView list; Handler handler; + PopupShareActionProvider shareProvider; public static void startActivity(Context context) { Intent i = new Intent(context, MainActivity.class); @@ -234,7 +235,7 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr share.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - PopupShareActionProvider shareProvider = new PopupShareActionProvider(getContext(), share); + shareProvider = new PopupShareActionProvider(getContext(), share); Intent emailIntent = new Intent(Intent.ACTION_SEND); emailIntent.setType("audio/mp4a-latm"); @@ -435,6 +436,9 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + setTheme(((MainApplication) getApplication()).getMainTheme()); + setContentView(R.layout.activity_main); // ATTENTION: This was auto-generated to implement the App Indexing API. @@ -533,19 +537,10 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr updateHeader(); final int selected = getLastRecording(); - list.setSelection(selected); if (selected != -1) { - handler.post(new Runnable() { - @Override - public void run() { - recordings.select(selected); - } - }); + list.setSelection(selected); + recordings.select(selected); } - final SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); - SharedPreferences.Editor edit = shared.edit(); - edit.putString(MainApplication.PREFERENCE_LAST, ""); - edit.commit(); } int getLastRecording() { @@ -556,8 +551,12 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr for (int i = 0; i < recordings.getCount(); i++) { File f = recordings.getItem(i); String n = f.getName().toLowerCase(); - if (n.equals(last)) + if (n.equals(last)) { + SharedPreferences.Editor edit = shared.edit(); + edit.putString(MainApplication.PREFERENCE_LAST, ""); + edit.commit(); return i; + } } return -1; } @@ -672,6 +671,6 @@ public class MainActivity extends AppCompatActivity implements AbsListView.OnScr long free = storage.getFree(f); long sec = storage.average(free); TextView text = (TextView) findViewById(R.id.space_left); - text.setText(((MainApplication)getApplication()).formatFree(free, sec)); + text.setText(((MainApplication) getApplication()).formatFree(free, sec)); } } 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 50eedb0..1670e24 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 @@ -147,6 +147,9 @@ public class RecordingActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + setTheme(((MainApplication) getApplication()).getUserTheme()); + setContentView(R.layout.activity_recording); pitch = (PitchView) findViewById(R.id.recording_pitch); diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java index b468dcd..4fa81ae 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java @@ -6,6 +6,7 @@ import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.media.Ringtone; @@ -23,6 +24,7 @@ import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; import android.view.MenuItem; +import android.widget.ListView; import android.widget.Toast; import com.github.axet.audiorecorder.R; @@ -41,7 +43,7 @@ import java.util.List; * href="http://developer.android.com/guide/topics/ui/settings.html">Settings * API Guide for more information on developing a Settings UI. */ -public class SettingsActivity extends AppCompatPreferenceActivity { +public class SettingsActivity extends AppCompatPreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { /** * A preference value change listener that updates the preference's summary * to reflect its new value. @@ -120,8 +122,12 @@ public class SettingsActivity extends AppCompatPreferenceActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setupActionBar(); + final SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(this); + shared.registerOnSharedPreferenceChangeListener(this); + getFragmentManager().beginTransaction().replace(android.R.id.content, new GeneralPreferenceFragment()).commit(); } @@ -195,6 +201,23 @@ public class SettingsActivity extends AppCompatPreferenceActivity { return true; } + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(MainApplication.PREFERENCE_THEME)) { + finish(); + startActivity(new Intent(this, SettingsActivity.class)); + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + final SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(this); + shared.unregisterOnSharedPreferenceChangeListener(this); + } + /** * This fragment shows general preferences only. It is used when the * activity is showing a two-pane settings UI. @@ -226,6 +249,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } bindPreferenceSummaryToValue(findPreference(MainApplication.PREFERENCE_RATE)); + bindPreferenceSummaryToValue(findPreference(MainApplication.PREFERENCE_THEME)); } @Override diff --git a/app/src/main/java/com/github/axet/audiorecorder/animations/RecordingAnimation.java b/app/src/main/java/com/github/axet/audiorecorder/animations/RecordingAnimation.java index 34ddddb..eb13e12 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/animations/RecordingAnimation.java +++ b/app/src/main/java/com/github/axet/audiorecorder/animations/RecordingAnimation.java @@ -3,6 +3,7 @@ package com.github.axet.audiorecorder.animations; import android.annotation.TargetApi; import android.os.Build; import android.os.Handler; +import android.util.Log; import android.view.View; import android.view.animation.Transformation; import android.widget.ListView; diff --git a/app/src/main/java/com/github/axet/audiorecorder/app/MainApplication.java b/app/src/main/java/com/github/axet/audiorecorder/app/MainApplication.java index 793d3fd..fdf7213 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/app/MainApplication.java +++ b/app/src/main/java/com/github/axet/audiorecorder/app/MainApplication.java @@ -1,9 +1,14 @@ package com.github.axet.audiorecorder.app; import android.app.Application; +import android.content.Context; import android.content.SharedPreferences; +import android.content.res.TypedArray; import android.preference.PreferenceManager; +import android.util.Log; +import android.util.TypedValue; +import com.github.axet.androidlibrary.widgets.ThemeUtils; import com.github.axet.audiorecorder.R; public class MainApplication extends Application { @@ -13,12 +18,37 @@ public class MainApplication extends Application { public static final String PREFERENCE_SILENT = "silence"; public static final String PREFERENCE_ENCODING = "encoding"; public static final String PREFERENCE_LAST = "last_recording"; + public static final String PREFERENCE_THEME = "theme"; @Override public void onCreate() { super.onCreate(); PreferenceManager.setDefaultValues(this, R.xml.pref_general, false); + + Context context = this; + context.setTheme(getUserTheme()); + Log.d("123", "color " + Integer.toHexString(ThemeUtils.getThemeColor(context, android.R.attr.textColorSecondary))); + } + + public int getUserTheme() { + final SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(this); + String theme = shared.getString(MainApplication.PREFERENCE_THEME, ""); + if (theme.equals("Theme_Dark")) { + return R.style.AppThemeDark; + } else { + return R.style.AppThemeLight; + } + } + + public int getMainTheme() { + final SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(this); + String theme = shared.getString(MainApplication.PREFERENCE_THEME, ""); + if (theme.equals("Theme_Dark")) { + return R.style.AppThemeDark_NoActionBar; + } else { + return R.style.AppThemeLight_NoActionBar; + } } static public String formatTime(int tt) { 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 index 79af854..6e704d1 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/app/RawSamples.java +++ b/app/src/main/java/com/github/axet/audiorecorder/app/RawSamples.java @@ -5,6 +5,12 @@ import android.util.Log; import com.github.axet.audiorecorder.activities.RecordingActivity; +import org.apache.commons.math3.complex.Complex; +import org.apache.commons.math3.transform.DftNormalization; +import org.apache.commons.math3.transform.FastFourierTransformer; +import org.apache.commons.math3.transform.TransformType; +import org.apache.commons.math3.util.MathArrays; + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; @@ -137,7 +143,7 @@ public class RawSamples { public static double getDB(double amplitude) { // https://en.wikipedia.org/wiki/Sound_pressure - return 20.0 * Math.log10(amplitude / 32768d); + return 20.0 * Math.log10(amplitude / 0x7FFF); } public void close() { diff --git a/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java b/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java index adfb693..b05f623 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java +++ b/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java @@ -5,8 +5,11 @@ import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Build; import android.os.IBinder; import android.support.annotation.Nullable; @@ -16,6 +19,7 @@ import android.widget.RemoteViews; import com.github.axet.audiorecorder.R; import com.github.axet.audiorecorder.activities.RecordingActivity; +import com.github.axet.audiorecorder.app.MainApplication; /** * RecordingActivity more likly to be removed from memory when paused then service. Notification button @@ -140,6 +144,19 @@ public class RecordingService extends Service { view.setOnClickPendingIntent(R.id.notification_pause, pe); view.setImageViewResource(R.id.notification_pause, !recording ? R.drawable.play : R.drawable.pause); + getBaseContext().setTheme(((MainApplication) getApplication()).getUserTheme()); + + view.apply(new ContextWrapper(getBaseContext()) { + public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException { + return new ContextWrapper(getBaseContext().createPackageContext(packageName, flags)) { + @Override + public Resources.Theme getTheme() { + return getBaseContext().getTheme(); + } + }; + } + }, null); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setOngoing(true) .setContentTitle("Recording") diff --git a/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTBarView.java b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTBarView.java new file mode 100644 index 0000000..a2bee4b --- /dev/null +++ b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTBarView.java @@ -0,0 +1,105 @@ +package com.github.axet.audiorecorder.widgets; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import com.github.axet.audiorecorder.app.RawSamples; + +public class FFTBarView extends FFTView { + public static final String TAG = FFTBarView.class.getSimpleName(); + + int barCount; + float barWidth; + float barDeli; + + public FFTBarView(Context context) { + this(context, null); + } + + public FFTBarView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FFTBarView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + create(); + } + + void create() { + super.create(); + } + + public void setBuffer(double[] buf) { + super.setBuffer(buf); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // set initial width + int w = dp2px(15); + int d = dp2px(4); + int s = w + d; + + int mw = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + + // get count of bars and delimeters + int dc = (mw - w) / s; + int bc = dc + 1; + + // get rate + float k = w / d; + + // get one part of (bar+del) size + float e = mw / (bc * k + dc); + + barCount = bc; + barWidth = e * k; + barDeli = e; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + + @Override + public void onDraw(Canvas canvas) { + if (barCount == 0) + return; + + int h = getHeight() - getPaddingTop() - getPaddingBottom(); + + float left = getPaddingLeft(); + + for (int i = 0; i < barCount; i++) { + double max = 0; + + if (buffer != null) { + int step = buffer.length / barCount; + int offset = i * step; + int end = Math.min(offset + step, buffer.length); + for (int k = offset; k < end; k++) { + double s = buffer[k]; + max = Math.max(max, s); + } + } + + float y = getPaddingTop() + h - h * ((float) max / 0x7fff) - dp2px(1); + + if (y < getPaddingTop()) + y = getPaddingTop(); + + canvas.drawRect(left, y, left + barWidth, getPaddingTop() + h, paint); + left += barWidth + barDeli; + } + } + +} diff --git a/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTChartView.java b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTChartView.java new file mode 100644 index 0000000..dfcd50e --- /dev/null +++ b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTChartView.java @@ -0,0 +1,94 @@ +package com.github.axet.audiorecorder.widgets; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import com.github.axet.audiorecorder.app.RawSamples; + +public class FFTChartView extends FFTView { + public static final String TAG = FFTChartView.class.getSimpleName(); + + public FFTChartView(Context context) { + this(context, null); + } + + public FFTChartView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FFTChartView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + create(); + } + + void create() { + super.create(); + } + + public void setBuffer(double[] buf) { + super.setBuffer(buf); + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + + @Override + public void onDraw(Canvas canvas) { + if (buffer == null) + return; + + canvas.drawColor(Color.RED); + + int h = getHeight(); + + float startX = 0, startY = h; + + int w = getWidth() - getPaddingLeft() - getPaddingRight(); + + float step = w / (float) buffer.length; + + double min = Integer.MAX_VALUE; + double max = Integer.MIN_VALUE; + + for (int i = 0; i < buffer.length; i++) { + double v = buffer[i]; + + min = Math.min(v, min); + max = Math.max(v, max); + + v = (RawSamples.MAXIMUM_DB + v) / RawSamples.MAXIMUM_DB; + + float endX = startX; + float endY = (float) (h - h * v); + + canvas.drawLine(startX, startY, endX, endY, paint); + + startX = endX + step; + startY = endY; + } + + String tMin = "" + min; + canvas.drawText(tMin, 0, getHeight(), textPaint); + + String tMax = "" + max; + textPaint.getTextBounds(tMax, 0, tMax.length(), textBounds); + canvas.drawText("" + max, w - textBounds.width(), getHeight(), textPaint); + } + +} diff --git a/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTView.java b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTView.java new file mode 100644 index 0000000..3a57222 --- /dev/null +++ b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTView.java @@ -0,0 +1,151 @@ +package com.github.axet.audiorecorder.widgets; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import com.github.axet.audiorecorder.app.RawSamples; + +import org.apache.commons.math3.complex.Complex; +import org.apache.commons.math3.transform.DftNormalization; +import org.apache.commons.math3.transform.FastFourierTransformer; +import org.apache.commons.math3.transform.TransformType; + +public class FFTView extends View { + public static final String TAG = FFTView.class.getSimpleName(); + + Paint paint; + double[] buffer; + + Paint textPaint; + Rect textBounds; + + public FFTView(Context context) { + this(context, null); + } + + public FFTView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FFTView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + create(); + } + + void create() { + paint = new Paint(); + paint.setColor(0xff0433AE); + paint.setStrokeWidth(dp2px(1)); + + textBounds = new Rect(); + + textPaint = new Paint(); + textPaint.setColor(Color.GRAY); + textPaint.setAntiAlias(true); + textPaint.setTextSize(20f); + + if (isInEditMode()) { + short[] b = simple(); + b = generateSound(16000, 4000, 100); + buffer = fft(b, 0, b.length); + //buffer = RawSamples.generateSound(16000, 4000, 100); + //buffer = RawSamples.fft(buffer, 0, buffer.length); + } + } + + public void setBuffer(double[] buf) { + buffer = buf; + } + + public static short[] generateSound(int sampleRate, int freqHz, int durationMs) { + int count = sampleRate * durationMs / 1000; + short[] samples = new short[count]; + for (int i = 0; i < count; i++) { + short sample = (short) (Math.sin(2 * Math.PI * i / (sampleRate / freqHz)) * 0x7FFF); + samples[i] = sample; + } + return samples; + } + + public static double[] asDouble(short[] buffer, int offset, int len) { + double[] dd = new double[len]; + for (int i = 0; i < len; i++) { + dd[i] = buffer[offset + i] / (float) 0x7fff; + } + return dd; + } + + public static double[] fft(short[] buffer, int offset, int len) { + int len2 = (int) Math.pow(2, Math.ceil(Math.log(len) / Math.log(2))); + + final double[][] dataRI = new double[][]{ + new double[len2], new double[len2] + }; + + double[] dataR = dataRI[0]; + double[] dataI = dataRI[1]; + + double powerInput = 0; + for (int i = 0; i < len; i++) { + dataR[i] = buffer[offset + i] / (float) 0x7fff; + powerInput += dataR[i] * dataR[i]; + } + powerInput = Math.sqrt(powerInput / len); + + FastFourierTransformer.transformInPlace(dataRI, DftNormalization.STANDARD, TransformType.FORWARD); + + double[] data = new double[len2 / 2]; + + data[0] = 10 * Math.log10(Math.pow(new Complex(dataR[0], dataI[0]).abs() / len2, 2)); + + double powerOutput = 0; + for (int i = 1; i < data.length; i++) { + Complex c = new Complex(dataR[i], dataI[i]); + double p = c.abs(); + p = p / len2; + p = p * p; + p = p * 2; + double dB = 10 * Math.log10(p); + + powerOutput += p; + data[i] = dB; + } + powerOutput = Math.sqrt(powerOutput); + +// if(powerInput != powerOutput) { +// throw new RuntimeException("in " + powerInput + " out " + powerOutput); +// } + + return data; + } + + public static short[] simple() { + int sampleRate = 1000; + int count = sampleRate; + short[] samples = new short[count]; + for (int i = 0; i < count; i++) { + double x = i / (double) count; + double y = 0; + //y += 0.6 * Math.sin(20 * 2 * Math.PI * x); + //y += 0.4 * Math.sin(50 * 2 * Math.PI * x); + //y += 0.2 * Math.sin(80 * 2 * Math.PI * x); + y += Math.sin(100 * 2 * Math.PI * x); + y += Math.sin(200 * 2 * Math.PI * x); + y += Math.sin(300 * 2 * Math.PI * x); + // max = 2.2; + samples[i] = (short) (y / 3 * 0x7fff); + } + return samples; + } + + int dp2px(float dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); + } + +} 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 ccd664c..272bdf4 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 @@ -2,6 +2,7 @@ package com.github.axet.audiorecorder.widgets; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -15,6 +16,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.github.axet.androidlibrary.widgets.ThemeUtils; +import com.github.axet.audiorecorder.R; +import com.github.axet.audiorecorder.app.MainApplication; import com.github.axet.audiorecorder.app.RawSamples; import java.util.LinkedList; @@ -39,8 +43,6 @@ public class PitchView extends ViewGroup { // in other words how many milliseconds do we need to show whole pitch. int pitchTime; - Paint paint; - Paint paintRed; List data = new LinkedList<>(); // how many pitches we can fit on screen @@ -75,12 +77,54 @@ public class PitchView extends ViewGroup { Handler handler; - int pitchColor = 0xff0433AE; - Paint cutColor = new Paint(); + public static class HandlerUpdate implements Runnable { + long start; + long updateSpeed; + Handler handler; + Runnable run; + + public static HandlerUpdate start(Handler handler, Runnable run, long updateSpeed) { + HandlerUpdate r = new HandlerUpdate(); + r.run = run; + r.start = System.currentTimeMillis(); + r.updateSpeed = updateSpeed; + r.handler = handler; + // post instead of draw.run() so 'start' will measure actual queue time + handler.postDelayed(r, updateSpeed); + return r; + } + + public static void stop(Handler handler, Runnable run) { + handler.removeCallbacks(run); + } + + @Override + public void run() { + this.run.run(); + + long cur = System.currentTimeMillis(); + + long diff = cur - start; + + start = cur; + + long delay = updateSpeed + (updateSpeed - diff); + if (delay > updateSpeed) + delay = updateSpeed; + + if (delay > 0) + this.handler.postDelayed(this, delay); + else + this.handler.post(this); + } + } public class PitchGraphView extends View { + Paint paint; + Paint paintRed; Paint editPaint; Paint playPaint; + Paint cutColor; public PitchGraphView(Context context) { this(context, null); @@ -93,12 +137,24 @@ public class PitchView extends ViewGroup { public PitchGraphView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + paint = new Paint(); + paint.setColor(getThemeColor(R.attr.colorPrimary)); + paint.setStrokeWidth(pitchWidth); + + paintRed = new Paint(); + paintRed.setColor(Color.RED); + paintRed.setStrokeWidth(pitchWidth); + + cutColor = new Paint(); + cutColor.setColor(getThemeColor(android.R.attr.textColorHint)); + cutColor.setStrokeWidth(pitchWidth); + editPaint = new Paint(); - editPaint.setColor(Color.BLACK); + editPaint.setColor(getThemeColor(R.attr.colorPrimaryDark)); editPaint.setStrokeWidth(pitchWidth); playPaint = new Paint(); - playPaint.setColor(Color.BLUE); + playPaint.setColor(getThemeColor(R.attr.colorPrimaryDark)); playPaint.setStrokeWidth(pitchWidth / 2); } @@ -141,8 +197,7 @@ public class PitchView extends ViewGroup { tick = 0; time = cur; } - data.subList(0, 1).clear(); - samples += 1; + fit(data.size() - 1); } offset = pitchSize * tick; @@ -192,7 +247,7 @@ public class PitchView extends ViewGroup { } // paint play mark - if (playPos != -1) { + if (playPos > 0) { float x = playPos * pitchSize + pitchSize / 2f; canvas.drawLine(x, 0, x, getHeight(), playPaint); } @@ -205,6 +260,8 @@ public class PitchView extends ViewGroup { String text; Rect textBounds; + double dB; + public PitchCurrentView(Context context) { this(context, null); } @@ -225,19 +282,19 @@ public class PitchView extends ViewGroup { textPaint.setTextSize(20f); paint = new Paint(); - paint.setColor(pitchColor); + paint.setColor(getThemeColor(R.attr.colorPrimary)); paint.setStrokeWidth(pitchWidth); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); - int h = 0; - textPaint.getTextBounds(this.text, 0, this.text.length(), textBounds); + + int h = getPaddingTop(); h += textBounds.height(); h += dp2px(2); - h += dp2px(pitchDlimiter) + getPaddingTop() + getPaddingBottom(); + h += pitchWidth + getPaddingBottom(); setMeasuredDimension(w, h); } @@ -252,20 +309,9 @@ public class PitchView extends ViewGroup { super.onLayout(changed, left, top, right, bottom); } - public int getEnd() { - int end = data.size() - 1; + void update(int end) { + dB = getDB(end) / RawSamples.MAXIMUM_DB; - if (editPos != -1) { - end = editPos; - } - if (playPos != -1) { - end = (int) playPos; - } - - return end; - } - - void updateText(int end) { String str = ""; str = Integer.toString((int) getDB(end)) + " dB"; @@ -276,29 +322,25 @@ public class PitchView extends ViewGroup { @Override public void onDraw(Canvas canvas) { if (data.size() > 0) { - int end = getEnd(); - - updateText(end); - - float y = getPaddingTop() + textBounds.height(); - - int x = getWidth() / 2 - textBounds.width() / 2; - canvas.drawText(text, x, y, textPaint); - - y += dp2px(2); - - double dB = getDB(end) / RawSamples.MAXIMUM_DB; - - float left = (float) dB; - float right = (float) dB; - - float mid = getWidth() / 2f; - - y = y + dp2px(pitchDlimiter) / 2; - - canvas.drawLine(mid, y, mid - mid * left - 1, y, paint); - canvas.drawLine(mid, y, mid + mid * right + 1, y, paint); + current.update(getEnd()); } + + float y = getPaddingTop() + textBounds.height(); + + int x = getWidth() / 2 - textBounds.width() / 2; + canvas.drawText(text, x, y, textPaint); + + y += dp2px(2); + + float left = (float) dB; + float right = (float) dB; + + float mid = getWidth() / 2f; + + y += pitchWidth / 2; + + canvas.drawLine(mid, y, mid - mid * left - 1, y, paint); + canvas.drawLine(mid, y, mid + mid * right + 1, y, paint); } } @@ -325,33 +367,42 @@ 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); +// fft = new FFTChartView(getContext()) { +// @Override +// public void onDraw(Canvas canvas) { +// if (data.size() > 0) { +// short[] buf = dataSamples.get(getEnd()); +// double[] d = FFTView.fft(buf, 0, buf.length); +// //double[] d = asDouble(buf, 0, buf.length); +// fft.setBuffer(d); +// } +// +// super.onDraw(canvas); +// } +// }; +// fft.setPadding(0, dp2px(2), 0, 0); +// addView(fft); + current = new PitchCurrentView(getContext()); current.setPadding(0, dp2px(2), 0, 0); addView(current); if (isInEditMode()) { for (int i = 0; i < 3000; i++) { - data.add((Math.random() * RawSamples.MAXIMUM_DB)); + data.add(-Math.sin(i) * RawSamples.MAXIMUM_DB); } } - paint = new Paint(); - paint.setColor(0xff0433AE); - paint.setStrokeWidth(pitchWidth); - - paintRed = new Paint(); - paintRed.setColor(Color.RED); - paintRed.setStrokeWidth(pitchWidth); - time = System.currentTimeMillis(); } + public int getThemeColor(int id) { + return ThemeUtils.getThemeColor(getContext(), id); + } + public int getMaxPitchCount(int width) { int pitchScreenCount = width / pitchSize + 1; @@ -390,8 +441,7 @@ public class PitchView extends ViewGroup { public void drawCalc() { graph.calc(); - graph.invalidate(); - current.invalidate(); + draw(); } public void drawEnd() { @@ -400,6 +450,19 @@ public class PitchView extends ViewGroup { draw(); } + public int getEnd() { + int end = data.size() - 1; + + if (editPos != -1) { + end = editPos; + } + if (playPos > 0) { + end = (int) playPos; + } + + return end; + } + public double getDB(int i) { double db = data.get(i); @@ -433,56 +496,46 @@ public class PitchView extends ViewGroup { return pitchTime; } - int getThemeColor(int id) { - TypedValue typedValue = new TypedValue(); - Context context = getContext(); - Resources.Theme theme = context.getTheme(); - if (theme.resolveAttribute(id, typedValue, true)) { - if (Build.VERSION.SDK_INT >= 23) - return context.getResources().getColor(typedValue.resourceId, theme); - else - return context.getResources().getColor(typedValue.resourceId); - } else { - return Color.TRANSPARENT; - } - } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - graph.measure(widthMeasureSpec, heightMeasureSpec); - current.measure(widthMeasureSpec, heightMeasureSpec); + int ww = getMeasuredWidth() - getPaddingRight() - getPaddingLeft(); + int hh = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + current.measure(MeasureSpec.makeMeasureSpec(ww, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(hh, MeasureSpec.AT_MOST)); + + hh = hh - current.getMeasuredHeight(); + + graph.measure(MeasureSpec.makeMeasureSpec(ww, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(hh, MeasureSpec.AT_MOST)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - int gb = graph.getMeasuredHeight() - current.getMeasuredHeight(); - graph.layout(0, 0, graph.getMeasuredWidth(), gb); - current.layout(0, gb, current.getMeasuredWidth(), gb + current.getMeasuredHeight()); + graph.layout(getPaddingLeft(), getPaddingTop(), + getPaddingLeft() + graph.getMeasuredWidth(), getPaddingTop() + graph.getMeasuredHeight()); + + current.layout(getPaddingLeft(), graph.getBottom(), + getPaddingLeft() + current.getMeasuredWidth(), graph.getBottom() + current.getMeasuredHeight()); } int dp2px(float dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } - @Override - protected void onDraw(Canvas canvas) { - graph.draw(canvas); - current.draw(canvas); - } - public void stop() { if (edit != null) - handler.removeCallbacks(edit); + HandlerUpdate.stop(handler, edit); edit = null; if (draw != null) - handler.removeCallbacks(draw); + HandlerUpdate.stop(handler, draw); draw = null; if (play != null) - handler.removeCallbacks(play); + HandlerUpdate.stop(handler, play); play = null; draw(); @@ -503,12 +556,12 @@ public class PitchView extends ViewGroup { editPos = data.size() - 1; if (draw != null) { - handler.removeCallbacks(draw); + HandlerUpdate.stop(handler, draw); draw = null; } if (play != null) { - handler.removeCallbacks(play); + HandlerUpdate.stop(handler, play); play = null; } @@ -523,92 +576,45 @@ public class PitchView extends ViewGroup { if (edit == null) { editFlash = true; - edit = new Runnable() { - long start = System.currentTimeMillis(); - + edit = HandlerUpdate.start(handler, new Runnable() { @Override public void run() { draw(); - editFlash = !editFlash; - - long cur = System.currentTimeMillis(); - - long diff = cur - start; - - long delay = EDIT_UPDATE_SPEED + (EDIT_UPDATE_SPEED - diff); - if (delay > EDIT_UPDATE_SPEED) - delay = EDIT_UPDATE_SPEED; - - start = cur; - - if (delay > 0) - handler.postDelayed(edit, delay); - else - handler.post(edit); } - }; - // post instead of draw.run() so 'start' will measure actual queue time - handler.postDelayed(edit, EDIT_UPDATE_SPEED); + }, EDIT_UPDATE_SPEED); } } public void record() { if (edit != null) - handler.removeCallbacks(edit); + HandlerUpdate.stop(handler, edit); edit = null; editPos = -1; if (play != null) - handler.removeCallbacks(play); + HandlerUpdate.stop(handler, play); play = null; playPos = -1; if (draw == null) { time = System.currentTimeMillis(); - draw = new Runnable() { - long start = System.currentTimeMillis(); - int stableCount = 0; - + draw = HandlerUpdate.start(handler, new Runnable() { @Override public void run() { drawCalc(); - long cur = System.currentTimeMillis(); - - long diff = cur - start; - - long delay = UPDATE_SPEED + (UPDATE_SPEED - diff); - if (delay > UPDATE_SPEED) - delay = UPDATE_SPEED; - - start = cur; - - if (delay > 0) - handler.postDelayed(draw, delay); - else - handler.post(draw); } - }; - // post instead of draw.run() so 'start' will measure actual queue time - handler.postDelayed(draw, UPDATE_SPEED); + }, UPDATE_SPEED); } } // current paying pos in actual samples public void play(float pos) { - playPos = pos - samples; - - editFlash = true; - - int max = data.size() - 1; - - if (playPos < 0 || playPos > max) + if (pos < 0) { playPos = -1; - - if (playPos < 0) { if (play != null) { - handler.removeCallbacks(play); + HandlerUpdate.stop(handler, play); play = null; } if (edit == null) { @@ -617,40 +623,31 @@ public class PitchView extends ViewGroup { return; } + playPos = pos - samples; + + editFlash = true; + + int max = data.size() - 1; + + if (playPos > max) + playPos = max; + if (edit != null) - handler.removeCallbacks(edit); + HandlerUpdate.stop(handler, edit); edit = null; if (draw != null) - handler.removeCallbacks(draw); + HandlerUpdate.stop(handler, draw); draw = null; if (play == null) { time = System.currentTimeMillis(); - play = new Runnable() { - long start = System.currentTimeMillis(); - + play = HandlerUpdate.start(handler, new Runnable() { @Override public void run() { draw(); - long cur = System.currentTimeMillis(); - - long diff = cur - start; - - start = cur; - - long delay = UPDATE_SPEED + (UPDATE_SPEED - diff); - if (delay > UPDATE_SPEED) - delay = UPDATE_SPEED; - - if (delay > 0) - handler.postDelayed(play, delay); - else - handler.post(play); } - }; - // post instead of draw.run() so 'start' will measure actual queue time - handler.postDelayed(play, UPDATE_SPEED); + }, UPDATE_SPEED); } } } diff --git a/app/src/main/res/drawable/round.xml b/app/src/main/res/drawable/round.xml deleted file mode 100644 index 5589315..0000000 --- a/app/src/main/res/drawable/round.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6700b32..e6f67ec 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -10,14 +10,14 @@ + android:theme="@style/AppThemeLight.AppBarOverlay"> + app:popupTheme="@style/AppThemeLight.PopupOverlay" /> diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml index 70748fb..1bb525a 100644 --- a/app/src/main/res/layout/activity_recording.xml +++ b/app/src/main/res/layout/activity_recording.xml @@ -38,7 +38,8 @@ android:id="@+id/recording_pitch" android:layout_width="match_parent" android:layout_height="120dp" - android:layout_centerInParent="true" /> + android:layout_centerInParent="true" + android:padding="5dp" /> - - - - @@ -96,28 +93,24 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - - - diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 63256d6..8d13482 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -43,7 +43,7 @@ diff --git a/app/src/main/res/layout/notifictaion_recording.xml b/app/src/main/res/layout/notifictaion_recording.xml index 0e921aa..7caff47 100644 --- a/app/src/main/res/layout/notifictaion_recording.xml +++ b/app/src/main/res/layout/notifictaion_recording.xml @@ -3,13 +3,13 @@ android:id="@+id/status_bar_latest_event_content" android:layout_width="match_parent" android:layout_height="64dp" - android:background="@android:color/white"> + android:background="?android:windowBackground"> + android:tint="?android:attr/colorForeground" /> + android:textColor="?android:attr/colorForeground" /> diff --git a/app/src/main/res/layout/recording.xml b/app/src/main/res/layout/recording.xml index 19391cb..461335b 100644 --- a/app/src/main/res/layout/recording.xml +++ b/app/src/main/res/layout/recording.xml @@ -70,8 +70,8 @@ android:layout_marginBottom="5dp" android:layout_marginLeft="5dp" android:layout_marginRight="5dp" - android:background="#dedede" android:orientation="vertical" + android:background="?attr/secondBackground" android:padding="5dp"> + android:tint="?attr/colorAccent" /> + android:tint="?attr/colorAccent" /> + android:tint="?attr/colorAccent" /> diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 251fb9f..d7d6ad1 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -1,6 +1,11 @@ -> - - + - - + + diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index a031aa0..d4d59d5 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -1,4 +1,5 @@ - + + + diff --git a/docs/fft.py b/docs/fft.py new file mode 100644 index 0000000..a3547ea --- /dev/null +++ b/docs/fft.py @@ -0,0 +1,80 @@ +import numpy as np +import matplotlib.pyplot as plt +import scipy.fftpack +import random + +# +# https://web.archive.org/web/20120615002031/http://www.mathworks.com/support/tech-notes/1700/1702.html +# + +def noise(y, amp): + return y + amp * np.random.sample(len(y)) + +# Fe = sample rate +# N = samples count +def plot(Fe, N, x, y): + plt.subplot(2, 1, 1) + print "power wav = %s" % np.sqrt(np.mean(y**2)) + plt.plot(x, y) + + plt.subplot(2, 1, 2) + yf = scipy.fftpack.fft(y) + + NumUniquePts = np.ceil((N+1)/2) + # Bin 0 contains the value for the DC component that the signal is riding on. + fftx = yf[1:NumUniquePts] + mx = np.abs(fftx) + mx = mx / N + mx = mx**2 + if N % 2 > 0: + mx[2:len(mx)] = mx[2:len(mx)]*2 + else: + mx[2:len(mx)-1] = mx[2:len(mx)-1]*2 + print "power fft = %s" % np.sqrt(np.sum(mx)) + + end = Fe/2 + start = end / (N/2) + xf = np.linspace(start, end, N/2 - 1) + mx = np.sqrt(mx) + plt.plot(xf, mx) + + plt.show() + +def simple(Fe): + N = Fe + x = np.linspace(0.0, 1.0, N) + + y = np.zeros(len(x)) + + y += 0.9 * np.sin(50.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*x) + y += 0.6 * np.sin(20.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*x) + y += 0.2 * np.sin(80.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*x) + + #y = noise(y, 2) + + plot(Fe, N, x, y) + +def real_sound_weave(durationMs): + Fe = 16000 + N = Fe * durationMs / 1000 + x = np.linspace(0.0, N, N) + + y = np.zeros(len(x)) + + y += np.sin(2.0 * np.pi * x / (Fe / float(4500))) + y += 0.5 * np.sin(2.0 * np.pi * x / (Fe / float(4000))) + y += 0.5 * np.sin(2.0 * np.pi * x / (Fe / float(1000))) + y += 0.9 * np.sin(2.0 * np.pi * x / (Fe / float(7500))) + y += 1 * np.sin(2.0 * np.pi * x / (Fe / float(3000))) + + m = np.max(np.abs(y)) + + y = y * 0x7fff +# y = y / m + + #y = noise(y, 0x7fff) + + plot(Fe, N, x, y) + +#simple(1000) +real_sound_weave(100) \ No newline at end of file