add fft chart view

This commit is contained in:
Alexey Kuznetsov 2016-03-31 21:59:31 +03:00
commit 1e0f71e14e
8 changed files with 382 additions and 37 deletions

View file

@ -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")
}

View file

@ -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;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
@ -10,4 +13,9 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
public void testFFT() {
short[] buf = RawSamples.generateSound(16000, 4500, 100);
short[] fft = RawSamples.fft(buf, 0, buf.length);
}
}

View file

@ -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);
}
});
}

View file

@ -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)

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -42,6 +42,7 @@ public class PitchView extends ViewGroup {
Paint paint;
Paint paintRed;
List<Double> data = new LinkedList<>();
List<short[]> 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) {

View file

@ -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)
simple(1000)
#real_sound_weave(4500)