compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.android.support:wearable:2.0.0-alpha1'
compile 'com.google.android.gms:play-services-wearable:9.0.2'
+ compile project(':vizlib')
}
dependencies {
// compile 'com.github.wendykierp:JTransforms:3.1' // brought just the bit we need in
- compile 'org.apache.commons:commons-math3:+'
+ // compile 'org.apache.commons:commons-math3:+' // moved to java.lang.Math instead
}
\ No newline at end of file
package com.acmetensortoys.watchviz;
import android.Manifest;
-import android.bluetooth.BluetoothManager;
-import android.content.Context;
import android.content.pm.PackageManager;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.media.AudioFormat;
-import android.media.AudioRecord;
-import android.media.MediaRecorder;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.wearable.activity.WearableActivity;
import android.support.wearable.view.BoxInsetLayout;
import android.util.Log;
-import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
-import com.acmetensortoys.watchviz.render.Grid;
-import com.acmetensortoys.watchviz.render.WholeMax;
-
import java.text.SimpleDateFormat;
-import java.util.ArrayDeque;
import java.util.Date;
-import java.util.Deque;
import java.util.Locale;
-import org.jtransforms.fft.FloatFFT_1D;
+import com.acmetensortoys.watchviz.vizlib.AudioCanvas;
public class MainActivity extends WearableActivity
{
private static final SimpleDateFormat AMBIENT_DATE_FORMAT =
new SimpleDateFormat("HH:mm", Locale.US);
- private static final int AUDIO_RECORDER_BUFFER_SIZE = 2048;
- private static final int AUDIO_SAMPLES = 512;
-
private BoxInsetLayout mOuterContainer;
private LinearLayout mInnerContainer;
private TextView mTextView, mClockView, mDebugView;
-
- private SurfaceView cyclersv;
- private Thread cycler;
-
- private RenderCB cyclercb;
- private Deque<Class<? extends RenderCB>> cyclercbq = new ArrayDeque<>();
- private void _setCyclerCB(Class<? extends RenderCB> next) {
- try {
- cyclercb = next.getConstructor().newInstance();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- String name = cyclercb.getClass().getSimpleName();
- mDebugView.setText(name.substring(0,Math.min(10,name.length())));
- }
- private void nextCyclerCB() {
- synchronized(this) {
- Class <? extends RenderCB> next = cyclercbq.removeFirst();
- cyclercbq.addLast(next);
- _setCyclerCB(next);
- }
- }
- private void prevCyclerCB() {
- synchronized(this) {
- Class <? extends RenderCB> next = cyclercbq.removeLast();
- cyclercbq.addFirst(next);
- _setCyclerCB(next);
- }
- }
- {
- cyclercbq.add(WholeMax.class);
- cyclercbq.add(Grid.class);
- }
-
- // The surface to which this callback is bound is created only after audio permissions
- // have been checked. We therefore can simply start in the created callback. Stopping
- // happens in onPause below, which fires before the surface is destroyed.
- private SurfaceHolder.Callback shc = new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(final SurfaceHolder h) {
- Log.d("shc", "Surface Created");
- Canvas c = h.lockCanvas();
- c.drawColor(Color.RED);
- h.unlockCanvasAndPost(c);
-
- final AudioRecord ar = new AudioRecord(
- MediaRecorder.AudioSource.MIC,
- 11025, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,
- AUDIO_RECORDER_BUFFER_SIZE);
- Log.d("shc", "Audio session ID:" + ar.getAudioSessionId());
-
- cycler = new Thread() {
- public void run() {
- // Raw audio samples
- float[] samples = new float[AUDIO_SAMPLES];
-
- // FFT data and engine
- float[] fft = new float[AUDIO_SAMPLES];
- FloatFFT_1D fftc = new FloatFFT_1D(AUDIO_SAMPLES);
-
- ar.startRecording();
- while (!Thread.interrupted()) {
- final RenderCB rcb;
- ar.read(samples, 0, samples.length, AudioRecord.READ_BLOCKING);
- System.arraycopy(samples,0,fft,0,AUDIO_SAMPLES);
- /*
- * Debug: triangle wave
- */
- /*
- for(int i = 0; i < samples.length; i++) {
- samples[i] = (float)((i % 16) - 8) / 8;
- if (i % 32 >= 16) { samples[i] *= -1; }
- }
- */
-
- fftc.realForward(fft);
-
- synchronized(this) {
- rcb = cyclercb;
- }
-
- Canvas cv = h.lockCanvas();
- if (cv == null) {
- // the surface must have been destroyed out from under us;
- // just stop here.
- break;
- }
- cv.drawColor(Color.BLACK);
- rcb.render(cv,samples,fft);
- h.unlockCanvasAndPost(cv);
-
- // try { Thread.sleep(100); } catch (InterruptedException e) { break; }
- }
- ar.stop();
- ar.release();
- Log.d("cycler", "exit");
- }
- };
- cycler.start();
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- Log.d("shc", "Surface Changed");
- ;
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- Log.d("shc", "Surface Destroyed");
- }
- };
+ private SurfaceView mACSurfaceView;
+ private AudioCanvas mAudioCanvas;
private void createSurface() {
Log.d("createSurface", "top");
-
- nextCyclerCB();
-
- cyclersv = new SurfaceView(this);
- cyclersv.getHolder().addCallback(shc);
- cyclersv.setOnClickListener(new View.OnClickListener(){
+ mACSurfaceView = new SurfaceView(this);
+ mAudioCanvas = new AudioCanvas(mDebugView, mACSurfaceView);
+ mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- final RenderCB rcb;
- synchronized(this) { rcb = cyclercb; }
- rcb.onClick();
+ mAudioCanvas.prevCyclerCB();
}
});
- mTextView.setOnClickListener(new View.OnClickListener() {
+ mACSurfaceView.setOnClickListener(new View.OnClickListener(){
@Override
- public void onClick(View v) {
- prevCyclerCB();
+ public void onClick(View v) { mAudioCanvas.onClick(); }
+ });
+ mACSurfaceView.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ mAudioCanvas.nextCyclerCB();
+ return true;
}
});
- cyclersv.setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- nextCyclerCB();
- return true;
- }
- });
-
- mInnerContainer.addView(cyclersv, -1,
+ mInnerContainer.addView(mACSurfaceView, -1,
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
}
private void removeSurface() {
- Log.d("removeSurface", "stopping cycler");
- if(cycler != null) {
- cycler.interrupt();
- try { cycler.join(); }
- catch (InterruptedException e) { Log.d("shc", "IE while join cycler"); }
- }
Log.d("removeSurface", "removing view");
- mInnerContainer.removeView(cyclersv);
- cyclersv = null;
+ mInnerContainer.removeView(mACSurfaceView);
Log.d("removeSurface", "done");
}
defaultConfig {
applicationId "com.acmetensortoys.watchviz"
- minSdkVersion 9
+ minSdkVersion 23
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
dependencies {
- compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:23.4.0'
+ compile project(':vizlib')
}
dependencies {
-include ':app', ':phoneapp'
+include ':vizlib', ':app', ':phoneapp'
--- /dev/null
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.3"
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ testCompile 'junit:junit:4.12'
+}
--- /dev/null
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/nwf/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
--- /dev/null
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.acmetensortoys.vizlib">
+
+ <application android:allowBackup="true"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+>
+
+ </application>
+
+</manifest>
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.TextView;
+
+import com.acmetensortoys.watchviz.vizlib.render.Grid;
+import com.acmetensortoys.watchviz.vizlib.render.WholeMax;
+
+import org.jtransforms.fft.FloatFFT_1D;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+public class AudioCanvas {
+ private TextView mDebugView;
+ private SurfaceView mSurfaceView;
+
+ private RenderCB renderCB;
+ private Deque<Class<? extends RenderCB>> cbq = new ArrayDeque<>();
+ private Thread cycler;
+
+ public AudioCanvas(TextView debugView, SurfaceView surfaceView) {
+ this.mDebugView = debugView;
+ this.mSurfaceView = surfaceView;
+
+ mSurfaceView.getHolder().addCallback(shc);
+
+ cbq.add(Grid.class);
+ cbq.add(WholeMax.class);
+ nextCyclerCB();
+ }
+
+ private void _setCyclerCB(Class<? extends RenderCB> next) {
+ try {
+ renderCB = next.getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ String name = renderCB.getClass().getSimpleName();
+ mDebugView.setText(name.substring(0,Math.min(10,name.length())));
+ }
+ public void nextCyclerCB() {
+ synchronized(this) {
+ Class <? extends RenderCB> next = cbq.removeFirst();
+ cbq.addLast(next);
+ _setCyclerCB(next);
+ }
+ }
+ public void prevCyclerCB() {
+ synchronized(this) {
+ Class <? extends RenderCB> next = cbq.removeLast();
+ cbq.addFirst(next);
+ _setCyclerCB(next);
+ }
+ }
+ public void onClick() {
+ final RenderCB rcb;
+ synchronized(this) { rcb = renderCB; }
+ rcb.onClick();
+ }
+
+ // The surface to which this callback is bound is created only after audio permissions
+ // have been checked. We therefore can simply start in the created callback. Stopping
+ // happens in onPause below, which fires before the surface is destroyed.
+ private SurfaceHolder.Callback shc = new SurfaceHolder.Callback() {
+
+ private static final int AUDIO_RECORDER_BUFFER_SIZE = 2048;
+ private static final int AUDIO_SAMPLES = 512;
+
+ @Override
+ public void surfaceCreated(final SurfaceHolder h) {
+ Log.d("shc", "Surface Created");
+ Canvas c = h.lockCanvas();
+ c.drawColor(Color.RED);
+ h.unlockCanvasAndPost(c);
+
+ final AudioRecord ar = new AudioRecord(
+ MediaRecorder.AudioSource.MIC,
+ 11025, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,
+ AUDIO_RECORDER_BUFFER_SIZE);
+ Log.d("shc", "Audio session ID:" + ar.getAudioSessionId());
+
+ cycler = new Thread() {
+ public void run() {
+ // Raw audio samples
+ float[] samples = new float[AUDIO_SAMPLES];
+
+ // FFT data and engine
+ float[] fft = new float[AUDIO_SAMPLES];
+ FloatFFT_1D fftc = new FloatFFT_1D(AUDIO_SAMPLES);
+
+ ar.startRecording();
+ while (!Thread.interrupted()) {
+ final RenderCB rcb;
+ ar.read(samples, 0, samples.length, AudioRecord.READ_BLOCKING);
+ System.arraycopy(samples,0,fft,0,AUDIO_SAMPLES);
+ /*
+ * Debug: triangle wave
+ */
+ /*
+ for(int i = 0; i < samples.length; i++) {
+ samples[i] = (float)((i % 16) - 8) / 8;
+ if (i % 32 >= 16) { samples[i] *= -1; }
+ }
+ */
+
+ fftc.realForward(fft);
+
+ synchronized(this) {
+ rcb = renderCB;
+ }
+
+ Canvas cv = h.lockCanvas();
+ if (cv == null) {
+ // the surface must have been destroyed out from under us;
+ // just stop here.
+ break;
+ }
+ cv.drawColor(Color.BLACK);
+ rcb.render(cv,samples,fft);
+ h.unlockCanvasAndPost(cv);
+
+ // try { Thread.sleep(100); } catch (InterruptedException e) { break; }
+ }
+ ar.stop();
+ ar.release();
+ Log.d("cycler", "exit");
+ }
+ };
+ cycler.start();
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.d("shc", "Surface Changed");
+ ;
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.d("shc", "Surface Destroyed");
+ if(cycler != null) {
+ cycler.interrupt();
+ try { cycler.join(); }
+ catch (InterruptedException e) { Log.d("shc", "IE while join cycler"); }
+ }
+ }
+ };
+}
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib;
+
+public interface Meta {
+ float get();
+ void update(float last);
+}
-package com.acmetensortoys.watchviz;
+package com.acmetensortoys.watchviz.vizlib;
import android.graphics.Canvas;
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib.meta;
+
+import com.acmetensortoys.watchviz.vizlib.Meta;
+
+public class Avg implements Meta {
+ public float window = 0f;
+ public int ix = 0;
+ public float[] list;
+
+ public Avg(int shift) {
+ list = new float[1<<shift];
+ }
+
+ @Override
+ public float get() {
+ return window / list.length;
+ }
+
+ @Override
+ public void update(float last) {
+ window -= list[ix];
+ list[ix] = last;
+
+ if(ix == list.length-1) {
+ window = 0;
+ for(float v : list) { window += v; }
+ ix = 0;
+ } else {
+ window += list[ix];
+ ix += 1;
+ }
+ }
+}
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib.meta;
+
+import com.acmetensortoys.watchviz.vizlib.Meta;
+
+public class Max implements Meta {
+ public float window = Float.NEGATIVE_INFINITY;
+ public int ix = 0;
+ final public float[] list;
+
+ public Max(int shift) {
+ if (shift < 0 || shift > 16) {
+ throw new RuntimeException("Improper shift: " + shift);
+ }
+ list = new float[1<<shift];
+ }
+
+ @Override
+ public float get() {
+ return window;
+ }
+
+ @Override
+ public void update(float last) {
+ if (last > window) {
+ // If new thing is bigger, set both...
+ window = list[ix] = last;
+ } else if (list[ix] < window) {
+ // if smaller but current ix is not witness...
+ list[ix] = last;
+ } else {
+ // Whoop, smaller and current thing maybe is the witness...
+ list[ix] = last;
+ window = last;
+ for (float f : list) { window = Math.max(window, f); }
+ }
+
+ ix = (ix + 1) % list.length;
+ }
+}
-package com.acmetensortoys.watchviz.render;
+package com.acmetensortoys.watchviz.vizlib.render;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
-import com.acmetensortoys.watchviz.RenderCB;
+import com.acmetensortoys.watchviz.vizlib.RenderCB;
+import com.acmetensortoys.watchviz.vizlib.meta.Avg;
public final class Grid extends RenderCB {
private boolean doDebug;
private final Paint dbp = new Paint();
private float[] hsv = new float[]{0.0f, 1.0f, 1.0f};
+ // 2^7 == 128 frames, at 512 samples per frame and 11025 KHz, this works out
+ // to six seconds, which seems fine.
+ private Avg meta = new Avg(7);
+
public Grid()
{
dbp.setColor(Color.WHITE);
hsv[0] = hsv[0] >= 359 ? 0.0f : hsv[0] + 1.0f;
p.setColor(c);
- final float scale = 32;
+ float thisFrameSum = 0f;
+ final float winAvg = meta.get();
int rxs = cv.getWidth() / 8;
int rys = cv.getHeight() / 8;
for (int rx = 0; rx < 8; rx++) {
for (int ry = 0; ry < 8; ry++) {
- int ix = (rx * 8 + ry) * 4;
- float x = (Math.abs(fft[ix]) + Math.abs(fft[ix+2])) * scale;
- int b = x > 255 ? 255 : (int)x;
- p.setAlpha(b > 223 ? 255 : b + 32);
+ int ix = (rx * 8 + ry) * 4 + 2;
+ float x = (Math.abs(fft[ix]) + Math.abs(fft[ix+2]));
+
+ thisFrameSum += x;
+
+ // Two points define a line: (0 -> 0x20), (meta -> 0x60), cap at 0xFF.
+ float sx = (0x40 / winAvg) * x + 0x20;
+ int b = sx > 255 ? 255 : (int)sx;
+
+ p.setAlpha(b);
cv.drawRect(rx * rxs, ry * rys, (rx + 1) * rxs - 1, (ry + 1) * rys - 1, p);
if(doDebug) {
cv.drawText(Integer.toHexString(b), rx*rxs + rxs/2, ry*rys + rys/2, dbp);
}
}
}
+
+ meta.update(thisFrameSum/64);
}
};
-package com.acmetensortoys.watchviz.render;
+package com.acmetensortoys.watchviz.vizlib.render;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
-import com.acmetensortoys.watchviz.RenderCB;
+import com.acmetensortoys.watchviz.vizlib.RenderCB;
import java.util.Locale;
public void render(Canvas cv, float[] audio, float[] fft) {
float msamp = 0.0f;
int mix = -1;
- /* Restrict search to lowest half in agreement with Grid */
- for (int i = 0; i < fft.length/2; i += 2) {
+ /* Restrict search to lowest half in agreement with Grid, and skip DC component */
+ for (int i = 2; i < fft.length/2; i += 2) {
if (fft[i] > msamp) {
msamp = fft[i];
mix = i;
package org.jtransforms.fft;\r
\r
import org.jtransforms.utils.CommonUtils;\r
-import static org.apache.commons.math3.util.FastMath.*;\r
+// import static org.apache.commons.math3.util.FastMath.*;\r
+import static java.lang.Math.*;\r
\r
/**\r
* Computes 1D Discrete Fourier Transform (DFT) of complex and real, single\r
* ***** END LICENSE BLOCK ***** */
package org.jtransforms.utils;
-import static org.apache.commons.math3.util.FastMath.*;
+// import static org.apache.commons.math3.util.FastMath.*;
+import static java.lang.Math.*;
/**
* Static utility methods.
--- /dev/null
+<resources>
+ <string name="app_name">Viz Lib</string>
+</resources>