Skip to content

Commit b517897

Browse files
committed
minimal working tuner
0 parents  commit b517897

File tree

10 files changed

+1037
-0
lines changed

10 files changed

+1037
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build
2+
.gradle

LICENSE

+619
Large diffs are not rendered by default.

build.gradle

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
buildscript {
2+
repositories {
3+
google()
4+
jcenter()
5+
}
6+
7+
dependencies {
8+
classpath 'com.android.tools.build:gradle:3.4.0'
9+
}
10+
}
11+
12+
apply plugin: 'com.android.application'
13+
14+
repositories {
15+
mavenCentral()
16+
maven {
17+
url "https://maven.google.com"
18+
}
19+
}
20+
21+
dependencies {
22+
}
23+
24+
android {
25+
compileSdkVersion 28
26+
buildToolsVersion "28.0.2"
27+
28+
lintOptions {
29+
disable "GoogleAppIndexingWarning"
30+
}
31+
32+
defaultConfig {
33+
applicationId "mn.tck.semitone"
34+
}
35+
}
36+
37+
gradle.projectsEvaluated {
38+
tasks.withType(JavaCompile) {
39+
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
40+
}
41+
}

src/main/AndroidManifest.xml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="mn.tck.semitone">
3+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
4+
<uses-sdk android:minSdkVersion="16" />
5+
<application android:label="@string/app_name"
6+
android:allowBackup="true">
7+
<activity android:name=".MainActivity"
8+
android:label="@string/app_name">
9+
<intent-filter>
10+
<action android:name="android.intent.action.MAIN" />
11+
<category android:name="android.intent.category.LAUNCHER" />
12+
</intent-filter>
13+
</activity>
14+
</application>
15+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Semitone - tuner, metronome, and piano for Android
3+
* Copyright (C) 2019 Andy Tockman <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package mn.tck.semitone;
20+
21+
import android.app.*;
22+
import android.content.*;
23+
import android.graphics.*;
24+
import android.os.*;
25+
import android.util.*;
26+
import android.view.*;
27+
import android.widget.*;
28+
29+
public class CentErrorView extends TextView {
30+
31+
private Paint centerPaint, linePaint;
32+
double error;
33+
34+
public CentErrorView(Context context, AttributeSet attrs) {
35+
super(context, attrs);
36+
37+
centerPaint = new Paint();
38+
centerPaint.setColor(Color.WHITE);
39+
centerPaint.setStrokeWidth(1);
40+
41+
linePaint = new Paint();
42+
linePaint.setColor(Color.RED);
43+
linePaint.setStrokeWidth(2);
44+
45+
error = 0;
46+
}
47+
48+
public void setError(double error) {
49+
this.error = error;
50+
setText(String.format("%+.2f cents", error*100));
51+
}
52+
53+
@Override protected void onDraw(Canvas canvas) {
54+
super.onDraw(canvas);
55+
int width = getWidth(), height = getHeight(), middle = width / 2;
56+
57+
// draw middle indicator
58+
canvas.drawLine(middle, 0, middle, height/4, centerPaint);
59+
canvas.drawLine(middle, height*3/4, middle, height, centerPaint);
60+
61+
// draw error position
62+
int xpos = middle + (int)(error*width);
63+
canvas.drawLine(xpos, 0, xpos, height, linePaint);
64+
}
65+
66+
}
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Semitone - tuner, metronome, and piano for Android
3+
* Copyright (C) 2019 Andy Tockman <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package mn.tck.semitone;
20+
21+
public class DSP {
22+
23+
public static int fftlen, fftpow;
24+
25+
static double[] cos;
26+
static double[] sin;
27+
28+
public static void init(int bufsize) {
29+
fftpow = 31 - Integer.numberOfLeadingZeros(bufsize);
30+
fftlen = 1 << fftpow;
31+
32+
cos = new double[fftlen/2];
33+
sin = new double[fftlen/2];
34+
for (int i = 0; i < fftlen/2; ++i) {
35+
cos[i] = Math.cos(-2*Math.PI*i/fftlen);
36+
sin[i] = Math.sin(-2*Math.PI*i/fftlen);
37+
}
38+
}
39+
40+
// in-place Cooley-Tukey algorithm
41+
private static void fft(double[] re, double[] im) {
42+
// bit reversal
43+
{
44+
int j = 0;
45+
int n2 = fftlen/2;
46+
for (int i = 1; i < fftlen-1; ++i) {
47+
int n1 = n2;
48+
while (j >= n1) {
49+
j -= n1;
50+
n1 /= 2;
51+
}
52+
j += n1;
53+
54+
double tmp;
55+
if (i < j) {
56+
tmp = re[i]; re[i] = re[j]; re[j] = tmp;
57+
tmp = im[i]; im[i] = im[j]; im[j] = tmp;
58+
}
59+
}
60+
}
61+
62+
int n1 = 0, n2 = 1;
63+
for (int i = 0; i < fftpow; ++i) {
64+
n1 = n2;
65+
n2 *= 2;
66+
int a = 0;
67+
for (int j = 0; j < n1; ++j) {
68+
for (int k = j; k < fftlen; k += n2) {
69+
double tre = cos[a] * re[k+n1] - sin[a] * im[k+n1],
70+
tim = sin[a] * re[k+n1] + cos[a] * im[k+n1];
71+
re[k+n1] = re[k] - tre;
72+
im[k+n1] = im[k] - tim;
73+
re[k] += tre;
74+
im[k] += tim;
75+
}
76+
a += 1 << (fftpow - i - 1);
77+
}
78+
}
79+
}
80+
81+
// in-place autocorrelation (scaled by N, but that doesn't matter for us)
82+
private static void autocorr(double[] are) {
83+
double[] aim = new double[fftlen];
84+
fft(are, aim);
85+
for (int i = 0; i < fftlen; ++i) {
86+
// corr(a, a) = ifft(fft(a) * conj(fft(a)))
87+
are[i] = are[i] * are[i] + aim[i] * aim[i];
88+
aim[i] = 0;
89+
}
90+
fft(aim, are); // inverse fft
91+
}
92+
93+
// get frequency from mic data (buf must have length fftlen)
94+
public static double freq(double[] buf, int sr) {
95+
// TODO check buf length
96+
// TODO clone buf if necessary
97+
autocorr(buf);
98+
// look for the maximum value after the first local minimum
99+
// (only look halfway through cause it's symmetric)
100+
boolean looking = false;
101+
double maxval = 0;
102+
int j = -1; // maxidx
103+
for (int i = 0; i < fftlen/2; ++i) {
104+
if (looking) {
105+
double weighted = buf[i] * 1; // naive weighting
106+
if (weighted > maxval) {
107+
maxval = weighted;
108+
j = i;
109+
}
110+
} else {
111+
// looking = buf[i] < buf[i+1];
112+
looking = buf[i] < 0;
113+
}
114+
}
115+
// TODO handle this better
116+
if (j == -1) return 440;
117+
// quadratic interpolation
118+
double interp = 0.5 * (buf[j-1] - buf[j+1]) / (buf[j-1] - 2*buf[j] + buf[j+1]);
119+
return (double)sr / (j + interp);
120+
}
121+
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Semitone - tuner, metronome, and piano for Android
3+
* Copyright (C) 2019 Andy Tockman <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package mn.tck.semitone;
20+
21+
import android.app.*;
22+
import android.content.*;
23+
import android.graphics.*;
24+
import android.os.*;
25+
import android.util.*;
26+
import android.view.*;
27+
import android.widget.*;
28+
29+
import android.media.AudioFormat;
30+
import android.media.AudioRecord;
31+
import android.media.MediaRecorder.AudioSource;
32+
33+
import java.util.Arrays;
34+
35+
public class MainActivity extends Activity {
36+
37+
final static String[] notenames = {"A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"};
38+
39+
final static int SAMPLE_RATE = 44100;
40+
final static int HIST_SIZE = 16;
41+
static int bufsize;
42+
static AudioRecord ar;
43+
static DSP dsp;
44+
45+
TextView notename;
46+
CentErrorView centerror;
47+
48+
@Override protected void onCreate(Bundle state) {
49+
super.onCreate(state);
50+
requestWindowFeature(Window.FEATURE_NO_TITLE);
51+
setContentView(R.layout.activity_main);
52+
53+
notename = (TextView) findViewById(R.id.notename);
54+
notename.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
55+
@Override public void onGlobalLayout() {
56+
notename.getViewTreeObserver().removeOnGlobalLayoutListener(this);
57+
notename.setTextSize(TypedValue.COMPLEX_UNIT_PX,
58+
Util.maxTextSize("G#000", notename.getWidth()));
59+
}
60+
});
61+
centerror = (CentErrorView) findViewById(R.id.centerror);
62+
63+
bufsize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
64+
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
65+
ar = new AudioRecord(AudioSource.MIC, SAMPLE_RATE,
66+
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
67+
bufsize);
68+
ar.startRecording();
69+
70+
DSP.init(bufsize);
71+
72+
new Thread(new TunerThread()).start();
73+
}
74+
75+
class TunerThread implements Runnable {
76+
@Override public void run() {
77+
short[] buf = new short[bufsize];
78+
double[] dbuf = new double[DSP.fftlen];
79+
double[] hist = new double[HIST_SIZE];
80+
double[] sorted = new double[HIST_SIZE];
81+
for (;;) {
82+
// copy data to fft buffer - scale down to avoid huge numbers
83+
ar.read(buf, 0, bufsize);
84+
for (int i = 0; i < DSP.fftlen; ++i) dbuf[i] = buf[i] / 1024.0;
85+
86+
// calculate frequency and note
87+
double freq = DSP.freq(dbuf, SAMPLE_RATE),
88+
semitone = 12 * Math.log(freq/440)/Math.log(2);
89+
90+
// insert into moving average history
91+
for (int i = 1; i < HIST_SIZE; ++i) sorted[i-1] = hist[i-1] = hist[i];
92+
sorted[HIST_SIZE-1] = hist[HIST_SIZE-1] = semitone;
93+
94+
// find median
95+
Arrays.sort(sorted);
96+
final double median = (sorted[HIST_SIZE/2-1]+sorted[HIST_SIZE/2])/2;
97+
98+
final int rounded = (int)Math.round(median);
99+
final int note = Math.floorMod(rounded, 12);
100+
101+
runOnUiThread(new Runnable() {
102+
@Override public void run() {
103+
notename.setText(notenames[note] +
104+
(Math.floorDiv(rounded, 12) + 5 - (note <= 2 ? 1 : 0)));
105+
centerror.setError(median - rounded);
106+
}
107+
});
108+
}
109+
}
110+
}
111+
112+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Semitone - tuner, metronome, and piano for Android
3+
* Copyright (C) 2019 Andy Tockman <[email protected]>
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package mn.tck.semitone;
20+
21+
import android.text.TextPaint;
22+
23+
public class Util {
24+
25+
public static int maxTextSize(String text, int maxWidth) {
26+
TextPaint paint = new TextPaint();
27+
for (int textSize = 10;; ++textSize) {
28+
paint.setTextSize(textSize);
29+
if (paint.measureText(text) > maxWidth) return textSize - 1;
30+
}
31+
}
32+
33+
}

0 commit comments

Comments
 (0)