From 1e0f71e14e612f2a31fb9acb99b331d059cba91b Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Thu, 31 Mar 2016 21:59:31 +0300 Subject: [PATCH] add fft chart view --- app/build.gradle | 1 + .../axet/audiorecorder/ApplicationTest.java | 10 +- .../activities/RecordingActivity.java | 8 +- .../axet/audiorecorder/app/RawSamples.java | 42 +++++ .../audiorecorder/widgets/FFTBarView.java | 152 ++++++++++++++++++ .../audiorecorder/widgets/FFTChartView.java | 128 +++++++++++++++ .../axet/audiorecorder/widgets/PitchView.java | 61 ++++--- docs/fft.py | 17 +- 8 files changed, 382 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/github/axet/audiorecorder/widgets/FFTBarView.java create mode 100644 app/src/main/java/com/github/axet/audiorecorder/widgets/FFTChartView.java diff --git a/app/build.gradle b/app/build.gradle index 5e657ba..3683802 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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..2ac4be3 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,9 @@ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } -} \ No newline at end of file + + public void testFFT() { + short[] buf = RawSamples.generateSound(16000, 4500, 100); + short[] fft = RawSamples.fft(buf, 0, buf.length); + } +} 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..1f10142 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 @@ -273,7 +273,9 @@ public class RecordingActivity extends AppCompatActivity { pitch.clear(cut / samplesUpdate); for (int i = 0; i < len; i += samplesUpdate) { double dB = RawSamples.getDB(buf, i, samplesUpdate); - pitch.add(dB); + short[] ss = new short[samplesUpdate]; + System.arraycopy(buf, i, ss, 0, ss.length); + pitch.add(dB, ss); } updateSamples(samplesTime); } @@ -613,10 +615,12 @@ public class RecordingActivity extends AppCompatActivity { for (int i = 0; i < readSize; i += samplesUpdate) { final double dB = RawSamples.getDB(buffer, i, samplesUpdate); + final short[] ss = new short[samplesUpdate]; + System.arraycopy(buffer, i, ss, 0, ss.length); handle.post(new Runnable() { @Override public void run() { - pitch.add(dB); + pitch.add(dB, ss); } }); } 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..ab6bba8 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; @@ -140,6 +146,42 @@ public class RawSamples { return 20.0 * Math.log10(amplitude / 32768d); } + 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 short[] 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]; + + for (int i = 0; i < len; i++) { + dataR[i] = buffer[offset + i]; + } + + FastFourierTransformer.transformInPlace(dataRI, DftNormalization.STANDARD, TransformType.FORWARD); + + short[] data = new short[len2 / 2]; + + for (int i = 0; i < data.length; i++) { + Complex c = new Complex(dataR[i], dataI[i]); + data[i] = (short) (2.0 / len * c.abs()); + } + + return data; + } + public void close() { try { if (is != null) 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..7429a84 --- /dev/null +++ b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTBarView.java @@ -0,0 +1,152 @@ +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 View { + public static final String TAG = FFTBarView.class.getSimpleName(); + + Paint paint; + short[] buffer; + + int barCount; + float barWidth; + float barDeli; + + int max; + + Paint textPaint; + Rect textBounds; + + 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() { + 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()) { + //buffer = simple(); + buffer = RawSamples.generateSound(16000, 4000, 100); + buffer = RawSamples.fft(buffer, 0, buffer.length); + } + } + + public void setBuffer(short[] buf) { + buffer = RawSamples.fft(buf, 0, buf.length); + + max = Integer.MIN_VALUE; + for (int i = 0; i < buffer.length; i++) { + max = Math.max(buffer[i], max); + } + } + + short[] simple() { + int sampleRate = 1000; + int count = sampleRate; + short[] samples = new short[count]; + for (int i = 0; i < count; i++) { + double x = i / (double) sampleRate; + double y = 0; + y += 0.9 * Math.sin(50 * 2 * Math.PI * x); + y += 0.5 * Math.sin(80 * 2 * Math.PI * x); + y += 0.7 * Math.sin(40 * 2 * Math.PI * x); + samples[i] = (short) (y / 2.1 * 0x7fff); + } + return samples; + } + + @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) { + } + + int dp2px(float dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); + } + + @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++) { + short 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..1eab256 --- /dev/null +++ b/app/src/main/java/com/github/axet/audiorecorder/widgets/FFTChartView.java @@ -0,0 +1,128 @@ +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 View { + public static final String TAG = FFTChartView.class.getSimpleName(); + + Paint paint; + short[] buffer; + + Paint textPaint; + Rect textBounds; + + 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() { + 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()) { + buffer = simple(); + //buffer = RawSamples.generateSound(16000, 4000, 100); + //buffer = RawSamples.fft(buffer, 0, buffer.length); + } + } + + public void setBuffer(short[] buf) { + buffer = RawSamples.fft(buf, 0, buf.length); + } + + short[] simple() { + int sampleRate = 1000; + int count = sampleRate; + short[] samples = new short[count]; + for (int i = 0; i < count; i++) { + double x = i / (double) sampleRate; + double y = 0; + y += 0.9 * Math.sin(50 * 2 * Math.PI * x); + y += 0.5 * Math.sin(80 * 2 * Math.PI * x); + y += 0.7 * Math.sin(40 * 2 * Math.PI * x); + samples[i] = (short) (y / 2.1 * 0x7fff); + } + return samples; + } + + @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) { + } + + int dp2px(float dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); + } + + @Override + public void onDraw(Canvas canvas) { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + + for (int i = 0; i < buffer.length; i++) { + min = Math.min(buffer[i], min); + max = Math.max(buffer[i], max); + } + + int h = getHeight(); + + if (min < 0) { + h = h / 2; + } + + float startX = 0, startY = h; + + float step = canvas.getWidth() / (float) buffer.length; + + for (int i = 0; i < buffer.length; i++) { + float endX = startX; + float endY = h - h * (buffer[i] / (float) 0x7fff); + + 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, getWidth() - textBounds.width(), getHeight(), textPaint); + } + +} 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 34221b1..cf7a33b 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 @@ -42,6 +42,7 @@ public class PitchView extends ViewGroup { Paint paint; Paint paintRed; List data = new LinkedList<>(); + List dataSamples = new LinkedList<>(); // how many pitches we can fit on screen int pitchScreenCount; @@ -55,6 +56,7 @@ public class PitchView extends ViewGroup { int pitchSize; PitchGraphView graph; + FFTBarView fft; PitchCurrentView current; long time = 0; @@ -183,8 +185,7 @@ public class PitchView extends ViewGroup { tick = 0; time = cur; } - data.subList(0, 1).clear(); - samples += 1; + fit(data.size() - 1); } offset = pitchSize * tick; @@ -294,19 +295,6 @@ public class PitchView extends ViewGroup { super.onLayout(changed, left, top, right, bottom); } - public int getEnd() { - int end = data.size() - 1; - - if (editPos != -1) { - end = editPos; - } - if (playPos > 0) { - end = (int) playPos; - } - - return end; - } - void updateText(int end) { String str = ""; @@ -373,6 +361,10 @@ public class PitchView extends ViewGroup { graph = new PitchGraphView(getContext()); addView(graph); + fft = new FFTBarView(getContext()); + fft.setPadding(0, dp2px(2), 0, 0); + addView(fft); + current = new PitchCurrentView(getContext()); current.setPadding(0, dp2px(2), 0, 0); addView(current); @@ -415,6 +407,7 @@ public class PitchView extends ViewGroup { if (data.size() > max) { int cut = data.size() - max; data.subList(0, cut).clear(); + dataSamples.subList(0, cut).clear(); samples += cut; int m = data.size() - 1; @@ -426,14 +419,14 @@ public class PitchView extends ViewGroup { } } - public void add(double a) { + public void add(double a, short[] ss) { data.add(a); + dataSamples.add(ss); } public void drawCalc() { graph.calc(); - graph.invalidate(); - current.invalidate(); + draw(); } public void drawEnd() { @@ -442,6 +435,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); @@ -468,6 +474,10 @@ public class PitchView extends ViewGroup { public void draw() { graph.invalidate(); + if(data.size()>0) { + fft.setBuffer(dataSamples.get(getEnd())); + } + fft.invalidate(); current.invalidate(); } @@ -495,20 +505,19 @@ public class PitchView extends ViewGroup { current.measure(widthMeasureSpec, heightMeasureSpec); - int hh = MeasureSpec.getSize(heightMeasureSpec) - current.getMeasuredHeight(); + fft.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(dp2px(20), MeasureSpec.getMode(widthMeasureSpec))); + + int hh = MeasureSpec.getSize(heightMeasureSpec) - current.getMeasuredHeight() - fft.getMeasuredHeight(); graph.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(hh, MeasureSpec.getMode(widthMeasureSpec))); - - int w = Math.max(graph.getMeasuredWidth(), current.getMeasuredWidth()); - int h = graph.getMeasuredHeight() + current.getMeasuredHeight(); - - setMeasuredDimension(w, h); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { graph.layout(0, 0, graph.getMeasuredWidth(), graph.getMeasuredHeight()); - current.layout(0, graph.getMeasuredHeight(), current.getMeasuredWidth(), - graph.getMeasuredHeight() + current.getMeasuredHeight()); + fft.layout(0, graph.getMeasuredHeight(), fft.getMeasuredWidth(), + graph.getMeasuredHeight() + fft.getMeasuredHeight()); + current.layout(0, fft.getBottom(), current.getMeasuredWidth(), + fft.getBottom() + current.getMeasuredHeight()); } int dp2px(float dp) { diff --git a/docs/fft.py b/docs/fft.py index 62d0906..534286f 100644 --- a/docs/fft.py +++ b/docs/fft.py @@ -12,21 +12,21 @@ def plot(Fe, N, x, y): yf = scipy.fftpack.fft(y) xf = np.linspace(0.0, Fe/2, N/2) + yf = 2.0/N * np.abs(yf[:N/2]) plt.subplot(2, 1, 2) - plt.plot(xf, 2.0/N * np.abs(yf[:N/2])) - + plt.plot(xf, yf) + plt.show() def noise(y, amp): return y + amp*np.random.sample(len(y)) -def simple(): - Fe = 1000 +def simple(Fe): N = Fe x = np.linspace(0.0, 1.0, N) - y = np.sin(50.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*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 = noise(y, 2) + #y = noise(y, 2) plot(Fe, N, x, y) @@ -37,8 +37,9 @@ def real_sound_weave(freqHz): x = np.linspace(0.0, N, N) y = np.sin(2.0 * np.pi * x / (Fe / float(freqHz))) * 0x7FFF - y = noise(y, 0x7fff) + #y = noise(y, 0x7fff) plot(Fe, N, x, y) -real_sound_weave(4500) \ No newline at end of file +simple(1000) +#real_sound_weave(4500) \ No newline at end of file