Plumb a GridViewPager into being. Lots of wrestling with drawing gunk.
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+/*
+ This is the glue code that provides the Activity that Android interacts with.
+ Most of the actual UI work of interest is in MainGridViewAdapter.
+ */
+
package com.acmetensortoys.watchviz;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.wearable.activity.WearableActivity;
-import android.support.wearable.view.BoxInsetLayout;
+import android.support.wearable.view.GridViewPager;
import android.util.Log;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
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 static final SimpleDateFormat AMBIENT_DATE_FORMAT =
new SimpleDateFormat("HH:mm", Locale.US);
+ private static final int TIME_UPDATE_NOT_AMBIENT_PERIOD = 2000;
- private BoxInsetLayout mOuterContainer;
- private LinearLayout mInnerContainer;
+ private Handler mHandler;
+ private RelativeLayout mTextContainer;
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(mACSurfaceView, mAudioProvider);
+ private GridViewPager mGridView;
- final RenderingSelector rs = new RenderingSelector(mDebugView, mAudioCanvas);
-
- mTextView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- rs.prevCyclerCB();
- }
- });
- mACSurfaceView.setOnClickListener(new View.OnClickListener(){
- @Override
- public void onClick(View v) { mAudioCanvas.onClick(); }
- });
- mACSurfaceView.setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- rs.nextCyclerCB();
- return true;
- }
- });
- mInnerContainer.addView(mACSurfaceView, -1,
- new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT));
+ private void createSurfaces() {
+ Log.d("createSurfaces", "top");
+ MainGridViewAdapter mgva = new MainGridViewAdapter(this, mAudioProvider, mDebugView);
+ mGridView.setOnPageChangeListener(mgva);
+ mGridView.setAdapter(mgva);
+ // Fake the selection callback. Why doesn't this happen automatically? :(
+ mgva.onPageSelected(mGridView.getCurrentItem().x, mGridView.getCurrentItem().y);
}
- private void removeSurface() {
- Log.d("removeSurface", "removing view");
- mInnerContainer.removeView(mACSurfaceView);
- Log.d("removeSurface", "done");
+ private void removeSurfaces() {
+ Log.d("removeSurfaces", "removing grid view");
+ mGridView.setAdapter(null);
+ Log.d("removeSurfaces", "done");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ // grab an interface to our local looper
+ mHandler = new Handler();
+
setContentView(R.layout.activity_main);
setAmbientEnabled();
- mOuterContainer = (BoxInsetLayout) findViewById(R.id.top_container);
- mInnerContainer = (LinearLayout) findViewById(R.id.linear_container);
+ mTextContainer = (RelativeLayout) findViewById(R.id.text_container);
mTextView = (TextView) findViewById(R.id.text);
mDebugView = (TextView) findViewById(R.id.dbg);
mClockView = (TextView) findViewById(R.id.clock);
+ mGridView = (GridViewPager) findViewById(R.id.grid);
+ // mGridView.setBackgroundColor(Color.WHITE);
+ // mGridView.setDrawingCacheBackgroundColor(Color.WHITE);
+ // mGridView.setOverScrollMode(View.OVER_SCROLL_NEVER);
updateDisplay();
}
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, 1);
} else {
- createSurface();
+ createSurfaces();
}
Log.d("onStart", "BLE: " + getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE));
public void onRequestPermissionsResult(int rq, @NonNull String[] ps, @NonNull int[] rs) {
if(rq == 1) {
if (rs[0] == PackageManager.PERMISSION_GRANTED) {
- createSurface();
+ createSurfaces();
} else {
mDebugView.setText("No audio");
}
@Override
public void onEnterAmbient(Bundle ambientDetails) {
super.onEnterAmbient(ambientDetails);
+ mHandler.removeCallbacks(updateTimeNotAmbient);
updateDisplay();
}
- @Override
- public void onUpdateAmbient() {
- super.onUpdateAmbient();
- mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
- }
-
@Override
public void onExitAmbient() {
- updateDisplay();
super.onExitAmbient();
+ updateDisplay();
+ mHandler.postDelayed(updateTimeNotAmbient, TIME_UPDATE_NOT_AMBIENT_PERIOD);
}
private void updateDisplay() {
final int black = getResources().getColor(android.R.color.black,null);
- mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
+ final int white = getResources().getColor(android.R.color.white,null);
+ updateTime();
if (isAmbient()) {
- final int white = getResources().getColor(android.R.color.white,null);
- mOuterContainer.setBackgroundColor(black);
+ mTextContainer.setBackgroundColor(black);
mTextView.setTextColor(white);
mClockView.setTextColor(white);
mDebugView.setTextColor(white);
} else {
- mOuterContainer.setBackground(null);
+ mTextContainer.setBackgroundColor(white);
mTextView.setTextColor(black);
mClockView.setTextColor(black);
mDebugView.setTextColor(black);
}
}
+ @Override
+ public void onUpdateAmbient() {
+ super.onUpdateAmbient();
+ updateTime();
+ }
+
+ private Runnable updateTimeNotAmbient = new Runnable() {
+ @Override
+ public void run() {
+ updateTime();
+ mHandler.postDelayed(this, TIME_UPDATE_NOT_AMBIENT_PERIOD);
+ }
+ };
+
+ private void updateTime() {
+ mClockView.post(new Runnable() {
+ @Override
+ public void run() {
+ mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
+ }
+ });
+ }
+
@Override
public void onPause() {
Log.d("onPause", "top");
@Override
public void onStop() {
Log.d("onStop", "top");
- removeSurface();
+ removeSurfaces();
super.onStop();
}
--- /dev/null
+/*
+ AcmeTensorToys WatchViz: a sound visualization application for Android Wear
+ Copyright (C) 2016 Nathaniel Wesley Filardo
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* This file is responsible for navigation of the UI, through different renderers
+ and (eventually), their settings, as well as global preferences and all that.
+ */
+
+
+package com.acmetensortoys.watchviz;
+
+import android.content.Context;
+import android.support.wearable.view.GridPagerAdapter;
+import android.support.wearable.view.GridViewPager;
+import android.util.Log;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.acmetensortoys.watchviz.vizlib.AudioCanvas;
+import com.acmetensortoys.watchviz.vizlib.AudioProvider;
+import com.acmetensortoys.watchviz.vizlib.Rendering;
+import com.acmetensortoys.watchviz.vizlib.rendering.Grid;
+import com.acmetensortoys.watchviz.vizlib.rendering.WholeMax;
+
+public class MainGridViewAdapter
+ extends GridPagerAdapter
+ implements GridViewPager.OnPageChangeListener {
+
+ /* All returned Objects are actually Pages so we know what to do with them later */
+ private abstract static class InstantiatedPage {
+ abstract public boolean ownsview(View v);
+ abstract public void deinstantiate(MainGridViewAdapter m, ViewGroup g);
+ };
+ private static class ViewInstantiatedPage extends InstantiatedPage {
+ public View v;
+ public ViewInstantiatedPage(View v) { this.v = v; }
+ @Override
+ public boolean ownsview(View v2) { return v.equals(v2); }
+ @Override
+ public void deinstantiate(MainGridViewAdapter m, ViewGroup g) { g.removeView(v); }
+ }
+ private abstract static class Page {
+ public String title;
+ abstract public InstantiatedPage instantiate(MainGridViewAdapter m, ViewGroup g, int r, int c);
+ }
+ private static class RendererPage extends Page {
+ private Class<? extends Rendering> re;
+ public RendererPage(Class<? extends Rendering> r) {
+ this.re = r;
+ this.title = r.getSimpleName();
+ }
+ public InstantiatedPage instantiate(MainGridViewAdapter mgva, ViewGroup g, int r, int c) {
+ SurfaceView sv = new SurfaceView(mgva.mCtx);
+ AudioCanvas ac = new AudioCanvas(r+"x"+c, sv, mgva.mAp);
+ try {
+ ac.setRendering(re.getConstructor().newInstance());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ g.addView(sv, -1,
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ return new ViewInstantiatedPage(sv);
+ }
+ }
+
+ private Page gridPage = new RendererPage(Grid.class);
+ private Page wholePage = new RendererPage(WholeMax.class);
+ private final Page[][] pages = {
+ {gridPage, gridPage},
+ {wholePage }
+ };
+
+ private final Context mCtx;
+ private final AudioProvider mAp;
+ private final TextView mDv;
+
+ public MainGridViewAdapter(Context c, AudioProvider ap, TextView dv) {
+ mCtx = c;
+ mAp = ap;
+ mDv = dv;
+ }
+
+ @Override
+ public int getRowCount() {
+ return pages.length;
+ }
+
+ @Override
+ public int getCurrentColumnForRow(int row, int currentColumn) {
+ return 0;
+ }
+
+ @Override
+ public int getColumnCount(int i) {
+ return pages[i].length;
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int row, int col) {
+ Log.d("MGVA", "instantiate:" + row + "x" + col);
+ return pages[row][col].instantiate(this, container, row, col);
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object o) {
+ return ((InstantiatedPage)o).ownsview(view);
+ }
+
+ @Override
+ public void destroyItem(ViewGroup viewGroup, int row, int col, Object o) {
+ Log.d("MGVA", "destroy:" + row + "x" + col);
+ ((InstantiatedPage)o).deinstantiate(this, viewGroup);
+ }
+
+ @Override
+ public void startUpdate(ViewGroup container) {
+ // Log.d("MGVA", "startUpdate top");
+ super.startUpdate(container);
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ // super.finishUpdate(container);
+ Log.d("MGVA", "finishUpdate bottom");
+ }
+
+ @Override
+ public void onPageScrolled(int i, int i1, float v, float v1, int i2, int i3) {
+ // Log.d("MGVA", "scrolled");
+ }
+
+ @Override
+ public void onPageSelected(int row, int col) {
+ // Log.d("MGVA", "selected");
+ final String name = pages[row][col].title;
+ mDv.post(new Runnable() {
+ @Override
+ public void run() {
+ mDv.setText(name.substring(0, Math.min(10, name.length())));
+ }
+ });
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int i) {
+ ;
+ }
+}
<?xml version="1.0" encoding="utf-8"?>
-<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/top_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.acmetensortoys.watchviz.MainActivity"
- tools:deviceIds="wear">
+ tools:deviceIds="wear"
+ android:id="@+id/outermost"
+ android:background="@color/blue">
- <LinearLayout
- android:id="@+id/linear_container"
+ <!-- Argh. Oh boy did I lose a lot of time to this. The GridViewPager
+ needs to be the first child in the tree so that it gets drawn first.
+ That seems hideously wrong, because it means that it's getting its size
+ information before the relative constraint solver does its thing, but
+ at this point I am exceptionally out of patience for the whole Android
+ UI layer, so here we are. -->
+
+ <!-- TODO Someone still isn't getting their background correct; the watch face
+ bleeds though around the edges of the scrolling. Have I mentioned that I hate
+ everything ever? -->
+
+ <android.support.wearable.view.GridViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
-
- <RelativeLayout
- android:orientation="horizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:id="@+id/grid"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="0dp"
+ android:layout_below="@+id/text_container"
+ android:background="@color/green"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true">
+ </android.support.wearable.view.GridViewPager>
- <TextView
- android:id="@+id/text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/acmetensor"
- app:layout_box="all"
- android:layout_alignParentLeft="true"
- android:layout_alignParentTop="true"
- android:layout_alignParentStart="true" />
+ <RelativeLayout
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/text_container"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="0dp"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="0dp"
+ android:background="@color/orange"
+ android:transitionGroup="false"
+ android:layout_alignParentRight="true">
- <TextView
- android:text="@string/acmetensor"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:id="@+id/dbg"
- android:layout_alignParentTop="true"
- android:layout_centerHorizontal="true"
- android:layout_alignParentBottom="false"
- android:layout_alignParentRight="false"
- android:layout_alignParentLeft="false"
- android:layout_alignWithParentIfMissing="false" />
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/acmetensor"
+ app:layout_box="all"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true" />
- <TextView
- android:id="@+id/clock"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textColor="@android:color/white"
- app:layout_box="all"
- android:layout_alignParentTop="true"
- android:layout_alignParentEnd="true"
- android:layout_alignParentRight="false" />
+ <TextView
+ android:text="@string/acmetensor"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/dbg"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentBottom="false"
+ android:layout_alignParentRight="false"
+ android:layout_alignParentLeft="false"
+ android:layout_alignWithParentIfMissing="false" />
- </RelativeLayout>
+ <TextView
+ android:id="@+id/clock"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@android:color/white"
+ app:layout_box="all"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="false" />
- <!-- Use placeholder? -->
- <!-- <SurfaceView
- android:layout_width="match_parent"
- android:layout_height="fill_parent"
- android:id="@+id/surface" /> -->
+ </RelativeLayout>
- </LinearLayout>
-</android.support.wearable.view.BoxInsetLayout>
+</RelativeLayout>
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Rect;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
public class AudioCanvas implements Renderer {
- private AudioProvider mAudioProvider;
- private SurfaceView mSurfaceView;
- private Rendering rendering;
+ private final AudioProvider mAp;
+ private final SurfaceView mSv;
+ private AudioReceiver mAr;
+ private Rendering mRend;
+ private Rect gvr = new Rect();
- public AudioCanvas(SurfaceView surfaceView, AudioProvider ap) {
- this.mSurfaceView = surfaceView;
- this.mAudioProvider = ap;
+ public AudioCanvas(final String debug1, SurfaceView sv, AudioProvider ap) {
+ this.mAp = ap;
+ this.mSv = sv;
+ sv.getHolder().addCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(final SurfaceHolder h) {
+ Log.d("shc:" + debug1, "Surface Created");
+ Canvas c = h.lockCanvas();
+ c.drawColor(Color.RED);
+ h.unlockCanvasAndPost(c);
- mSurfaceView.getHolder().addCallback(shc);
+ synchronized(AudioCanvas.this) {
+ if (mAr != null) {
+ Log.d("shc:" + debug1, "Create with existing mAr?");
+ mAp.remove(mAr);
+ }
+ mAr = new AudioReceiver() {
+ @Override
+ public void onAudio(float[] audio, float[] fft) {
+ final Rendering rcb;
+ synchronized (AudioCanvas.this) {
+ rcb = mRend;
+ if (rcb == null) {
+ mAp.remove(this);
+ }
+ }
+ Canvas cv = h.lockCanvas();
+ if (cv == null) {
+ // the surface must have been destroyed out from under us;
+ // 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, audio, fft);
+ h.unlockCanvasAndPost(cv);
+ }
+ };
+ }
+ }
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.d("shc:"+debug1, "Surface Changed:"+format+":"+width+":"+height);
+ ;
+ }
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.d("shc:"+debug1, "Surface Destroyed");
+ synchronized(AudioCanvas.this) {
+ mAp.remove(mAr);
+ mAr = null;
+ }
+ }
+ });
+ sv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ manageSubscription();
+ return true;
+ }
+ });
+ sv.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Rendering rcb;
+ synchronized(AudioCanvas.this) { rcb = mRend; }
+ if (rcb != null) {
+ rcb.onClick();
+ }
+ }
+ });
}
public void setRendering(Rendering r) {
- synchronized(this) { rendering = r; }
- }
-
- public void onClick() {
- final Rendering rcb;
- synchronized(this) { rcb = rendering; }
- rcb.onClick();
+ synchronized(AudioCanvas.this) {
+ mRend = r;
+ manageSubscription();
+ }
}
-
- // 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 AudioReceiver ar;
-
- @Override
- public void surfaceCreated(final SurfaceHolder h) {
- Log.d("shc", "Surface Created");
- Canvas c = h.lockCanvas();
- c.drawColor(Color.RED);
- h.unlockCanvasAndPost(c);
-
- synchronized(this) {
- if (ar != null) {
- Log.d("shc", "Create with existing ar?");
- mAudioProvider.remove(ar);
+ private void manageSubscription() {
+ boolean gvrf = mSv.getGlobalVisibleRect(gvr);
+ // Log.d("asv:"+debug1,"manageSubscription:gvrf="+gvrf+":gvr="+gvr.toString());
+ synchronized(AudioCanvas.this) {
+ if (mAr != null) {
+ // Surface has been created
+ if (gvrf && mRend != null) {
+ // Visible and have rendering
+ mAp.add(mAr);
+ } else {
+ // Invisible or no rendering set
+ mAp.remove(mAr);
}
- 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, on the presumption that we will be
- // removed from the provider by the hook below
- return;
- }
- cv.drawColor(Color.BLACK);
- rcb.render(cv, audio, fft);
- h.unlockCanvasAndPost(cv);
- }
- };
- mAudioProvider.add(ar);
}
}
-
- @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");
- synchronized(this) {
- mAudioProvider.remove(ar);
- ar = null;
- }
- }
- };
+ }
}
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_SAMPLE_HZ = 11025;
private static final int AUDIO_RECORDER_BUFFER_SIZE = 2048;
private static final int AUDIO_SAMPLES = 512;
+ // AudioReceiver callbacks run every (AUDIO_SAMPLES / AUDIO_SAMPLE_HZ) seconds.
+
private Set<AudioReceiver> audioReceivers;
private Thread audioSourceThread;
public void add(AudioReceiver ar) {
synchronized(this) {
boolean empty = audioReceivers.isEmpty();
- audioReceivers.add(ar);
+ boolean did = audioReceivers.add(ar);
if (empty) {
+ Log.d("AudioProvider", "Starting audio source thread...");
this.start();
+ } else if (did) {
+ Log.d("AudioProvider", "Added receiver; now=" + audioReceivers.size());
}
}
}
-
public void remove(AudioReceiver ar) {
synchronized(this) {
- audioReceivers.remove(ar);
+ boolean did = audioReceivers.remove(ar);
+ if(!did) {
+ // Nothing to do!
+ return;
+ }
if(audioReceivers.isEmpty()) {
+ Log.d("AudioProvider", "Stopping audio source thread...");
this.stop();
+ } else {
+ Log.d("AudioProvider", "Removed receiver; left=" + audioReceivers.size());
}
}
}
-
public AudioProvider() {
- audioReceivers = Collections.synchronizedSet(new HashSet<AudioReceiver>());
+ audioReceivers = new HashSet<AudioReceiver>();
}
private void start() {
final AudioRecord ar = new AudioRecord(
MediaRecorder.AudioSource.MIC,
- 11025, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,
+ AUDIO_SAMPLE_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,
AUDIO_RECORDER_BUFFER_SIZE);
Log.d("shc", "Audio session ID:" + ar.getAudioSessionId());
System.arraycopy(samples, 0, fft, 0, AUDIO_SAMPLES);
fftc.realForward(fft);
- for(AudioReceiver ar : audioReceivers) {
+ // Snapshot the current callbacks and iterate that. This prevents
+ // concurrent mutation exceptions, at the cost of being kind of terrible.
+ Set<AudioReceiver> ars;
+ synchronized(this) {
+ ars = new HashSet<>(audioReceivers);
+ }
+ for (AudioReceiver ar : ars) {
ar.onAudio(samples, fft);
}
}
}
};
+ audioSourceThread.start();
}
-
private void stop() {
synchronized(this) {
- audioSourceThread.interrupt();
-
- try { audioSourceThread.join(); }
- catch (InterruptedException ie) { ; }
- finally { audioSourceThread = null; }
+ if (audioSourceThread != null) {
+ audioSourceThread.interrupt();
+
+ try {
+ audioSourceThread.join();
+ } catch (InterruptedException ie) {
+ ;
+ } finally {
+ audioSourceThread = null;
+ }
+ }
}
}
}
public abstract class Rendering {
abstract public void render(Canvas c, float[] audio, float[] fft);
- public void onClick() { }
+ 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();
- }
- }
-}