Java tutorial
//////////////////////////////////////////////////////////////////////////////// // // Tuner - An Android Tuner written in Java. // // Copyright (C) 2013 Bill Farmer // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. // // Bill Farmer william j farmer [at] yahoo [dot] co [dot] uk. // /////////////////////////////////////////////////////////////////////////////// package org.billthefarmer.tuner; import android.Manifest; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.media.AudioFormat; import android.media.AudioRecord; import android.os.Bundle; import android.preference.PreferenceManager; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; import java.util.Locale; import org.json.JSONArray; import org.json.JSONException; // Main Activity public class MainActivity extends Activity implements View.OnClickListener, View.OnLongClickListener { private static final String PREF_INPUT = "pref_input"; private static final String PREF_REFERENCE = "pref_reference"; private static final String PREF_TRANSPOSE = "pref_transpose"; private static final String PREF_FILTER = "pref_filter"; private static final String PREF_DOWNSAMPLE = "pref_downsample"; private static final String PREF_MULTIPLE = "pref_multiple"; private static final String PREF_SCREEN = "pref_screen"; private static final String PREF_STROBE = "pref_strobe"; private static final String PREF_ZOOM = "pref_zoom"; private static final String PREF_COLOUR = "pref_colour"; private static final String PREF_CUSTOM = "pref_custom"; // Note values for display private static final String notes[] = { "C", "C", "D", "E", "E", "F", "F", "G", "A", "A", "B", "B" }; private static final String sharps[] = { "", "\u266F", "", "\u266D", "", "", "\u266F", "", "\u266D", "", "\u266D", "" }; private SignalView signal; private Spectrum spectrum; private Display display; private Strobe strobe; private Status status; private Meter meter; private Scope scope; private Audio audio; private Toast toast; // On Create @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Find the views, not all may be present spectrum = (Spectrum) findViewById(R.id.spectrum); display = (Display) findViewById(R.id.display); strobe = (Strobe) findViewById(R.id.strobe); status = (Status) findViewById(R.id.status); meter = (Meter) findViewById(R.id.meter); scope = (Scope) findViewById(R.id.scope); // Add custom view to action bar ActionBar actionBar = getActionBar(); actionBar.setCustomView(R.layout.signal_view); actionBar.setDisplayShowCustomEnabled(true); signal = (SignalView) actionBar.getCustomView(); // Create audio audio = new Audio(); // Connect views to audio if (spectrum != null) spectrum.audio = audio; if (display != null) display.audio = audio; if (strobe != null) strobe.audio = audio; if (status != null) status.audio = audio; if (signal != null) signal.audio = audio; if (meter != null) meter.audio = audio; if (scope != null) scope.audio = audio; // Set up the click listeners setClickListeners(); } // On create options menu @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it // is present. MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.activity_main, menu); return true; } // Set click listeners void setClickListeners() { // Scope if (scope != null) scope.setOnClickListener(this); // Spectrum if (spectrum != null) { spectrum.setOnClickListener(this); spectrum.setOnLongClickListener(this); } // Display if (display != null) { display.setOnClickListener(this); display.setOnLongClickListener(this); } // Strobe if (strobe != null) strobe.setOnClickListener(this); // Meter if (meter != null) { meter.setOnClickListener(this); meter.setOnLongClickListener(this); } } // On click @Override public void onClick(View v) { // Get id int id = v.getId(); switch (id) { // Scope case R.id.scope: audio.filter = !audio.filter; if (audio.filter) showToast(R.string.filter_on); else showToast(R.string.filter_off); break; // Spectrum case R.id.spectrum: audio.zoom = !audio.zoom; if (audio.zoom) showToast(R.string.zoom_on); else showToast(R.string.zoom_off); break; // Display case R.id.display: audio.lock = !audio.lock; if (display != null) display.invalidate(); if (audio.lock) showToast(R.string.lock_on); else showToast(R.string.lock_off); break; // Strobe case R.id.strobe: audio.strobe = !audio.strobe; if (audio.strobe) showToast(R.string.strobe_on); else showToast(R.string.strobe_off); break; // Meter case R.id.meter: audio.copyToClipboard(); showToast(R.string.copied_clip); break; } } // On long click @Override public boolean onLongClick(View v) { // Get id int id = v.getId(); switch (id) { // Spectrum case R.id.spectrum: audio.downsample = !audio.downsample; if (audio.downsample) showToast(R.string.downsample_on); else showToast(R.string.downsample_off); break; // Display case R.id.display: audio.multiple = !audio.multiple; if (audio.multiple) showToast(R.string.multiple_on); else showToast(R.string.multiple_off); break; // Meter case R.id.meter: audio.screen = !audio.screen; if (audio.screen) showToast(R.string.screen_on); else showToast(R.string.screen_off); Window window = getWindow(); if (audio.screen) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); break; } return true; } // On options item @Override public boolean onOptionsItemSelected(MenuItem item) { // Get id int id = item.getItemId(); switch (id) { // Help case R.id.help: return onHelpClick(item); // Settings case R.id.settings: return onSettingsClick(item); default: return false; } } // On help click private boolean onHelpClick(MenuItem item) { Intent intent = new Intent(this, HelpActivity.class); startActivity(intent); return true; } // On settings click private boolean onSettingsClick(MenuItem item) { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); return true; } // Show toast. void showToast(int key) { Resources resources = getResources(); String text = resources.getString(key); // Cancel the last one if (toast != null) toast.cancel(); // Make a new one toast = Toast.makeText(this, text, Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, 0); toast.show(); // Update status if (status != null) status.invalidate(); } // On start @Override protected void onStart() { super.onStart(); } // On Resume @Override protected void onResume() { super.onResume(); // Get preferences getPreferences(); // Update status if (status != null) status.invalidate(); // Start the audio thread audio.start(); } @Override protected void onPause() { super.onPause(); // Save preferences savePreferences(); // Stop audio thread audio.stop(); } // On stop @Override protected void onStop() { super.onStop(); } // On destroy @Override protected void onDestroy() { super.onDestroy(); // Get rid of all those pesky objects audio = null; scope = null; spectrum = null; display = null; strobe = null; meter = null; status = null; signal = null; toast = null; // Hint that it might be a good idea System.runFinalization(); } // Save preferences void savePreferences() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean(PREF_FILTER, audio.filter); editor.putBoolean(PREF_DOWNSAMPLE, audio.downsample); editor.putBoolean(PREF_MULTIPLE, audio.multiple); editor.putBoolean(PREF_SCREEN, audio.screen); editor.putBoolean(PREF_STROBE, audio.strobe); editor.putBoolean(PREF_ZOOM, audio.zoom); editor.apply(); } // Get preferences void getPreferences() { // Load preferences PreferenceManager.setDefaultValues(this, R.xml.preferences, false); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); // Set preferences if (audio != null) { audio.input = Integer.parseInt(preferences.getString(PREF_INPUT, "0")); audio.reference = preferences.getInt(PREF_REFERENCE, 440); audio.transpose = Integer.parseInt(preferences.getString(PREF_TRANSPOSE, "0")); audio.filter = preferences.getBoolean(PREF_FILTER, false); audio.downsample = preferences.getBoolean(PREF_DOWNSAMPLE, false); audio.multiple = preferences.getBoolean(PREF_MULTIPLE, false); audio.screen = preferences.getBoolean(PREF_SCREEN, false); audio.strobe = preferences.getBoolean(PREF_STROBE, false); audio.zoom = preferences.getBoolean(PREF_ZOOM, true); // Check screen if (audio.screen) { Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { Window window = getWindow(); window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } // Check for strobe before setting colours if (strobe != null) { strobe.colour = Integer.valueOf(preferences.getString(PREF_COLOUR, "0")); if (strobe.colour == 3) { JSONArray custom; try { custom = new JSONArray(preferences.getString(PREF_CUSTOM, null)); strobe.foreground = custom.getInt(0); strobe.background = custom.getInt(1); } catch (JSONException e) { e.printStackTrace(); } } // Ensure the view dimensions have been set if (strobe.width > 0 && strobe.height > 0) strobe.createShaders(); } } } // Show alert void showAlert(int appName, int errorBuffer) { // Create an alert dialog builder AlertDialog.Builder builder = new AlertDialog.Builder(this); // Set the title, message and button builder.setTitle(appName); builder.setMessage(errorBuffer); builder.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Dismiss dialog dialog.dismiss(); } }); // Create the dialog AlertDialog dialog = builder.create(); // Show it dialog.show(); } // Audio protected class Audio implements Runnable { // Preferences protected int input; protected int transpose; protected boolean lock; protected boolean zoom; protected boolean filter; protected boolean screen; protected boolean strobe; protected boolean multiple; protected boolean downsample; protected double reference; // Data protected Thread thread; protected double buffer[]; protected short data[]; protected int sample; // Output data protected double lower; protected double higher; protected double nearest; protected double frequency; protected double difference; protected double cents; protected double fps; protected int count; protected int note; // Private data private long timer; private int divisor = 1; private AudioRecord audioRecord; private static final int MAXIMA = 8; private static final int OVERSAMPLE = 16; private static final int SAMPLES = 16384; private static final int RANGE = SAMPLES * 3 / 8; private static final int STEP = SAMPLES / OVERSAMPLE; private static final int SIZE = 4096; private static final int OCTAVE = 12; private static final int C5_OFFSET = 57; private static final long TIMER_COUNT = 24; private static final double MIN = 0.5; private static final double G = 3.023332184e+01; private static final double K = 0.9338478249; private double xv[]; private double yv[]; private Complex x; protected float signal; protected Maxima maxima; protected double xa[]; private double xp[]; private double xf[]; private double dx[]; private double x2[]; private double x3[]; private double x4[]; private double x5[]; // Constructor protected Audio() { buffer = new double[SAMPLES]; xv = new double[2]; yv = new double[2]; x = new Complex(SAMPLES); maxima = new Maxima(MAXIMA); xa = new double[RANGE]; xp = new double[RANGE]; xf = new double[RANGE]; dx = new double[RANGE]; x2 = new double[RANGE / 2]; x3 = new double[RANGE / 3]; x4 = new double[RANGE / 4]; x5 = new double[RANGE / 5]; } // Start audio protected void start() { // Start the thread thread = new Thread(this, "Audio"); thread.start(); } // Run @Override public void run() { processAudio(); } // Stop protected void stop() { cleanUpAudioRecord(); Thread t = thread; thread = null; // Wait for the thread to exit while (t != null && t.isAlive()) Thread.yield(); } // Stop and release the audio recorder private void cleanUpAudioRecord() { if (audioRecord != null && audioRecord.getState() == AudioRecord.STATE_INITIALIZED) { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } audioRecord.release(); } } // Process Audio protected void processAudio() { // Sample rates to try Resources resources = getResources(); int rates[] = resources.getIntArray(R.array.sample_rates); int divisors[] = resources.getIntArray(R.array.divisors); int size = 0; int state = 0; int index = 0; for (int rate : rates) { // Check sample rate size = AudioRecord.getMinBufferSize(rate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); // Loop if invalid sample rate if (size == AudioRecord.ERROR_BAD_VALUE) { index++; continue; } // Check valid input selected, or other error if (size == AudioRecord.ERROR) { runOnUiThread(new Runnable() { @Override public void run() { showAlert(R.string.app_name, R.string.error_buffer); } }); thread = null; return; } // Set divisor divisor = divisors[index]; // Create the AudioRecord object try { audioRecord = new AudioRecord(input, rate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(size, SIZE * divisor)); } // Exception catch (Exception e) { runOnUiThread(new Runnable() { @Override public void run() { showAlert(R.string.app_name, R.string.error_init); } }); thread = null; return; } // Check state state = audioRecord.getState(); if (state != AudioRecord.STATE_INITIALIZED) { audioRecord.release(); index++; continue; } // Must be a valid sample rate sample = rate; break; } // Check valid sample rate if (size == AudioRecord.ERROR_BAD_VALUE) { runOnUiThread(new Runnable() { @Override public void run() { showAlert(R.string.app_name, R.string.error_buffer); } }); thread = null; return; } // Check AudioRecord initialised if (state != AudioRecord.STATE_INITIALIZED) { runOnUiThread(new Runnable() { @Override public void run() { showAlert(R.string.app_name, R.string.error_init); } }); audioRecord.release(); thread = null; return; } // Calculate fps and expect fps = ((double) sample / divisor) / SAMPLES; final double expect = 2.0 * Math.PI * STEP / SAMPLES; // Create buffer for input data data = new short[STEP * divisor]; // Start recording audioRecord.startRecording(); // Max data double dmax = 0.0; // Continue until the thread is stopped while (thread != null) { // Read a buffer of data // NOTE: audioRecord.read(short[], int, int) can block // indefinitely, until audioRecord.stop() is called // from another thread size = audioRecord.read(data, 0, STEP * divisor); // Stop the thread if no data or error state if (size <= 0) { thread = null; break; } // If display not locked update scope if (scope != null && !lock) scope.postInvalidate(); // Move the main data buffer up System.arraycopy(buffer, STEP, buffer, 0, SAMPLES - STEP); // Max signal double rm = 0; // Butterworth filter, 3dB/octave for (int i = 0; i < STEP; i++) { xv[0] = xv[1]; xv[1] = data[i * divisor] / G; yv[0] = yv[1]; yv[1] = (xv[0] + xv[1]) + (K * yv[0]); // Choose filtered/unfiltered data buffer[(SAMPLES - STEP) + i] = audio.filter ? yv[1] : data[i * divisor]; // Find root mean signal double v = data[i * divisor] / 32768.0; rm += v * v; } // Signal value rm /= STEP; signal = (float) Math.sqrt(rm); // Maximum value if (dmax < 4096.0) dmax = 4096.0; // Calculate normalising value double norm = dmax; dmax = 0.0; // Copy data to FFT input arrays for tuner for (int i = 0; i < SAMPLES; i++) { // Find the magnitude if (dmax < Math.abs(buffer[i])) dmax = Math.abs(buffer[i]); // Calculate the window double window = 0.5 - 0.5 * Math.cos(2.0 * Math.PI * i / SAMPLES); // Normalise and window the input data x.r[i] = buffer[i] / norm * window; } // do FFT for tuner fftr(x); // Process FFT output for tuner for (int i = 1; i < RANGE; i++) { double real = x.r[i]; double imag = x.i[i]; xa[i] = Math.hypot(real, imag); // Do frequency calculation double p = Math.atan2(imag, real); double dp = xp[i] - p; xp[i] = p; // Calculate phase difference dp -= i * expect; int qpd = (int) (dp / Math.PI); if (qpd >= 0) qpd += qpd & 1; else qpd -= qpd & 1; dp -= Math.PI * qpd; // Calculate frequency difference double df = OVERSAMPLE * dp / (2.0 * Math.PI); // Calculate actual frequency from slot frequency plus // frequency difference and correction value xf[i] = i * fps + df * fps; // Calculate differences for finding maxima dx[i] = xa[i] - xa[i - 1]; } // Downsample if (downsample) { // x2 = xa << 2 for (int i = 0; i < RANGE / 2; i++) { x2[i] = 0.0; for (int j = 0; j < 2; j++) x2[i] += xa[(i * 2) + j] / 2.0; } // x3 = xa << 3 for (int i = 0; i < RANGE / 3; i++) { x3[i] = 0.0; for (int j = 0; j < 3; j++) x3[i] += xa[(i * 3) + j] / 3.0; } // x4 = xa << 4 for (int i = 0; i < RANGE / 4; i++) { x4[i] = 0.0; for (int j = 0; j < 4; j++) x2[i] += xa[(i * 4) + j] / 4.0; } // x5 = xa << 5 for (int i = 0; i < RANGE / 5; i++) { x5[i] = 0.0; for (int j = 0; j < 5; j++) x5[i] += xa[(i * 5) + j] / 5.0; } // Add downsamples for (int i = 1; i < RANGE; i++) { if (i < RANGE / 2) xa[i] += x2[i]; if (i < RANGE / 3) xa[i] += x3[i]; if (i < RANGE / 4) xa[i] += x4[i]; if (i < RANGE / 5) xa[i] += x5[i]; // Recalculate differences dx[i] = xa[i] - xa[i - 1]; } } // Maximum FFT output double max = 0.0; count = 0; int limit = RANGE - 1; // Find maximum value, and list of maxima for (int i = 1; i < limit; i++) { if (xa[i] > max) { max = xa[i]; frequency = xf[i]; } // If display not locked, find maxima and add to list if (!lock && count < MAXIMA && xa[i] > MIN && xa[i] > (max / 4.0) && dx[i] > 0.0 && dx[i + 1] < 0.0) { maxima.f[count] = xf[i]; // Cents relative to reference double cf = -12.0 * log2(reference / xf[i]); // Reference note maxima.r[count] = reference * Math.pow(2.0, Math.round(cf) / 12.0); // Note number maxima.n[count] = (int) (Math.round(cf) + C5_OFFSET); // Don't use if negative if (maxima.n[count] < 0) { maxima.n[count] = 0; continue; } // Set limit to octave above if (!downsample && (limit > i * 2)) limit = i * 2 - 1; count++; } } // Found flag boolean found = false; // Do the note and cents calculations if (max > MIN) { found = true; // Frequency if (!downsample) frequency = maxima.f[0]; // Cents relative to reference double cf = -12.0 * log2(reference / frequency); // Don't count silly values if (Double.isNaN(cf)) { cf = 0.0; found = false; } // Reference note nearest = audio.reference * Math.pow(2.0, Math.round(cf) / 12.0); // Lower and upper freq lower = reference * Math.pow(2.0, (Math.round(cf) - 0.55) / 12.0); higher = reference * Math.pow(2.0, (Math.round(cf) + 0.55) / 12.0); // Note number note = (int) Math.round(cf) + C5_OFFSET; if (note < 0) { note = 0; found = false; } // Find nearest maximum to reference note double df = 1000.0; for (int i = 0; i < count; i++) { if (Math.abs(maxima.f[i] - nearest) < df) { df = Math.abs(maxima.f[i] - nearest); frequency = maxima.f[i]; } } // Cents relative to reference note cents = -12.0 * log2(nearest / frequency) * 100.0; // Ignore silly values if (Double.isNaN(cents)) { cents = 0.0; found = false; } // Ignore if not within 50 cents of reference note if (Math.abs(cents) > 50.0) { cents = 0.0; found = false; } // Difference difference = frequency - nearest; } // Found if (found) { // If display not locked if (!lock) { // Update spectrum if (spectrum != null) spectrum.postInvalidate(); // Update display if (display != null) display.postInvalidate(); } // Reset count; timer = 0; } else { // If display not locked if (!lock) { if (timer > TIMER_COUNT) { difference = 0.0; frequency = 0.0; nearest = 0.0; higher = 0.0; lower = 0.0; cents = 0.0; count = 0; note = 0; // Update display if (display != null) display.postInvalidate(); } // Update spectrum if (spectrum != null) spectrum.postInvalidate(); } } timer++; } cleanUpAudioRecord(); } // Real to complex FFT, ignores imaginary values in input array private void fftr(Complex a) { final int n = a.r.length; final double norm = Math.sqrt(1.0 / n); for (int i = 0, j = 0; i < n; i++) { if (j >= i) { double tr = a.r[j] * norm; a.r[j] = a.r[i] * norm; a.i[j] = 0.0; a.r[i] = tr; a.i[i] = 0.0; } int m = n / 2; while (m >= 1 && j >= m) { j -= m; m /= 2; } j += m; } for (int mmax = 1, istep = 2 * mmax; mmax < n; mmax = istep, istep = 2 * mmax) { double delta = (Math.PI / mmax); for (int m = 0; m < mmax; m++) { double w = m * delta; double wr = Math.cos(w); double wi = Math.sin(w); for (int i = m; i < n; i += istep) { int j = i + mmax; double tr = wr * a.r[j] - wi * a.i[j]; double ti = wr * a.i[j] + wi * a.r[j]; a.r[j] = a.r[i] - tr; a.i[j] = a.i[i] - ti; a.r[i] += tr; a.i[i] += ti; } } } } // Copy to clipboard protected void copyToClipboard() { String text = ""; if (multiple) { for (int i = 0; i < count; i++) { // Calculate cents double cents = -12.0 * log2(maxima.r[i] / maxima.f[i]) * 100.0; // Ignore silly values if (Double.isNaN(cents)) continue; text += String.format(Locale.getDefault(), "%s%s%d\t%+5.2f\u00A2\t%4.2fHz\t%4.2fHz\t%+5.2fHz\n", notes[(maxima.n[i] - transpose + OCTAVE) % OCTAVE], sharps[(maxima.n[i] - transpose + OCTAVE) % OCTAVE], (maxima.n[i] - transpose) / OCTAVE, cents, maxima.r[i], maxima.f[i], maxima.r[i] - maxima.f[i]); } if (count == 0) text = String.format(Locale.getDefault(), "%s%s%d\t%+5.2f\u00A2\t%4.2fHz\t%4.2fHz\t%+5.2fHz\n", notes[(note - transpose + OCTAVE) % OCTAVE], sharps[(note - transpose + OCTAVE) % OCTAVE], (note - transpose) / OCTAVE, cents, nearest, frequency, difference); } else text = String.format(Locale.getDefault(), "%s%s%d\t%+5.2f\u00A2\t%4.2fHz\t%4.2fHz\t%+5.2fHz\n", notes[(note - transpose + OCTAVE) % OCTAVE], sharps[(note - transpose + OCTAVE) % OCTAVE], (note - transpose) / OCTAVE, cents, nearest, frequency, difference); ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); clipboard.setPrimaryClip(ClipData.newPlainText("Tuner clip", text)); } } // Log2 protected double log2(double d) { return Math.log(d) / Math.log(2.0); } // These two objects replace arrays of structs in the C version // because initialising arrays of objects in Java is, IMHO, barmy // Complex private class Complex { double r[]; double i[]; private Complex(int l) { r = new double[l]; i = new double[l]; } } // Maximum protected class Maxima { double f[]; double r[]; int n[]; protected Maxima(int l) { f = new double[l]; r = new double[l]; n = new int[l]; } } }