add fft chart view
This commit is contained in:
parent
bdc910031f
commit
1e0f71e14e
8 changed files with 382 additions and 37 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
17
docs/fft.py
17
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)
|
||||
simple(1000)
|
||||
#real_sound_weave(4500)
|
||||
Loading…
Add table
Add a link
Reference in a new issue