import java.util.Locale;
import com.acmetensortoys.watchviz.vizlib.AudioCanvas;
+import com.acmetensortoys.watchviz.vizlib.AudioProvider;
+import com.acmetensortoys.watchviz.vizlib.RenderingSelector;
public class MainActivity extends WearableActivity
{
private TextView mTextView, mClockView, mDebugView;
private SurfaceView mACSurfaceView;
private AudioCanvas mAudioCanvas;
+ private AudioProvider mAudioProvider = new AudioProvider();
private void createSurface() {
Log.d("createSurface", "top");
mACSurfaceView = new SurfaceView(this);
- mAudioCanvas = new AudioCanvas(mDebugView, mACSurfaceView);
+ mAudioCanvas = new AudioCanvas(mACSurfaceView, mAudioProvider);
+
+ final RenderingSelector rs = new RenderingSelector(mDebugView, mAudioCanvas);
+
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- mAudioCanvas.prevCyclerCB();
+ rs.prevCyclerCB();
}
});
mACSurfaceView.setOnClickListener(new View.OnClickListener(){
mACSurfaceView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
- mAudioCanvas.nextCyclerCB();
+ rs.nextCyclerCB();
return true;
}
});
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;
+public class AudioCanvas implements Renderer {
+ private AudioProvider mAudioProvider;
private SurfaceView mSurfaceView;
-
- private RenderCB renderCB;
- private Deque<Class<? extends RenderCB>> cbq = new ArrayDeque<>();
- private Thread cycler;
+ private Rendering rendering;
- public AudioCanvas(TextView debugView, SurfaceView surfaceView) {
- this.mDebugView = debugView;
+ public AudioCanvas(SurfaceView surfaceView, AudioProvider ap) {
this.mSurfaceView = surfaceView;
+ this.mAudioProvider = ap;
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 setRendering(Rendering r) {
+ synchronized(this) { rendering = r; }
}
+
public void onClick() {
- final RenderCB rcb;
- synchronized(this) { rcb = renderCB; }
+ final Rendering rcb;
+ synchronized(this) { rcb = rendering; }
rcb.onClick();
}
// 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;
+ private AudioReceiver ar;
@Override
public void surfaceCreated(final SurfaceHolder h) {
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;
- }
-
+ synchronized(this) {
+ if (ar != null) {
+ Log.d("shc", "Create with existing ar?");
+ mAudioProvider.remove(ar);
+ }
+ ar = new AudioReceiver() {
+ @Override
+ public void onAudio(float[] audio, float[] fft) {
+ final Rendering rcb;
+ synchronized (this) { rcb = rendering; }
Canvas cv = h.lockCanvas();
if (cv == null) {
// the surface must have been destroyed out from under us;
- // just stop here.
- break;
+ // just stop here, on the presumption that we will be
+ // removed from the provider by the hook below
+ return;
}
cv.drawColor(Color.BLACK);
- rcb.render(cv,samples,fft);
+ rcb.render(cv, audio, fft);
h.unlockCanvasAndPost(cv);
-
- // try { Thread.sleep(100); } catch (InterruptedException e) { break; }
}
- ar.stop();
- ar.release();
- Log.d("cycler", "exit");
- }
- };
- cycler.start();
+ };
+ mAudioProvider.add(ar);
+ }
}
@Override
@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"); }
+ synchronized(this) {
+ mAudioProvider.remove(ar);
+ ar = null;
}
}
};
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib;
+
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+
+import org.jtransforms.fft.FloatFFT_1D;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class AudioProvider {
+ private static final int AUDIO_RECORDER_BUFFER_SIZE = 2048;
+ private static final int AUDIO_SAMPLES = 512;
+
+ private Set<AudioReceiver> audioReceivers;
+ private Thread audioSourceThread;
+
+ public void add(AudioReceiver ar) {
+ synchronized(this) {
+ boolean empty = audioReceivers.isEmpty();
+ audioReceivers.add(ar);
+
+ if (empty) {
+ this.start();
+ }
+ }
+ }
+
+ public void remove(AudioReceiver ar) {
+ synchronized(this) {
+ audioReceivers.remove(ar);
+ if(audioReceivers.isEmpty()) {
+ this.stop();
+ }
+ }
+ }
+
+ public AudioProvider() {
+ audioReceivers = Collections.synchronizedSet(new HashSet<AudioReceiver>());
+ }
+
+ private void start() {
+ 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());
+
+ audioSourceThread = 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 Rendering rcb;
+ ar.read(samples, 0, samples.length, AudioRecord.READ_BLOCKING);
+ System.arraycopy(samples, 0, fft, 0, AUDIO_SAMPLES);
+ fftc.realForward(fft);
+
+ for(AudioReceiver ar : audioReceivers) {
+ ar.onAudio(samples, fft);
+ }
+ }
+ }
+ };
+ }
+
+ private void stop() {
+ synchronized(this) {
+ audioSourceThread.interrupt();
+
+ try { audioSourceThread.join(); }
+ catch (InterruptedException ie) { ; }
+ finally { audioSourceThread = null; }
+ }
+ }
+}
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib;
+
+public interface AudioReceiver {
+ void onAudio(float[] audio, float[] fft);
+}
package com.acmetensortoys.watchviz.vizlib;
-public interface Meta {
- float get();
- void update(float last);
+public abstract class Meta {
+ abstract public float get();
+ abstract public void update(float last);
}
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib;
+
+public interface Renderer {
+ void setRendering(Rendering r);
+}
import android.graphics.Canvas;
-public abstract class RenderCB {
+public abstract class Rendering {
abstract public void render(Canvas c, float[] audio, float[] fft);
public void onClick() { }
}
\ No newline at end of file
--- /dev/null
+package com.acmetensortoys.watchviz.vizlib;
+
+import android.widget.TextView;
+
+import com.acmetensortoys.watchviz.vizlib.rendering.Grid;
+import com.acmetensortoys.watchviz.vizlib.rendering.WholeMax;
+
+import java.util.ArrayList;
+
+public class RenderingSelector {
+ private TextView mDebugView;
+ private Renderer mRenderer;
+
+ private int renderCBix;
+ private ArrayList<Class<? extends Rendering>> cbs = new ArrayList<>();
+
+ public RenderingSelector(TextView dv, Renderer r) {
+ mDebugView = dv;
+ mRenderer = r;
+ cbs.add(Grid.class);
+ cbs.add(WholeMax.class);
+ renderCBix = 0;
+ _setCyclerCBByIx();
+ }
+
+ private void _setCyclerCB(Class<? extends Rendering> next) {
+ Rendering rendering;
+ try {
+ rendering = next.getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ mRenderer.setRendering(rendering);
+ String name = rendering.getClass().getSimpleName();
+ mDebugView.setText(name.substring(0,Math.min(10,name.length())));
+ }
+ private void _setCyclerCBByIx() {
+ _setCyclerCB(cbs.get(renderCBix));
+ }
+ public void nextCyclerCB() {
+ synchronized(this) {
+ if (renderCBix == cbs.size()-1) {
+ renderCBix = 0;
+ } else {
+ renderCBix += 1;
+ }
+ _setCyclerCBByIx();
+ }
+ }
+ public void prevCyclerCB() {
+ synchronized(this) {
+ if (renderCBix == 0) {
+ renderCBix = cbs.size()-1;
+ } else {
+ renderCBix -= 1;
+ }
+ _setCyclerCBByIx();
+ }
+ }
+ public void setCBIx(int ix) {
+ if ((ix < 0) || (ix >= cbs.size())) {
+ throw new ArrayIndexOutOfBoundsException(ix);
+ }
+ synchronized(this) {
+ renderCBix = ix;
+ _setCyclerCBByIx();
+ }
+ }
+}
import com.acmetensortoys.watchviz.vizlib.Meta;
-public class Avg implements Meta {
+public class Avg extends Meta {
public float window = 0f;
public int ix = 0;
public float[] list;
import com.acmetensortoys.watchviz.vizlib.Meta;
-public class Max implements Meta {
+public class Max extends Meta {
public float window = Float.NEGATIVE_INFINITY;
public int ix = 0;
final public float[] list;
-package com.acmetensortoys.watchviz.vizlib.render;
+package com.acmetensortoys.watchviz.vizlib.rendering;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
-import com.acmetensortoys.watchviz.vizlib.RenderCB;
+import com.acmetensortoys.watchviz.vizlib.Rendering;
import com.acmetensortoys.watchviz.vizlib.meta.Avg;
-public final class Grid extends RenderCB {
+public final class Grid extends Rendering {
private boolean doDebug;
private final Paint p = new Paint();
private final Paint dbp = new Paint();
-package com.acmetensortoys.watchviz.vizlib.render;
+package com.acmetensortoys.watchviz.vizlib.rendering;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
-import com.acmetensortoys.watchviz.vizlib.RenderCB;
+import com.acmetensortoys.watchviz.vizlib.Rendering;
import java.util.Locale;
-public final class WholeMax extends RenderCB {
+public final class WholeMax extends Rendering {
private boolean doDebug;
private final Paint dbp = new Paint();
private float[] hsv = new float[]{0.0f, 1.0f, 1.0f};