From: Nathaniel Wesley Filardo Date: Fri, 17 Jun 2016 06:19:20 +0000 (-0400) Subject: Some heavy refactoring X-Git-Url: https://hydra-www.ietfng.org/gitweb/?a=commitdiff_plain;h=58ff7e25de5bfd5c4737c01e71283dfeabb92387;p=acmetensortoys-watchviz Some heavy refactoring Move visualizations and surface off to its own library Add Meta-data tracking classes Improve Grid visualization by using Avg to scale --- diff --git a/app/build.gradle b/app/build.gradle index fd20a0d..ed0441b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,9 +26,10 @@ dependencies { 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 diff --git a/app/src/main/java/com/acmetensortoys/watchviz/MainActivity.java b/app/src/main/java/com/acmetensortoys/watchviz/MainActivity.java index 4d5020d..b2ec4ff 100644 --- a/app/src/main/java/com/acmetensortoys/watchviz/MainActivity.java +++ b/app/src/main/java/com/acmetensortoys/watchviz/MainActivity.java @@ -19,208 +19,65 @@ 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> cyclercbq = new ArrayDeque<>(); - private void _setCyclerCB(Class 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 next = cyclercbq.removeFirst(); - cyclercbq.addLast(next); - _setCyclerCB(next); - } - } - private void prevCyclerCB() { - synchronized(this) { - Class 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"); } diff --git a/phoneapp/build.gradle b/phoneapp/build.gradle index e46e822..df5573a 100644 --- a/phoneapp/build.gradle +++ b/phoneapp/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "com.acmetensortoys.watchviz" - minSdkVersion 9 + minSdkVersion 23 targetSdkVersion 23 versionCode 1 versionName "1.0" @@ -20,8 +20,9 @@ android { } 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 { diff --git a/settings.gradle b/settings.gradle index b8c9776..c007f84 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':phoneapp' +include ':vizlib', ':app', ':phoneapp' diff --git a/vizlib/.gitignore b/vizlib/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/vizlib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/vizlib/build.gradle b/vizlib/build.gradle new file mode 100644 index 0000000..b279fba --- /dev/null +++ b/vizlib/build.gradle @@ -0,0 +1,24 @@ +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' +} diff --git a/vizlib/proguard-rules.pro b/vizlib/proguard-rules.pro new file mode 100644 index 0000000..23c6bbc --- /dev/null +++ b/vizlib/proguard-rules.pro @@ -0,0 +1,17 @@ +# 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 *; +#} diff --git a/vizlib/src/main/AndroidManifest.xml b/vizlib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..27c719d --- /dev/null +++ b/vizlib/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/AudioCanvas.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/AudioCanvas.java new file mode 100644 index 0000000..f498ff9 --- /dev/null +++ b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/AudioCanvas.java @@ -0,0 +1,157 @@ +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> 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 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 next = cbq.removeFirst(); + cbq.addLast(next); + _setCyclerCB(next); + } + } + public void prevCyclerCB() { + synchronized(this) { + Class 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"); } + } + } + }; +} diff --git a/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/Meta.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/Meta.java new file mode 100644 index 0000000..f90b9b8 --- /dev/null +++ b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/Meta.java @@ -0,0 +1,6 @@ +package com.acmetensortoys.watchviz.vizlib; + +public interface Meta { + float get(); + void update(float last); +} diff --git a/app/src/main/java/com/acmetensortoys/watchviz/RenderCB.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/RenderCB.java similarity index 79% rename from app/src/main/java/com/acmetensortoys/watchviz/RenderCB.java rename to vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/RenderCB.java index b61723a..ace217b 100644 --- a/app/src/main/java/com/acmetensortoys/watchviz/RenderCB.java +++ b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/RenderCB.java @@ -1,4 +1,4 @@ -package com.acmetensortoys.watchviz; +package com.acmetensortoys.watchviz.vizlib; import android.graphics.Canvas; diff --git a/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/meta/Avg.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/meta/Avg.java new file mode 100644 index 0000000..09424bf --- /dev/null +++ b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/meta/Avg.java @@ -0,0 +1,33 @@ +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< 16) { + throw new RuntimeException("Improper shift: " + shift); + } + list = new float[1< 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; + } +} diff --git a/app/src/main/java/com/acmetensortoys/watchviz/render/Grid.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/render/Grid.java similarity index 60% rename from app/src/main/java/com/acmetensortoys/watchviz/render/Grid.java rename to vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/render/Grid.java index ff9ad4b..cdd64cd 100644 --- a/app/src/main/java/com/acmetensortoys/watchviz/render/Grid.java +++ b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/render/Grid.java @@ -1,10 +1,11 @@ -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; @@ -12,6 +13,10 @@ public final class Grid extends RenderCB { 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); @@ -28,21 +33,30 @@ public final class Grid extends RenderCB { 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); } }; diff --git a/app/src/main/java/com/acmetensortoys/watchviz/render/WholeMax.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/render/WholeMax.java similarity index 86% rename from app/src/main/java/com/acmetensortoys/watchviz/render/WholeMax.java rename to vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/render/WholeMax.java index 04e5bd9..77dba4f 100644 --- a/app/src/main/java/com/acmetensortoys/watchviz/render/WholeMax.java +++ b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/render/WholeMax.java @@ -1,11 +1,11 @@ -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; @@ -28,8 +28,8 @@ public final class WholeMax extends RenderCB { 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; diff --git a/app/src/main/java/org/jtransforms/fft/FloatFFT_1D.java b/vizlib/src/main/java/org/jtransforms/fft/FloatFFT_1D.java similarity index 97% rename from app/src/main/java/org/jtransforms/fft/FloatFFT_1D.java rename to vizlib/src/main/java/org/jtransforms/fft/FloatFFT_1D.java index aa6a990..b37eb5c 100644 --- a/app/src/main/java/org/jtransforms/fft/FloatFFT_1D.java +++ b/vizlib/src/main/java/org/jtransforms/fft/FloatFFT_1D.java @@ -27,7 +27,8 @@ package org.jtransforms.fft; import org.jtransforms.utils.CommonUtils; -import static org.apache.commons.math3.util.FastMath.*; +// import static org.apache.commons.math3.util.FastMath.*; +import static java.lang.Math.*; /** * Computes 1D Discrete Fourier Transform (DFT) of complex and real, single diff --git a/app/src/main/java/org/jtransforms/utils/CommonUtils.java b/vizlib/src/main/java/org/jtransforms/utils/CommonUtils.java similarity index 99% rename from app/src/main/java/org/jtransforms/utils/CommonUtils.java rename to vizlib/src/main/java/org/jtransforms/utils/CommonUtils.java index 31baad5..b3ebc03 100644 --- a/app/src/main/java/org/jtransforms/utils/CommonUtils.java +++ b/vizlib/src/main/java/org/jtransforms/utils/CommonUtils.java @@ -26,7 +26,8 @@ * ***** 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. diff --git a/vizlib/src/main/res/values/strings.xml b/vizlib/src/main/res/values/strings.xml new file mode 100644 index 0000000..dbd1c27 --- /dev/null +++ b/vizlib/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Viz Lib +