From fe8d5659907b3792513dc389a05b0d0aff77b171 Mon Sep 17 00:00:00 2001 From: Rudolf Zobel Date: Tue, 20 Apr 2021 06:06:06 +0200 Subject: [PATCH 1/2] import and create new branch audioMode --- .idea/compiler.xml | 6 ++++++ .idea/gradle.xml | 3 +++ .idea/jarRepositories.xml | 45 +++++++++++++++++++++++++++++++++++++++ .idea/misc.xml | 2 +- .idea/modules.xml | 8 +++---- 5 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 .idea/compiler.xml create mode 100644 .idea/jarRepositories.xml mode change 100755 => 100644 .idea/modules.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..61a9130c --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 8ba4aa5b..f619f17b 100755 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,8 +1,10 @@ + diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..44eea0ff --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 75dac502..635999df 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100755 new mode 100644 index 46b5571e..4fa28335 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,10 +2,10 @@ - - - - + + + + \ No newline at end of file From 4b1ebacdf9a912278dc6f9a127312870e91422ce Mon Sep 17 00:00:00 2001 From: Rudolf Zobel Date: Sun, 2 May 2021 05:44:29 +0200 Subject: [PATCH 2/2] add audio mode --- common/src/main/AndroidManifest.xml | 17 + .../hyperiongrabber/common/BootActivity.java | 8 +- .../common/HyperionNotification.java | 2 +- .../common/audio/AudioGradientView.java | 194 ++++++++ .../common/audio/HyperionAudioEncoder.java | 446 ++++++++++++++++++ .../audio/HyperionAudioEncoderBase.java | 151 ++++++ .../common/audio/HyperionAudioService.java | 366 ++++++++++++++ .../common/audio/HyperionAudioThread.java | 122 +++++ .../common/util/HyperionGrabberOptions.java | 65 +++ common/src/main/res/values/pref_values.xml | 27 ++ common/src/main/res/values/strings.xml | 13 + common/src/main/res/xml/pref_general.xml | 57 +++ mobile/src/main/AndroidManifest.xml | 2 +- .../mobile/AppCompatPreferenceActivity.java | 4 +- .../hyperiongrabber/mobile/MainActivity.java | 192 ++++++-- .../mobile/SettingsActivity.java | 18 +- mobile/src/main/res/layout/activity_main.xml | 54 ++- mobile/src/main/res/values/styles.xml | 7 + 18 files changed, 1700 insertions(+), 45 deletions(-) create mode 100644 common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/AudioGradientView.java create mode 100644 common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioEncoder.java create mode 100644 common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioEncoderBase.java create mode 100644 common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioService.java create mode 100644 common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioThread.java diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index a818349b..80e5d10b 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ + + + + + + + + + + + + + + diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/BootActivity.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/BootActivity.java index c7a7ab86..31b47829 100644 --- a/common/src/main/java/com/abrenoch/hyperiongrabber/common/BootActivity.java +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/BootActivity.java @@ -10,6 +10,8 @@ import android.support.annotation.RequiresApi; import android.support.v7.app.AppCompatActivity; +import com.abrenoch.hyperiongrabber.common.audio.HyperionAudioService; + public class BootActivity extends AppCompatActivity { public static final int REQUEST_MEDIA_PROJECTION = 1; @@ -37,9 +39,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } public static void startScreenRecorder(Context context, int resultCode, Intent data) { - Intent intent = new Intent(context, HyperionScreenService.class); - intent.setAction(HyperionScreenService.ACTION_START); - intent.putExtra(HyperionScreenService.EXTRA_RESULT_CODE, resultCode); + Intent intent = new Intent(context, HyperionAudioService.class); + intent.setAction(HyperionAudioService.ACTION_START); + intent.putExtra(HyperionAudioService.EXTRA_RESULT_CODE, resultCode); intent.putExtras(data); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent); diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/HyperionNotification.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/HyperionNotification.java index efea7c3b..6577e06b 100644 --- a/common/src/main/java/com/abrenoch/hyperiongrabber/common/HyperionNotification.java +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/HyperionNotification.java @@ -21,7 +21,7 @@ public class HyperionNotification { private final Context mContext; private Notification.Action mAction = null; - HyperionNotification (Context ctx, NotificationManager manager) { + public HyperionNotification (Context ctx, NotificationManager manager) { mNotificationManager = manager; mContext = ctx; NOTIFICATION_TITLE = mContext.getString(R.string.app_name); diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/AudioGradientView.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/AudioGradientView.java new file mode 100644 index 00000000..41ea41a5 --- /dev/null +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/AudioGradientView.java @@ -0,0 +1,194 @@ +package com.abrenoch.hyperiongrabber.common.audio; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.SweepGradient; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; + +import com.abrenoch.hyperiongrabber.common.R; +import com.abrenoch.hyperiongrabber.common.util.HyperionGrabberOptions; +import com.abrenoch.hyperiongrabber.common.util.Preferences; + +import java.util.Arrays; + +public class AudioGradientView extends View { + + private static final boolean DEBUG = true; + private static final String TAG = "AudioGradientView"; + + public int[] colors; + float[] positions; + int pixelCount; + + Paint paint; + SweepGradient gradient; + Rect viewBounds = new Rect(); + + public AudioGradientView(Context context) { + this(context, null); + } + + public AudioGradientView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public AudioGradientView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + Preferences prefs = new Preferences(getContext()); + int mHorizontalLEDCount = prefs.getInt(R.string.pref_key_x_led); + int mVerticalLEDCount = prefs.getInt(R.string.pref_key_y_led); + HyperionGrabberOptions options = new HyperionGrabberOptions(mHorizontalLEDCount, mVerticalLEDCount); + // find the common divisor for width & height best fit for the LED count (defined in options) + WindowManager window = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + final DisplayMetrics metrics = new DisplayMetrics(); + window.getDefaultDisplay().getRealMetrics(metrics); + int width = metrics.widthPixels; + int height = metrics.heightPixels; + Log.d("leds/display: ", Integer.toString(mHorizontalLEDCount) + " " + Integer.toString(mVerticalLEDCount) + " " + Integer.toString(width) + " " + Integer.toString(height)); + int divisor = options.findDivisor(width, height); + // set the scaled width & height based upon the found divisor + int mWidthScaled = (width / divisor); + int mHeightScaled = (height / divisor); + pixelCount = 2*mWidthScaled + 2*mHeightScaled; + if(DEBUG){ Log.d("pixelCount: ", Integer.toString(pixelCount)); } + + // overwrite dims with smallest even pixelnumber for debug only +// if (DEBUG){ +// mWidthScaled = 32; +// mHeightScaled = 18; +// pixelCount = 2*mWidthScaled + 2*mHeightScaled; +// positions2 = new float[pixelCount]; +// } + + colors = swirl(); +// if(!DEBUG){ +// positions = new float[pixelCount]; +// positions[0] = 0f; +// for(int i=1; i < pixelCount; i++){ +// positions[i] = i*1.0f/LED_COUNT; +// } +// } +// else + positions = new float[pixelCount]; + double[] alpha = new double[pixelCount]; + int i; + // start from 3 o'clock to diagonal left/top corner (alpha[0-8]) + for(i=1; i<=mHeightScaled/2; i++){ + alpha[i-1] = Math.toDegrees( Math.atan( (double)i / ( (double)mWidthScaled/2.0f ) ) ); + } + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + int j; + // start diagonal left/top corner to 12 o'clock (alpha[9-24]) + for(j=(mWidthScaled/2)-1; j >= 0; j--,i++){ + alpha[i-1] = 90 - Math.toDegrees( Math.atan( (double)j / ( (double)mHeightScaled/2.0f ) ) ); + } + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + // mirror 1.quarter(alpha[0-24]) at y-axis to 2.quarter(alpha[25-49]) + for(j=(mHeightScaled/2+mWidthScaled/2)-1; j > 0; j--,i++){ + alpha[i-1] = 90 + ( 90 - alpha[j-1]); + } + alpha[i-1] = alpha[i-2] + alpha[j]; + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + // mirror top half(alpha[0-49]) at x-axis to bottom half(alpha[50-99]) + for (j=0; j<(2*(mHeightScaled/2+mWidthScaled/2)) ; j++,i++){ + alpha[i] = 180 + alpha[j]; + } + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + // norm alpha[degree] to positions[norm] + for(i=0; i ??? call himself ??? ---> setColors invalidate() + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if(DEBUG){ + Log.d(TAG, "onMeasure: " + Arrays.toString(colors)); + } + if(colors==null) colors = swirl(); + + viewBounds.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); + gradient = new SweepGradient(viewBounds.centerX(), viewBounds.centerY(), colors, positions); + } + + // update Layout with new colors + public void setColors(int[] _colors){ + if (DEBUG) Log.d(TAG,"setColors" + Arrays.toString(_colors)); + colors = _colors; + requestLayout(); //call onMeasure + invalidate(); //call onDraw + } + + public int[] swirl(){ + int[] hues = new int[pixelCount]; + int[] colors = new int[pixelCount]; + float vs = 0.5f; // vs == hues from OnDataCaptureListener + for(int i = 0; i < pixelCount; i++){ + hues[i] = (int) (((float) i / pixelCount) * 360); + } + for(int i = 0; i < pixelCount; i++){ + float[] hsv = new float[]{(float) hues[i], 1, vs}; + int color = Color.HSVToColor(hsv); + colors[i] = color; + } + return colors; + + } + + // debug + private int[] rgbWhite() { + int[] res = new int[pixelCount]; + for(int i = 0; i < pixelCount; i=i+3){ + res[i] = 0; + if (i+1 < pixelCount) res[i+1] = 120; + if (i+2 < pixelCount) res[i+2] = 240; + + } + + + int[] colors = new int[pixelCount]; + float vs = 1.0f; + + for(int i = 0; i < pixelCount; i++){ + float[] hsv = new float[]{(float) res[i], 1, vs}; + int color = Color.HSVToColor(hsv); + colors[i] = color; + } + + return colors; + } + +} diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioEncoder.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioEncoder.java new file mode 100644 index 00000000..1eb330d6 --- /dev/null +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioEncoder.java @@ -0,0 +1,446 @@ +package com.abrenoch.hyperiongrabber.common.audio; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.SweepGradient; +import android.media.audiofx.Visualizer; +import android.os.Build; +import android.os.SystemClock; +import android.support.annotation.RequiresApi; +import android.util.Log; + +import com.abrenoch.hyperiongrabber.common.util.HyperionGrabberOptions; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class HyperionAudioEncoder extends HyperionAudioEncoderBase { + + private static final String TAG = "HyperionAudioEncoder"; + private static final boolean DEBUG = false; + + private Visualizer mAudioCapture = null; + + long eventTimeStart=0; + long eventTimeNew=0; + long eventTimeOld=0; + long eventTimeBetween=0; + boolean newTime=false; + float turnaround=0; + int blues = 188; + boolean direction = true; + + int red = 0; + int green = 120; + int blue = 240; + + float colorRange = 360.0f; // Farbraum + int[] plasmaColorWidthTop; + int[] plasmaColorHeightRight; + int[] plasmaColorWidthBottom; + int[] plasmaColorHeightLeft; + long mod; + + int mScaledPixelCount; + float[] positions; + int turnaroundRGB = 0; + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + HyperionAudioEncoder(final HyperionAudioThread.HyperionThreadListener listener, HyperionAudioService.HyperionAudioEncoderBroadcaster sender, + final int width, final int height, HyperionGrabberOptions options) { + super(listener, sender, width, height, options); + + prepare(); + } + + public HyperionAudioService.HyperionAudioEncoderBroadcaster getReceiver() {return mSender;} + + @TargetApi(Build.VERSION_CODES.M) + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void prepare() { + if (DEBUG) Log.d(TAG, "Preparing encoder"); + + // todo: change _METHOD to String + VISUALIZATION_METHOD = VisualisationMethod.RGBWHITE; + + setAudioReader(); + + mScaledPixelCount = 2*(mWidthScaled+mHeightScaled); + plasmaColorWidthTop = new int[mWidthScaled]; + plasmaColorHeightRight = new int[mHeightScaled]; + plasmaColorWidthBottom = new int[mWidthScaled]; + plasmaColorHeightLeft = new int [mHeightScaled]; + prepareEffects(); + setGradientPosition(); + } + + @Override + public void stopRecording() { + if (DEBUG) Log.i(TAG, "stopRecording Called"); + setCapturing(false); + mHandler.getLooper().quit(); + clearAndDisconnect(); + + mAudioCapture.setDataCaptureListener(null, 0, false, false); + mAudioCapture.setEnabled(false); + mAudioCapture.release(); + audioListener = null; + + } + + @Override + public void resumeRecording() { + if (DEBUG) Log.i(TAG, "resumeRecording Called"); + // #todo: +// if (!isCapturing() && mAudioCapture != null) { +// if (DEBUG) Log.i(TAG, "Resuming capture audio"); +// int res = mAudioCapture.setEnabled(true); +// if(res != Visualizer.SUCCESS){ +// Log.d(TAG, "Error starting audiocaputre: " + res); +// } +// setCapturing(true); +// } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void setOrientation(int orientation) { + if (orientation != mCurrentOrientation) { + mCurrentOrientation = orientation; + mIsCapturing = false; + setAudioReader(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) + private void setAudioReader() { + if (DEBUG) Log.d(TAG, "Setting audio reader " + String.valueOf(isCapturing())); + + mAudioCapture = new Visualizer(0); // 0: intern, 1: microphone + mAudioCapture.setDataCaptureListener(audioListener, mSamplingRate, true, false); + int res = mAudioCapture.setEnabled(true); + if(res != Visualizer.SUCCESS){ + Log.d(TAG, "Error starting audiocaputre: " + res); + } + setCapturing(true); + + eventTimeNew = SystemClock.uptimeMillis(); + } + + private Visualizer.OnDataCaptureListener audioListener = new Visualizer.OnDataCaptureListener() { + + @Override + public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) { + + setFrame(waveform); + if (DEBUG) { + Log.d(TAG, "onWaveFormDataCapture bytes: " + Arrays.toString(waveform)); + Log.d(TAG, "onWaveFormDataCapture i: " + Integer.toString(samplingRate)); + } + } + + @Override + public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { + // #todo: fft to hues -> test + } + }; + + private void setFrame(byte[] waveform){ + + int[] colors = new int[mScaledPixelCount]; + int steps = waveform.length / mScaledPixelCount; + float[] hues = new float[mScaledPixelCount]; + byte[] data = new byte[0]; + ByteBuffer byteBuffer; + + for(int j = 0; j < mScaledPixelCount; j++){ + int tmp_avg = 0; + for(int k = 0; k < steps; k++){ + tmp_avg += (int) waveform[steps*j+k] & 0xFF; + } + float intres = tmp_avg / (float) steps; + hues[j] = intres/(float) 255; + } + + colors = getColorArray(hues); + mColors = colors; + mSender.onSendColors(); + + byteBuffer = renderImageBuffer(colors); + data = getPixels(byteBuffer, mWidthScaled, mHeightScaled, 0, 0, 0, 0); + if (DEBUG) Log.d(TAG, "PixelArray" + Arrays.toString(data)); + + byte[] finalData = data; + new Thread(() -> mListener.sendFrame(finalData, mWidthScaled, mHeightScaled)).start(); + + } + + private ByteBuffer renderImageBuffer(int[] colors){ + + Bitmap bitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.RGB_565; + + bitmap = Bitmap.createBitmap(mWidthScaled, mHeightScaled, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + SweepGradient gradient; + Rect viewBounds = new Rect(); + viewBounds.set(0, 0, mWidthScaled, mHeightScaled); + gradient = new SweepGradient(viewBounds.centerX(), viewBounds.centerY(), colors, positions); + Paint paint = new Paint(); + paint.setShader(gradient); + canvas.drawRect(viewBounds, paint); + + int size = bitmap.getRowBytes() * bitmap.getHeight(); + ByteBuffer byteBuffer = ByteBuffer.allocate(size); + bitmap.copyPixelsToBuffer(byteBuffer); + + return byteBuffer; + + } + + private byte[] getPixels(ByteBuffer byteBuffer, int width, int height, int rowStride, int pixelStride, int firstX, int firstY){ + + // todo: hardcoded!!! maybe possible to get from bitmap + rowStride = 512; //plane.Image + pixelStride = 4; + firstX = 0; // not zero with black borders + firstY = 0; + + // + int rowPadding = rowStride - width * pixelStride; + int offset = 0; + + ByteArrayOutputStream bao = new ByteArrayOutputStream( + (width - firstX * 2) * (height - firstY * 2) * 3 + ); + + for (int y = 0, compareHeight = height - firstY - 1; y < height; y++, offset += rowPadding) { + if (y < firstY || y > compareHeight) { + offset += width * pixelStride; + continue; + } + for (int x = 0, compareWidth = width - firstX - 1; x < width; x++, offset += pixelStride) { + if (x < firstX || x > compareWidth) continue; + bao.write(byteBuffer.get(offset) & 0xff); // R + bao.write(byteBuffer.get(offset + 1) & 0xff); // G + bao.write(byteBuffer.get(offset + 2) & 0xff); // B + } + } + + return bao.toByteArray(); + + } + + private int[] getColorArray(float[] vs){ + int[] hues = new int[mScaledPixelCount]; + int[] colors = new int[mScaledPixelCount]; + + if (VISUALIZATION_METHOD == VisualisationMethod.RAINBOW_SWIRL){ + hues = rainBowSwirl(); + } else if (VISUALIZATION_METHOD == VisualisationMethod.RAINBOW_MOD){ + hues = single_color(); + } else if (VISUALIZATION_METHOD == VisualisationMethod.ICEBLUE){ + hues = iceblue(); + } else if (VISUALIZATION_METHOD == VisualisationMethod.RGBWHITE){ + hues = rgbWhite(); + } else if (VISUALIZATION_METHOD == VisualisationMethod.PLASMA){ + hues = plasma(); + } + else { + for (int i = 0; i < mScaledPixelCount; i++){ + hues[i] = 0; + } + } + + // plasma effect using mod with timeBetween + if (VISUALIZATION_METHOD != VisualisationMethod.PLASMA) { + turnaround += mTurnAroundRate % 360; + + } + + for (int i = 0; i < mScaledPixelCount; i++){ + float[] hsv = new float[]{(float) hues[i], 1, vs[i]}; + int color = Color.HSVToColor(hsv); + colors[i] = color; + } + + return colors; + } + + // #todo: create class Effects and export from encoder + // Effects + private int[] rainBowSwirl(){ + int[] res = new int[mScaledPixelCount]; + for(int i = 0; i < mScaledPixelCount; i++){ + res[i] = ((int) (((float) i / mScaledPixelCount) * 360) + (int) turnaround) % 360; + } + return res; + } + + private int[] single_color(){ + int[] res = new int[mScaledPixelCount]; + for(int i = 0; i < mScaledPixelCount; i++){ + res[i] = (int) turnaround % 360; + } + return res; + } + + private int[] iceblue(){ + // todo: adapt frequencies to color spectrum + // hochton: helbblau bass: dunkelblau + // 8bit pcm: jedes bit ein frequenzbereich? + // 8 blaustufen für die töne + int[] res = new int[mScaledPixelCount]; + for(int i = 0; i < mScaledPixelCount; i++){ + res[i] = blues; + } + if (blues == 225) direction = false; + if (blues == 188) direction = true; + if (direction) blues++; + if (!direction) blues--; + return res; + } + + private int[] rgbWhite() { + int[] res = new int[mScaledPixelCount]; + for(int i = 0; i < mScaledPixelCount; i=i+3){ + res[i] = red; + if (i+1 < mScaledPixelCount) res[i+1] = green; + if (i+2 < mScaledPixelCount) res[i+2] = blue; + + } + if ( turnaroundRGB == 3 ){ + if (red==240) red = 0; + else red = red+120; + if (green==240) green = 0; + else green = green+120; + if (blue==240) blue = 0; + else blue = blue+120; + turnaroundRGB = 0; + } + turnaroundRGB++; + return res; + } + + private int[] plasma() { + + int[] res = new int[mScaledPixelCount]; + + if(newTime){ + eventTimeNew = SystemClock.uptimeMillis(); + newTime =! newTime; + } else { + eventTimeOld = SystemClock.uptimeMillis(); + } + eventTimeBetween = Math.abs(eventTimeNew-eventTimeOld); + if (eventTimeBetween > 222) { + mod = eventTimeStart/11; + eventTimeStart = eventTimeStart+eventTimeBetween; + eventTimeBetween = 0; + newTime =! newTime; + } + + for(int i=0; i < mScaledPixelCount; i++){ + + if (i < plasmaColorWidthTop.length) { + res[i] = (int)(plasmaColorWidthTop[i] + mod) % (int)colorRange; + } + else if (i >= plasmaColorWidthTop.length & i < plasmaColorWidthTop.length+plasmaColorHeightRight.length) { + res[i] = (int)(plasmaColorHeightRight[i-plasmaColorWidthTop.length] + mod) % (int)colorRange; + } + else if (i >= plasmaColorWidthTop.length+plasmaColorHeightRight.length & i < plasmaColorWidthTop.length+plasmaColorHeightRight.length+plasmaColorWidthBottom.length) { + res[i] = (int)(plasmaColorWidthBottom[i-plasmaColorWidthTop.length-plasmaColorHeightRight.length] + mod) % (int)colorRange; + } + else if (i >= plasmaColorWidthTop.length+plasmaColorHeightRight.length+plasmaColorWidthBottom.length) { + res[i] = (int)(plasmaColorHeightLeft[i-plasmaColorWidthTop.length-plasmaColorHeightRight.length-plasmaColorWidthBottom.length] + mod) % (int)colorRange; + } + + } + + return res; + } + + private void prepareEffects() { + + int x; + int y; + + y = 0; + for (x = 0; x < mWidthScaled; x++) { // LEDcountHeight = 101 + plasmaColorWidthTop[x] = colorPlasmaFrameFactor(x, y); + } + x = mWidthScaled-1; + for (y = 0; y < mHeightScaled; y++) { // LEDcountHeight = 66 + plasmaColorHeightRight[y] = colorPlasmaFrameFactor(x, y); + } + y = mHeightScaled-1; + for (x = mWidthScaled-1; x >= 0; x--) { // LEDcountHeight = 101 + plasmaColorWidthBottom[x] = colorPlasmaFrameFactor(x, y); + } + x = 0; + for (y = mHeightScaled-1; y >= 0; y--) { // LEDcountHeight = 66 + plasmaColorHeightLeft[y] = colorPlasmaFrameFactor(x, y); + } + + } + + private int colorPlasmaFrameFactor (int x, int y) { + return (int) ( + colorRange/2.0f + (colorRange/2.0f * Math.sin(x / 16.0f)) + + colorRange/2.0f + (colorRange/2.0f * Math.sin(y / 8.0f)) + + colorRange/2.0f + (colorRange/2.0f * Math.sin((x+y)) / 16.0f) + + colorRange/2.0f + (colorRange/2.0f * Math.sin(Math.sqrt(Math.pow(x, 2.0f) + Math.pow(y, 2.0f)) / 8.0f)) + ) / 4; + } + + private void setGradientPosition () { + + positions = new float[mScaledPixelCount]; + double[] alpha = new double[mScaledPixelCount]; + int i; + // # Test dimensions: 32 x 18 px + // start from 3 o'clock to diagonal left/top corner (alpha[0-8]) + for(i=1; i<=mHeightScaled/2; i++){ + alpha[i-1] = Math.toDegrees( Math.atan( (double)i / ( (double)mWidthScaled/2.0f ) ) ); + } + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + int j; + // start diagonal left/top corner to 12 o'clock (alpha[9-24]) + for(j=(mWidthScaled/2)-1; j >= 0; j--,i++){ + alpha[i-1] = 90 - Math.toDegrees( Math.atan( (double)j / ( (double)mHeightScaled/2.0f ) ) ); + } + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + // mirror 1.quarter(alpha[0-24]) at y-axis to 2.quarter(alpha[25-49]) + for(j=(mHeightScaled/2+mWidthScaled/2)-1; j > 0; j--,i++){ + alpha[i-1] = 90 + ( 90 - alpha[j-1]); + } + alpha[i-1] = alpha[i-2] + alpha[j]; + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + // mirror top half(alpha[0-49]) at x-axis to bottom half(alpha[50-99]) + for (j=0; j<(2*(mHeightScaled/2+mWidthScaled/2)) ; j++,i++){ + alpha[i] = 180 + alpha[j]; + } + if (DEBUG){Log.d("alphas", Arrays.toString(alpha));} + // norm alpha[degree] to positions[norm] + for(i=0; i height ? Configuration.ORIENTATION_LANDSCAPE : + Configuration.ORIENTATION_PORTRAIT; + + if (DEBUG) { + Log.d(TAG, "Sampling Rate: " + String.valueOf(mSamplingRate)); + Log.d(TAG, "Turnaround Rate: " + String.valueOf(mTurnAroundRate)); + Log.d(TAG, "Original Width: " + String.valueOf(width)); + Log.d(TAG, "Original Height: " + String.valueOf(height)); + + } + + // find the common divisor for width & height best fit for the LED count (defined in options) + int divisor = options.findDivisor(width, height); + + // set the scaled width & height based upon the found divisor + mHeightScaled = (height / divisor); + mWidthScaled = (width / divisor); + + if (DEBUG) { + Log.d(TAG, "Common Divisor: " + String.valueOf(divisor)); + Log.d(TAG, "Scaled Width: " + String.valueOf(mWidthScaled)); + Log.d(TAG, "Scaled Height: " + String.valueOf(mHeightScaled)); + } + + final HandlerThread thread = new HandlerThread(TAG); + thread.start(); + mHandler = new Handler(thread.getLooper()); + + if (DEBUG) Log.d(TAG, "Encoder ready"); + } + + private Runnable clearAndDisconnectRunner = new Runnable() { + public void run() { + if (DEBUG) Log.d(TAG, "Clearing LEDs and disconnecting"); + try { + Thread.sleep(CLEAR_COMMAND_DELAY_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + mListener.clear(); + mListener.disconnect(); + } + }; + + private Runnable clearLightsRunner = new Runnable() { + public void run() { + if (DEBUG) Log.d(TAG, "Clearing LEDs"); + try { + Thread.sleep(CLEAR_COMMAND_DELAY_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + mListener.clear(); + } + }; + + public void clearLights() { + new Thread(clearLightsRunner).start(); + } + + void clearAndDisconnect() { + new Thread(clearAndDisconnectRunner).start(); + } + + public boolean isCapturing() { + return mIsCapturing; + } + + public void setCapturing(boolean isCapturing) { + mIsCapturing = isCapturing; + } + + public void sendStatus() { + if (mListener != null) { + mListener.sendStatus(isCapturing()); + } + } + + public void stopRecording() { + throw new RuntimeException("Stub!"); + } + + public void resumeRecording() { + throw new RuntimeException("Stub!"); + } + + public void setOrientation(int orientation) { + mCurrentOrientation = orientation; + } + + public int[] getColors(){ + return mColors; + } + + public int[] getScaledDimension () { return new int[]{mWidthScaled, mHeightScaled}; } + + public enum VisualisationMethod { + NONE, + RAINBOW_SWIRL, + RAINBOW_MOD, + ICEBLUE, + RGBWHITE, + PLASMA + } + +} diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioService.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioService.java new file mode 100644 index 00000000..e5ca41ed --- /dev/null +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioService.java @@ -0,0 +1,366 @@ +package com.abrenoch.hyperiongrabber.common.audio; + + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.content.LocalBroadcastManager; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import com.abrenoch.hyperiongrabber.common.HyperionNotification; +import com.abrenoch.hyperiongrabber.common.R; +import com.abrenoch.hyperiongrabber.common.util.HyperionGrabberOptions; +import com.abrenoch.hyperiongrabber.common.util.Preferences; + +import java.util.Arrays; +import java.util.Objects; + +public class HyperionAudioService extends Service { + public static final String BROADCAST_ERROR = "SERVICE_ERROR"; + public static final String BROADCAST_TAG = "SERVICE_STATUS"; + public static final String BROADCAST_FILTER = "SERVICE_FILTER"; + public static final String BROADCAST_COLORS = "SERVICE_COLORS"; + public static final String BROADCAST_X = "SERVICE_X"; + private static final boolean DEBUG = false; + private static final String TAG = "HyperionAudioService"; + + private static final String BASE = "com.abrenoch.hyperiongrabber.service."; + public static final String ACTION_START = BASE + "ACTION_START"; + public static final String ACTION_STOP = BASE + "ACTION_STOP"; + public static final String ACTION_EXIT = BASE + "ACTION_EXIT"; + public static final String GET_STATUS = BASE + "ACTION_STATUS"; + public static final String EXTRA_RESULT_CODE = BASE + "EXTRA_RESULT_CODE"; + private static final int NOTIFICATION_ID = 1; + private static final int NOTIFICATION_EXIT_INTENT_ID = 2; + + private boolean RECONNECT = false; + private boolean hasConnected = false; + private int mHorizontalLEDCount; + private int mVerticalLEDCount; + private NotificationManager mNotificationManager; + private String mStartError = null; + + private String mSamplingMethod; + private int mSamplingRate; + private String mStartEffect; + private float mTurnAroundRate; + private int mOffsetEffect; + + private HyperionAudioEncoder mHyperionAudioEncoder; + private HyperionAudioThread mHyperionAudioThread; + + HyperionAudioThreadBroadcaster mReceiver = new HyperionAudioThreadBroadcaster() { + @Override + public void onConnected() { + Log.d(TAG, "CONNECTED TO HYPERION INSTANCE"); + hasConnected = true; + notifyActivity(); + } + + @Override + public void onConnectionError(int errorID, String error) { + Log.e(TAG, "COULD NOT CONNECT TO HYPERION INSTANCE"); + if (error != null) Log.e(TAG, error); + if (!hasConnected) { + mStartError = getResources().getString(R.string.error_server_unreachable); + haltStartup(); + } + if (RECONNECT && hasConnected) { + Log.e(TAG, "AUTOMATIC RECONNECT ENABLED. CONNECTING ..."); + } else if (!RECONNECT && hasConnected) { + mStartError = getResources().getString(R.string.error_connection_lost); + stopSelf(); + } + } + + @Override + public void onReceiveStatus(boolean isCapturing) { + if (DEBUG) Log.v(TAG, "Received grabber status, notifying activity. Status: " + + String.valueOf(isCapturing)); + notifyActivity(); + } + + }; + + HyperionAudioEncoderBroadcaster mAudioReceiver = () -> { + if (DEBUG) Log.d(TAG, "SEND COLORS"); + sendColors(); + }; + + BroadcastReceiver mEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + + switch (Objects.requireNonNull(intent.getAction())) { + case Intent.ACTION_SCREEN_ON: + if (DEBUG) Log.v(TAG, "ACTION_SCREEN_ON intent received"); + if (mHyperionAudioEncoder != null && !isCapturing()) { + if (DEBUG) Log.v(TAG, "Encoder not grabbing, attempting to restart"); + mHyperionAudioEncoder.resumeRecording(); + } + notifyActivity(); + break; + case Intent.ACTION_SCREEN_OFF: + if (DEBUG) Log.v(TAG, "ACTION_SCREEN_OFF intent received"); + if (mHyperionAudioEncoder != null) { + if (DEBUG) Log.v(TAG, "Clearing current light data"); + mHyperionAudioEncoder.clearLights(); + } + break; + case Intent.ACTION_CONFIGURATION_CHANGED: + if (DEBUG) Log.v(TAG, "ACTION_CONFIGURATION_CHANGED intent received"); + if (mHyperionAudioEncoder != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (DEBUG) Log.v(TAG, "Configuration changed, checking orientation"); + mHyperionAudioEncoder.setOrientation(getResources().getConfiguration().orientation); + } + break; + case Intent.ACTION_SHUTDOWN: + case Intent.ACTION_REBOOT: + if (DEBUG) Log.v(TAG, "ACTION_SHUTDOWN|ACTION_REBOOT intent received"); + stopAudioRecord(); + break; + } + } + }; + + @Override + public void onCreate() { + + // #todo: edit HyperionNotiifications for Audio + mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + super.onCreate(); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private boolean prepared() { + Preferences prefs = new Preferences(getBaseContext()); + String host = prefs.getString(R.string.pref_key_host, null); + int port = prefs.getInt(R.string.pref_key_port, -1); + String priority = prefs.getString(R.string.pref_key_priority, "50"); + + mHorizontalLEDCount = prefs.getInt(R.string.pref_key_x_led); + mVerticalLEDCount = prefs.getInt(R.string.pref_key_y_led); + // #todo: add to SettingsActivity: samplingrate, tournaroundrate, ledcount + mSamplingMethod = prefs.getString(R.string.pref_key_sampling_method, "Waveform"); + mSamplingRate = prefs.getInt(R.string.pref_key_sampling_rate); + mStartEffect = prefs.getString(R.string.pref_key_effect, "Plasma"); + mTurnAroundRate = (float)prefs.getInt(R.string.pref_key_turnaround_rate)/100.0f; + mOffsetEffect = prefs.getInt(R.string.pref_key_turnaround_offset); +// Log.d("new settings", mSamplingMethod + ", " + +// Integer.toString(mSamplingRate) + ", " + +// mStartEffect + ", " + +// Float.toString(mTurnAroundRate) + ", " + +// Integer.toString(mOffsetEffect)); + + RECONNECT = prefs.getBoolean(R.string.pref_key_reconnect); + int delay = prefs.getInt(R.string.pref_key_reconnect_delay); + if (host == null || Objects.equals(host, "0.0.0.0") || Objects.equals(host, "")) { + mStartError = getResources().getString(R.string.error_empty_host); + return false; + } + if (port == -1) { + mStartError = getResources().getString(R.string.error_empty_port); + return false; + } + if (mHorizontalLEDCount <= 0 || mVerticalLEDCount <= 0) { + mStartError = getResources().getString(R.string.error_invalid_led_counts); + return false; + } + + mHyperionAudioThread = new HyperionAudioThread(mReceiver, host, port, Integer.parseInt(priority), RECONNECT, delay); + mHyperionAudioThread.start(); + mStartError = null; + return true; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.v(TAG, "Start command received"); + super.onStartCommand(intent, flags, startId); + if (intent == null || intent.getAction() == null) { + String nullItem = (intent == null ? "intent" : "action"); + if (DEBUG) Log.v(TAG, "Null " + nullItem + " provided to start command"); + } else { + final String action = intent.getAction(); + if (DEBUG) Log.v(TAG, "Start command action: " + String.valueOf(action)); + switch (action) { + case ACTION_START: + if (mHyperionAudioThread == null) { + boolean isPrepared = prepared(); + if (isPrepared) { + startAudioRecord(intent); + startForeground(NOTIFICATION_ID, getNotification()); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + intentFilter.addAction(Intent.ACTION_REBOOT); + intentFilter.addAction(Intent.ACTION_SHUTDOWN); + + registerReceiver(mEventReceiver, intentFilter); + } else { + haltStartup(); + } + } + break; + case ACTION_STOP: + stopAudioRecord(); + break; + case GET_STATUS: + notifyActivity(); + break; + case ACTION_EXIT: + stopSelf(); + break; + } + } + + return START_STICKY; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + if (DEBUG) Log.v(TAG, "Ending service"); + + try { + unregisterReceiver(mEventReceiver); + } catch (Exception e) { + if (DEBUG) Log.v(TAG, "Wake receiver not registered"); + } + + stopAudioRecord(); + stopForeground(true); + notifyActivity(); + + super.onDestroy(); + } + + private void haltStartup() { + startForeground(NOTIFICATION_ID, getNotification()); + stopSelf(); + } + + private Intent buildExitButton() { + Intent notificationIntent = new Intent(this, this.getClass()); + notificationIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + notificationIntent.setAction(ACTION_EXIT); + return notificationIntent; + } + + public Notification getNotification() { + HyperionNotification notification = new HyperionNotification(this, mNotificationManager); + String label = getString(R.string.notification_exit_button); + notification.setAction(NOTIFICATION_EXIT_INTENT_ID, label, buildExitButton()); + return notification.buildNotification(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void startAudioRecord(final Intent intent) { + if (DEBUG) Log.v(TAG, "Start audio recorder"); + WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + if (window != null) { + final DisplayMetrics metrics = new DisplayMetrics(); + window.getDefaultDisplay().getRealMetrics(metrics); + HyperionGrabberOptions options = new HyperionGrabberOptions(mHorizontalLEDCount, + mVerticalLEDCount, mSamplingMethod, mSamplingRate, mStartEffect, mTurnAroundRate, mOffsetEffect); + if (DEBUG) Log.v(TAG, "Starting the recorder"); + mHyperionAudioEncoder = new HyperionAudioEncoder(mHyperionAudioThread.getReceiver(), mAudioReceiver, + metrics.widthPixels, metrics.heightPixels, options); + mHyperionAudioEncoder.sendStatus(); + } + } + + private void stopAudioRecord() { + + if (DEBUG) Log.v(TAG, "Stop audio recorder"); + RECONNECT = false; + mNotificationManager.cancel(NOTIFICATION_ID); + if (mHyperionAudioEncoder != null) { + if (DEBUG) Log.v(TAG, "Stopping the current encoder"); + mHyperionAudioEncoder.stopRecording(); + } + releaseResource(); + if (mHyperionAudioThread != null) { + mHyperionAudioThread.interrupt(); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void releaseResource() { + // #todo: close get AudioCapture from Encoder and stop +// if (_mediaProjection != null) { +// _mediaProjection.stop(); +// _mediaProjection = null; +// } + } + + boolean isCapturing() { + + return mHyperionAudioEncoder != null && mHyperionAudioEncoder.isCapturing(); + } + + boolean isCommunicating() { + + return isCapturing() && hasConnected; + } + + int[] getColors() { + return mHyperionAudioEncoder.getColors(); + } + + private void notifyActivity() { + Intent intent = new Intent(BROADCAST_FILTER); + intent.putExtra(BROADCAST_TAG, isCommunicating()); + intent.putExtra(BROADCAST_ERROR, mStartError); + if (DEBUG) { + Log.v(TAG, "Sending status broadcast - communicating: " + + String.valueOf(isCommunicating())); + if (mStartError != null) { + Log.v(TAG, "Startup error: " + mStartError); + } + } + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + } + + private void sendColors() { + Intent intent = new Intent(BROADCAST_X); + intent.putExtra(BROADCAST_COLORS, getColors()); + if (DEBUG) { + Log.v(TAG, "Sending colors broadcast - capturing: " + + Arrays.toString( getColors() ) ); + + } + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + } + + public interface HyperionAudioThreadBroadcaster { + // void onResponse(String response); + void onConnected(); + void onConnectionError(int errorHash, String errorString); + void onReceiveStatus(boolean isCapturing); + + } + + public interface HyperionAudioEncoderBroadcaster { + void onSendColors(); + } +} diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioThread.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioThread.java new file mode 100644 index 00000000..d9b970bf --- /dev/null +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/audio/HyperionAudioThread.java @@ -0,0 +1,122 @@ +package com.abrenoch.hyperiongrabber.common.audio; + +import com.abrenoch.hyperiongrabber.common.network.Hyperion; +import com.abrenoch.hyperiongrabber.common.HyperionProto; +import java.io.IOException; + +public class HyperionAudioThread extends Thread { + private String HOST; + private int PORT; + private int PRIORITY; + private final int FRAME_DURATION = -1; + private boolean RECONNECT = false; + private boolean HAS_CONNECTED = false; + private int RECONNECT_DELAY; + private Hyperion mHyperion; + + HyperionAudioService.HyperionAudioThreadBroadcaster mSender; + + HyperionThreadListener mReceiver = new HyperionThreadListener() { + @Override + public void sendFrame(byte[] data, int width, int height) { + HyperionProto.HyperionRequest req = + Hyperion.setImageRequest(data, width, height, PRIORITY, FRAME_DURATION); + if (mHyperion != null && mHyperion.isConnected()) { + try { + mHyperion.sendRequest(req); + } catch (IOException e) { + mSender.onConnectionError(e.hashCode(), e.getMessage()); + e.printStackTrace(); + if (RECONNECT && HAS_CONNECTED) { + reconnectDelay(RECONNECT_DELAY); + try { + mHyperion = new Hyperion(HOST, PORT); + } catch (IOException i) { + i.printStackTrace(); + } + } + } + } + } + + @Override + public void clear() { + if (mHyperion != null && mHyperion.isConnected()) { + try { + mHyperion.clear(PRIORITY); + } catch (IOException e) { + mSender.onConnectionError(e.hashCode(), e.getMessage()); + e.printStackTrace(); + } + } + } + + @Override + public void disconnect() { + if (mHyperion != null) { + try { + mHyperion.disconnect(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void sendStatus(boolean isGrabbing) { + if (mSender != null) { + mSender.onReceiveStatus(isGrabbing); + } + } + }; + + public HyperionAudioThread(HyperionAudioService.HyperionAudioThreadBroadcaster listener, final String host, + final int port, final int priority, final boolean reconnect, final int delay){ + HOST = host; + PORT = port; + PRIORITY = priority; + RECONNECT = reconnect; + RECONNECT_DELAY = delay * 1000; + mSender = listener; + } + + public HyperionThreadListener getReceiver() {return mReceiver;} + + @Override + public void run(){ + do { + try { + mHyperion = new Hyperion(HOST, PORT); + } catch (IOException e) { + mSender.onConnectionError(e.hashCode(), e.getMessage()); + e.printStackTrace(); + if (RECONNECT && HAS_CONNECTED) { + reconnectDelay(RECONNECT_DELAY); + } + } finally { + if (mHyperion != null && mSender != null && mHyperion.isConnected()) { + HAS_CONNECTED = true; + mSender.onConnected(); + break; + } + } + } while (RECONNECT && HAS_CONNECTED); + } + + public void reconnectDelay(long t) { + try { + Thread.sleep(t); + } catch (InterruptedException e) { + RECONNECT = false; + HAS_CONNECTED = false; + Thread.currentThread().interrupt(); + } + } + + public interface HyperionThreadListener { + void sendFrame(byte[] data, int width, int height); + void clear(); + void disconnect(); + void sendStatus(boolean isGrabbing); + } +} diff --git a/common/src/main/java/com/abrenoch/hyperiongrabber/common/util/HyperionGrabberOptions.java b/common/src/main/java/com/abrenoch/hyperiongrabber/common/util/HyperionGrabberOptions.java index c35f6548..cde4df3d 100644 --- a/common/src/main/java/com/abrenoch/hyperiongrabber/common/util/HyperionGrabberOptions.java +++ b/common/src/main/java/com/abrenoch/hyperiongrabber/common/util/HyperionGrabberOptions.java @@ -14,6 +14,12 @@ public class HyperionGrabberOptions { private final boolean USE_AVERAGE_COLOR; private final int BLACK_THRESHOLD = 5; // The limit each RGB value must be under to be considered a black pixel [0-255] + private final String SAMPLING_METHOD; + private final int SAMPLING_RATE; + private final String START_EFFECT; + private final float TURNAROUND_RATE; + private final int TURNAROUND_OFFSET; + public HyperionGrabberOptions(int horizontalLED, int verticalLED, int frameRate, boolean useAvgColor) { /* @@ -26,17 +32,76 @@ public HyperionGrabberOptions(int horizontalLED, int verticalLED, int frameRate, FRAME_RATE = frameRate; USE_AVERAGE_COLOR = useAvgColor; + /* + * no input for videoMode + */ + SAMPLING_METHOD = "NONE"; + SAMPLING_RATE = 0; + START_EFFECT = "NONE"; + TURNAROUND_RATE = 0.0f; + TURNAROUND_OFFSET = 0; + if (DEBUG) { + Log.d(TAG, "Video Mode Options "); Log.d(TAG, "Horizontal LED Count: " + String.valueOf(horizontalLED)); Log.d(TAG, "Vertical LED Count: " + String.valueOf(verticalLED)); Log.d(TAG, "Minimum Image Packet: " + String.valueOf(MINIMUM_IMAGE_PACKET_SIZE)); } } + //AudioMode + public HyperionGrabberOptions(int horizontalLED, int verticalLED, String samplingMethod, int samplingRate, String startEffect, float turnAroundRate, int turnaroundOffset) { + MINIMUM_IMAGE_PACKET_SIZE = horizontalLED * verticalLED * 3; + SAMPLING_METHOD = samplingMethod; + SAMPLING_RATE = samplingRate; + START_EFFECT = startEffect; + TURNAROUND_RATE = turnAroundRate; + TURNAROUND_OFFSET = turnaroundOffset; + + /* + * no input for audioMode + */ + FRAME_RATE = 0; + USE_AVERAGE_COLOR = false; + + if (DEBUG) { + Log.d(TAG, "Audio Mode Options "); + Log.d(TAG, "Horizontal LED Count: " + String.valueOf(horizontalLED)); + Log.d(TAG, "Vertical LED Count: " + String.valueOf(verticalLED)); + Log.d(TAG, "Minimum Image Packet: " + String.valueOf(MINIMUM_IMAGE_PACKET_SIZE)); + Log.d(TAG, "Sampling Rate: " + String.valueOf(SAMPLING_RATE)); + Log.d(TAG, "Turnaround Rate: " + String.valueOf(TURNAROUND_RATE)); + } + } + + // pixelCount for AudioGradientView = scaledDimension + public HyperionGrabberOptions(int horizontalLED, int verticalLED) { + MINIMUM_IMAGE_PACKET_SIZE = horizontalLED * verticalLED * 3; + + // no need here + FRAME_RATE = 0; + USE_AVERAGE_COLOR = false; + SAMPLING_METHOD = "NONE"; + SAMPLING_RATE = 0; + START_EFFECT = "NONE"; + TURNAROUND_RATE = 0.0f; + TURNAROUND_OFFSET = 0; + } + public int getFrameRate() { return FRAME_RATE; } public boolean useAverageColor() { return USE_AVERAGE_COLOR; } + public String getSamplingMethod() { return SAMPLING_METHOD; } + + public int getSamplingRate() { return SAMPLING_RATE; } + + public String getStartEffect() { return START_EFFECT; } + + public float getTurnaroundRate() { return TURNAROUND_RATE; } + + public int getTurnaroundOffset() { return TURNAROUND_OFFSET; } + /** * returns the divisor best suited to be used to meet the minimum image packet size * Since we only want to scale using whole numbers, we need to find what common divisors diff --git a/common/src/main/res/values/pref_values.xml b/common/src/main/res/values/pref_values.xml index 607e9422..cd55c417 100644 --- a/common/src/main/res/values/pref_values.xml +++ b/common/src/main/res/values/pref_values.xml @@ -34,4 +34,31 @@ pref_key_use_avg_color false + + pref_key_sampling_method + Waveform + + Waveform + FFT + + + pref_key_sampling_rate + 11111 + + pref_key_effect + Rainbow swirl + + Rainbow swirl + Plasma + Rainbow mod + IceBlue + RGB white + + + pref_key_turnaround_rate + 200 + + pref_key_turnaround_offset + 111 + diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 9bc1d811..726a1a0f 100755 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -47,6 +47,19 @@ Capturing Capture Rate Frames per second in which to send data to hyperion + + Audio + Sampling Method + Audio Capture Method + Sampling Rate + Number of samples per second + Effect + Start effect + Effect Tempo + Floating number to turnaround effect transition + Effect Offset + Number LEDs of effect start + General Start screen grabber when device starts diff --git a/common/src/main/res/xml/pref_general.xml b/common/src/main/res/xml/pref_general.xml index 9d9726a6..a3111b55 100644 --- a/common/src/main/res/xml/pref_general.xml +++ b/common/src/main/res/xml/pref_general.xml @@ -104,6 +104,63 @@ + + + + + + + + + + + + + + diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 960f2a06..b2e24707 100755 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ package="com.abrenoch.hyperiongrabber.mobile"> diff --git a/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/AppCompatPreferenceActivity.java b/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/AppCompatPreferenceActivity.java index 263df9fc..4b357158 100755 --- a/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/AppCompatPreferenceActivity.java +++ b/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/AppCompatPreferenceActivity.java @@ -27,8 +27,8 @@ protected void onCreate(Bundle savedInstanceState) { getDelegate().installViewFactory(); getDelegate().onCreate(savedInstanceState); super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayShowHomeEnabled(true); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); + //getSupportActionBar().setDisplayShowHomeEnabled(true); + //getSupportActionBar().setDisplayHomeAsUpEnabled(true); } @Override diff --git a/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/MainActivity.java b/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/MainActivity.java index e91b2196..323551ff 100755 --- a/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/MainActivity.java +++ b/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/MainActivity.java @@ -1,5 +1,6 @@ package com.abrenoch.hyperiongrabber.mobile; +import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.app.Activity; @@ -8,10 +9,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.graphics.Color; import android.media.projection.MediaProjectionManager; import android.os.Build; import android.support.annotation.RequiresApi; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; @@ -25,20 +29,31 @@ import com.abrenoch.hyperiongrabber.common.BootActivity; import com.abrenoch.hyperiongrabber.common.HyperionScreenService; +import com.abrenoch.hyperiongrabber.common.audio.AudioGradientView; +import com.abrenoch.hyperiongrabber.common.audio.HyperionAudioEncoder; +import com.abrenoch.hyperiongrabber.common.audio.HyperionAudioService; + +import java.util.Arrays; public class MainActivity extends AppCompatActivity implements ImageView.OnClickListener, ImageView.OnFocusChangeListener { + private static final boolean DEBUG = false; + private static final String TAG = "MainActivity"; public static final int REQUEST_MEDIA_PROJECTION = 1; - private static final String TAG = "DEBUG"; private boolean mRecorderRunning = false; private static MediaProjectionManager mMediaProjectionManager; + private static final int MY_PERMISSIONS_RECORD_AUDIO = 200; + static boolean hasPermission = false; + public int[] mColors; + AudioGradientView mAudioGradientView; + private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - boolean checked = intent.getBooleanExtra(HyperionScreenService.BROADCAST_TAG, false); + boolean checked = intent.getBooleanExtra(HyperionAudioService.BROADCAST_TAG, false); mRecorderRunning = checked; - String error = intent.getStringExtra(HyperionScreenService.BROADCAST_ERROR); + String error = intent.getStringExtra(HyperionAudioService.BROADCAST_ERROR); if (error != null && (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !HyperionGrabberTileService.isListening())) { @@ -48,10 +63,27 @@ public void onReceive(Context context, Intent intent) { } }; + private BroadcastReceiver mColorReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + + if (DEBUG) Log.d(TAG, HyperionAudioService.BROADCAST_COLORS); + mColors = intent.getIntArrayExtra(HyperionAudioService.BROADCAST_COLORS); + if (DEBUG) Log.d(TAG, "Colors received: " + Arrays.toString(mColors)); + + mAudioGradientView.setColors(mColors); + + } + }; + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // permission for Android 6+ + requestAudioPermissions(); + setContentView(R.layout.activity_main); mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); @@ -62,23 +94,62 @@ protected void onCreate(Bundle savedInstanceState) { iv.setFocusable(true); iv.requestFocus(); + ImageView next = findViewById(R.id.next_effect); + next.setOnClickListener(this); + next.setOnFocusChangeListener(this); + next.setFocusable(true); + next.requestFocus(); + + ImageView prev = findViewById(R.id.previous_effect); + prev.setOnClickListener(this); + prev.setOnFocusChangeListener(this); + prev.setFocusable(true); + prev.requestFocus(); + setImageViews(mRecorderRunning, false); + mAudioGradientView = findViewById(R.id.audioGradientView); LocalBroadcastManager.getInstance(this).registerReceiver( - mMessageReceiver, new IntentFilter(HyperionScreenService.BROADCAST_FILTER)); + mMessageReceiver, new IntentFilter(HyperionAudioService.BROADCAST_FILTER)); + LocalBroadcastManager.getInstance(this).registerReceiver( + mColorReceiver, new IntentFilter(HyperionAudioService.BROADCAST_X)); + checkForInstance(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public void onClick(View view) { - if (!mRecorderRunning) { - startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), - REQUEST_MEDIA_PROJECTION); - } else { - stopScreenRecorder(); + if(view.getId()==R.id.power_toggle){ + if (!mRecorderRunning) { + startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), + REQUEST_MEDIA_PROJECTION); + } else { + stopScreenRecorder(); + } + mRecorderRunning = !mRecorderRunning; + } + else if(view.getId()==R.id.next_effect){ + + if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.RAINBOW_SWIRL) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.RAINBOW_MOD; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.RAINBOW_MOD) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.ICEBLUE; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.ICEBLUE) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.RGBWHITE; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.RGBWHITE) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.PLASMA; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.PLASMA) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.RAINBOW_SWIRL; + + + } + else if(view.getId()==R.id.previous_effect){ + + if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.PLASMA) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.RGBWHITE; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.RGBWHITE) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.ICEBLUE; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.ICEBLUE) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.RAINBOW_MOD; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.RAINBOW_MOD) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.RAINBOW_SWIRL; + else if(HyperionAudioEncoder.VISUALIZATION_METHOD==HyperionAudioEncoder.VisualisationMethod.RAINBOW_SWIRL) HyperionAudioEncoder.VISUALIZATION_METHOD=HyperionAudioEncoder.VisualisationMethod.PLASMA; + + } - mRecorderRunning = !mRecorderRunning; + } @Override @@ -104,6 +175,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } Log.i(TAG, "Starting screen capture"); startScreenRecorder(resultCode, (Intent) data.clone()); + mRecorderRunning = true; } } @@ -129,8 +201,8 @@ public boolean onOptionsItemSelected(MenuItem item) { private void checkForInstance() { if (isServiceRunning()) { - Intent intent = new Intent(this, HyperionScreenService.class); - intent.setAction(HyperionScreenService.GET_STATUS); + Intent intent = new Intent(this, HyperionAudioService.class); + intent.setAction(HyperionAudioService.GET_STATUS); startService(intent); } } @@ -141,34 +213,46 @@ public void startScreenRecorder(int resultCode, Intent data) { public void stopScreenRecorder() { if (mRecorderRunning) { - Intent intent = new Intent(this, HyperionScreenService.class); - intent.setAction(HyperionScreenService.ACTION_EXIT); + Intent intent = new Intent(this, HyperionAudioService.class); + intent.setAction(HyperionAudioService.ACTION_EXIT); startService(intent); } } private void setImageViews(boolean running, boolean animated) { - View rainbow = findViewById(R.id.sweepGradientView); + //View rainbow = findViewById(R.id.sweepGradientView); + View audioGradientView = findViewById(R.id.audioGradientView); View message = findViewById(R.id.grabberStartedText); View buttonImage = findViewById(R.id.power_toggle); + View buttonNextEffect = findViewById(R.id.next_effect); + View buttonPrevEffect = findViewById(R.id.previous_effect); + if (running) { - if (animated){ - fadeView(rainbow, true); - fadeView(message, true); - } else { - rainbow.setVisibility(View.VISIBLE); - message.setVisibility(View.VISIBLE); - } +// if (animated){ +// fadeView(rainbow, true); +// fadeView(message, true); +// } else { +// rainbow.setVisibility(View.VISIBLE); +// message.setVisibility(View.VISIBLE); +// } + message.setVisibility(View.VISIBLE); + audioGradientView.setVisibility(View.VISIBLE); buttonImage.setAlpha((float) 1); + buttonNextEffect.setVisibility(View.VISIBLE); + buttonPrevEffect.setVisibility(View.VISIBLE); } else { - if (animated){ - fadeView(rainbow, false); - fadeView(message, false); - } else { - rainbow.setVisibility(View.INVISIBLE); - message.setVisibility(View.INVISIBLE); - } +// if (animated){ +// fadeView(rainbow, false); +// fadeView(message, false); +// } else { +// rainbow.setVisibility(View.INVISIBLE); +// message.setVisibility(View.INVISIBLE); +// } + message.setVisibility(View.INVISIBLE); + audioGradientView.setVisibility(View.INVISIBLE); buttonImage.setAlpha((float) 0.25); + buttonNextEffect.setVisibility(View.INVISIBLE); + buttonPrevEffect.setVisibility(View.INVISIBLE); } } @@ -176,7 +260,7 @@ private boolean isServiceRunning() { ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); assert manager != null; for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (HyperionScreenService.class.getName().equals(service.service.getClassName())) { + if (HyperionAudioService.class.getName().equals(service.service.getClassName())) { return true; } } @@ -197,4 +281,52 @@ public void onAnimationEnd(Animator animation) { }) .start(); } + + private void requestAudioPermissions() { + if (ContextCompat.checkSelfPermission(MainActivity.this, + Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + + //When permission is not granted by user, show them message why this permission is needed. + if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, + Manifest.permission.RECORD_AUDIO)) { + Toast.makeText(this, "Please grant permissions to record audio", Toast.LENGTH_LONG).show(); + + //Give user option to still opt-in the permissions + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.RECORD_AUDIO}, + MY_PERMISSIONS_RECORD_AUDIO); + + } else { + // Show user dialog to grant permission to record audio + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.RECORD_AUDIO}, + MY_PERMISSIONS_RECORD_AUDIO); + } + } + //If permission is granted, then go ahead recording audio + else if (ContextCompat.checkSelfPermission(MainActivity.this, + Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED) { + hasPermission = true; + } + } + + //Handling callback + @Override + public void onRequestPermissionsResult(int requestCode, + String [] permissions, int[] grantResults) { + + if (requestCode == MY_PERMISSIONS_RECORD_AUDIO) { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted, yay! + hasPermission = true; + } else { + // permission denied, boo! Disable the + // functionality that depends on this permission. + Toast.makeText(MainActivity.this, "Permissions Denied to record audio", Toast.LENGTH_LONG).show(); + } + } + } } diff --git a/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/SettingsActivity.java b/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/SettingsActivity.java index 3c089a92..cff350dd 100755 --- a/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/SettingsActivity.java +++ b/mobile/src/main/java/com/abrenoch/hyperiongrabber/mobile/SettingsActivity.java @@ -38,12 +38,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity { // verify we have a valid int value for the following preference keys switch (prefResourceID) { - case R.string.pref_key_port: - case R.string.pref_key_reconnect_delay: - case R.string.pref_key_priority: - case R.string.pref_key_x_led: - case R.string.pref_key_y_led: - case R.string.pref_key_framerate: + case R.string.pref_key_port: + case R.string.pref_key_reconnect_delay: + case R.string.pref_key_priority: + case R.string.pref_key_x_led: + case R.string.pref_key_y_led: + case R.string.pref_key_framerate: + case R.string.pref_key_sampling_rate: + case R.string.pref_key_turnaround_rate: + case R.string.pref_key_turnaround_offset: try { Integer.parseInt(value.toString()); } catch (NumberFormatException e) { @@ -144,6 +147,9 @@ public void onCreate(Bundle savedInstanceState) { bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_key_reconnect_delay))); bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_key_x_led))); bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_key_y_led))); + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_key_sampling_rate))); + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_key_turnaround_rate))); + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_key_turnaround_offset))); } @Override diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index 23a981a0..bf8b5dc1 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -11,11 +11,16 @@ android:layout_height="match_parent" > - + + + + + + + + + + @color/colorAccent + + \ No newline at end of file