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