diff options
Diffstat (limited to 'src/org/cyanogenmod/audiofx/widget')
4 files changed, 712 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/audiofx/widget/Biquad.java b/src/org/cyanogenmod/audiofx/widget/Biquad.java new file mode 100644 index 0000000..9a52d2e --- /dev/null +++ b/src/org/cyanogenmod/audiofx/widget/Biquad.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.audiofx.widget; + +/** + * Evaluate transfer functions of biquad filters in direct form 1. + * + * @author alankila + */ +public class Biquad { + private Complex mB0, mB1, mB2, mA0, mA1, mA2; + + public void setHighShelf(double centerFrequency, double samplingFrequency, + double dbGain, double slope) { + double w0 = 2 * Math.PI * centerFrequency / samplingFrequency; + double a = Math.pow(10, dbGain/40); + double alpha = Math.sin(w0) / 2 * Math.sqrt((a + 1 / a) * (1 / slope - 1) + 2); + + mB0 = new Complex(a*((a+1) + (a-1) *Math.cos(w0) + 2*Math.sqrt(a)*alpha), 0); + mB1 = new Complex(-2*a*((a-1) + (a+1)*Math.cos(w0)), 0); + mB2 = new Complex(a*((a+1) + (a-1) *Math.cos(w0) - 2*Math.sqrt(a)*alpha), 0); + mA0 = new Complex((a+1) - (a-1) *Math.cos(w0) + 2*Math.sqrt(a)*alpha, 0); + mA1 = new Complex(2*((a-1) - (a+1) *Math.cos(w0)), 0); + mA2 = new Complex((a+1) - (a-1) *Math.cos(w0) - 2*Math.sqrt(a)*alpha, 0); + } + + public Complex evaluateTransfer(Complex z) { + Complex zSquared = z.mul(z); + Complex nom = mB0.add(mB1.div(z)).add(mB2.div(zSquared)); + Complex den = mA0.add(mA1.div(z)).add(mA2.div(zSquared)); + return nom.div(den); + } +} diff --git a/src/org/cyanogenmod/audiofx/widget/Complex.java b/src/org/cyanogenmod/audiofx/widget/Complex.java new file mode 100644 index 0000000..64c4a85 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/widget/Complex.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.audiofx.widget; + +/** + * Java support for complex numbers. + * + * @author alankila + */ +public class Complex { + private final double mReal, mIm; + + public Complex(double real, double im) { + mReal = real; + mIm = im; + } + + /** + * Length of complex number + * + * @return length + */ + public double rho() { + return Math.sqrt(mReal * mReal + mIm * mIm); + } + + /** + * Argument of complex number + * + * @return angle in radians + */ + public double theta() { + return Math.atan2(mIm, mReal); + } + + /** + * Complex conjugate + * + * @return conjugate + */ + public Complex con() { + return new Complex(mReal, -mIm); + } + + /** + * Complex addition + * + * @param other + * @return sum + */ + public Complex add(Complex other) { + return new Complex(mReal + other.mReal, mIm + other.mIm); + } + + /** + * Complex multipply + * + * @param other + * @return multiplication result + */ + public Complex mul(Complex other) { + return new Complex(mReal * other.mReal - mIm * other.mIm, + mReal * other.mIm + mIm * other.mReal); + } + + /** + * Complex multiply with real value + * + * @param a + * @return multiplication result + */ + public Complex mul(double a) { + return new Complex(mReal * a, mIm * a); + } + + /** + * Complex division + * + * @param other + * @return division result + */ + public Complex div(Complex other) { + double lengthSquared = other.mReal * other.mReal + other.mIm * other.mIm; + return mul(other.con()).div(lengthSquared); + } + + /** + * Complex division with real value + * + * @param a + * @return division result + */ + public Complex div(double a) { + return new Complex(mReal / a, mIm / a); + } +} diff --git a/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java b/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java new file mode 100644 index 0000000..6644643 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/widget/EqualizerSurface.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * - Original code by Antti S. Lankila for DSPManager + * - Modified extensively by cyanogen for multi-band support + */ + +package org.cyanogenmod.audiofx.widget; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Paint.Cap; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.Shader; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; + +import android.view.animation.DecelerateInterpolator; +import org.cyanogenmod.audiofx.R; + +import java.util.Arrays; + +public class EqualizerSurface extends SurfaceView implements ValueAnimator.AnimatorUpdateListener { + + private static int SAMPLING_RATE = 44100; + + private int mWidth; + private int mHeight; + + private float mMinFreq = 10; + private float mMaxFreq = 21000; + + private float mMinDB = -15; + private float mMaxDB = 15; + + private int mNumBands = 6; + + private float[] mLevels = new float[mNumBands]; + private float[] mTargetLevels = new float[mNumBands]; + private float[] mCenterFreqs = new float[mNumBands]; + private final Paint mWhite, mControlBarText, mControlBar; + private final Paint mFrequencyResponseBg; + private final Paint mFrequencyResponseHighlight, mFrequencyResponseHighlight2; + + private BandUpdatedListener mBandUpdatedListener; + int mBarWidth; + int mTextSize; + private ValueAnimator mAnimation; + + public EqualizerSurface(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + setWillNotDraw(false); + + mWhite = new Paint(); + mWhite.setColor(getResources().getColor(R.color.color_grey)); + mWhite.setStyle(Style.STROKE); + mWhite.setTextSize(mTextSize = context.getResources().getDimensionPixelSize(R.dimen.eq_label_text_size)); + mWhite.setTypeface(Typeface.DEFAULT_BOLD); + mWhite.setAntiAlias(true); + + mControlBarText = new Paint(mWhite); + mControlBarText.setTextAlign(Paint.Align.CENTER); + mControlBarText.setShadowLayer(2, 0, 0, getResources().getColor(R.color.cb)); + + mControlBar = new Paint(); + mControlBar.setStyle(Style.FILL); + mControlBar.setColor(getResources().getColor(R.color.cb)); + mControlBar.setAntiAlias(true); + mControlBar.setStrokeCap(Cap.SQUARE); + mControlBar.setShadowLayer(2, 0, 0, getResources().getColor(R.color.black)); + mBarWidth = context.getResources().getDimensionPixelSize(R.dimen.eq_bar_width); +// mControlBar.setStrokeWidth(mBarWidth); + + mFrequencyResponseBg = new Paint(); + mFrequencyResponseBg.setStyle(Style.FILL); + mFrequencyResponseBg.setAntiAlias(true); + + mFrequencyResponseHighlight = new Paint(); + mFrequencyResponseHighlight.setStyle(Style.STROKE); + mFrequencyResponseHighlight.setStrokeWidth(6); + mFrequencyResponseHighlight.setColor(getResources().getColor(R.color.freq_hl)); + mFrequencyResponseHighlight.setAntiAlias(true); + + mFrequencyResponseHighlight2 = new Paint(); + mFrequencyResponseHighlight2.setStyle(Style.STROKE); + mFrequencyResponseHighlight2.setStrokeWidth(3); + mFrequencyResponseHighlight2.setColor(getResources().getColor(R.color.freq_hl2)); + mFrequencyResponseHighlight2.setAntiAlias(true); + } + + /** + * Listener for bands being modified via touch events + * + * Invoked with the index of the modified band, and the + * new value in dB. If the widget is read-only, will set + * changed = false. + * + */ + public interface BandUpdatedListener { + public void onBandUpdated(int band, float dB); + public void onBandAnimating(int band, float dB); + public void onBandAnimationCompleted(); + } + + public void setBandLevelRange(float minDB, float maxDB) { + mMinDB = minDB; + mMaxDB = maxDB; + } + + public void setCenterFreqs(float[] centerFreqsKHz) { + mNumBands = centerFreqsKHz.length; + mLevels = new float[mNumBands]; + mCenterFreqs = Arrays.copyOf(centerFreqsKHz, mNumBands); + System.arraycopy(centerFreqsKHz, 0, mCenterFreqs, 0, mNumBands); + mMinFreq = mCenterFreqs[0] / 2; + mMaxFreq = (float) Math.pow(mCenterFreqs[mNumBands - 1], 2) / mCenterFreqs[mNumBands -2] / 2; + } + + public float[] softCopyLevels() { + float[] levels = new float[mNumBands]; + for (int i = 0; i < levels.length; i++) { + levels[i] = mLevels[i]; + } + return levels; + } + /* + @Override + protected Parcelable onSaveInstanceState() { + Bundle b = new Bundle(); + b.putParcelable("super", super.onSaveInstanceState()); + b.putFloatArray("levels", mLevels); + return b; + } + + @Override + protected void onRestoreInstanceState(Parcelable p) { + Bundle b = (Bundle) p; + super.onRestoreInstanceState(b.getBundle("super")); + mLevels = b.getFloatArray("levels"); + } + */ + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + setLayerType(View.LAYER_TYPE_HARDWARE, null); + buildLayer(); + } + + /** + * Returns a color that is assumed to be blended against black background, + * assuming close to sRGB behavior of screen (gamma 2.2 approximation). + * + * @param intensity desired physical intensity of color component + * @param alpha alpha value of color component + */ + private static int gamma(float intensity, float alpha) { + /* intensity = (component * alpha)^2.2 + * <=> + * intensity^(1/2.2) / alpha = component + */ + + double gamma = Math.round(255 * Math.pow(intensity, 1 / 2.2) / alpha); + return (int) Math.min(255, Math.max(0, gamma)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + final Resources res = getResources(); + mWidth = right - left; +// mHeight = bottom - top + (int) mWhite.getTextSize(); + mHeight = bottom - top; + + /** + * red > +7 + * yellow > +3 + * holo_blue_bright > 0 + * holo_blue < 0 + * holo_blue_dark < 3 + */ + int[] responseColors = new int[] { + res.getColor(R.color.eq_yellow), + res.getColor(R.color.eq_green), + res.getColor(R.color.eq_holo_bright), + res.getColor(R.color.eq_holo_blue), + res.getColor(R.color.eq_holo_dark) + }; + float[] responsePositions = new float[] { + 0, 0.2f, 0.45f, 0.6f, 1f + }; + + mFrequencyResponseBg.setShader(new LinearGradient(0, 0, 0, mHeight - mTextSize, + responseColors, responsePositions, Shader.TileMode.CLAMP)); + + int[] barColors = new int[] { + res.getColor(R.color.cb_shader), + res.getColor(R.color.cb_shader_alpha) + }; + float[] barPositions = new float[] { + 0.95f, 1f + }; + +// mControlBar.setShader(new LinearGradient(0, 0, 0, mHeight - mTextSize, +// barColors, barPositions, Shader.TileMode.CLAMP)); + } + + int mPasses = 140; + float[] mStartLevels; + float[] mDeltas; + public void setBands(float[] bands) { + if (mAnimation != null) { + mAnimation.cancel(); + mAnimation = null; + } + mTargetLevels = bands; + + mStartLevels = new float[mLevels.length]; + mDeltas = new float[mLevels.length]; + for (int i = 0; i < mStartLevels.length; i++) { + mStartLevels[i] = mLevels[i]; + + mDeltas[i] = mTargetLevels[i] - mStartLevels[i]; + } + + mAnimation = ValueAnimator.ofFloat(0f,1f); + mAnimation.addUpdateListener(this); + mAnimation.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mPasses = 35; + } + + @Override + public void onAnimationEnd(Animator animation) { + mPasses = 140; + mLevels = mTargetLevels; + animation.removeAllListeners(); + mAnimation = null; + invalidate(); + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + mAnimation.setDuration(1000); +// mAnimation.setStartDelay(100); + mAnimation.setInterpolator(new DecelerateInterpolator()); + mAnimation.start(); + + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float fraction = (Float) animation.getAnimatedFraction(); +// final float fraction = ((Float) (animation.getAnimatedValue())).floatValue(); + + for (int i = 0; i < mNumBands; i++) { +// float delta = mTargetLevels[i] - mLevels[i]; + float newValue = mDeltas[i] * fraction; + mLevels[i] = mStartLevels[i] + newValue; + } + invalidate(); + } + + public void setBand(int i, float value) { + mLevels[i] = value; + postInvalidate(); + } + + public float getBand(int i) { + return mLevels[i]; + } + + @Override + protected void onDraw(Canvas canvas) { + /* clear canvas */ + canvas.drawRGB(0, 0, 0); + + Biquad[] biquads = new Biquad[mNumBands - 1]; + for (int i = 0; i < (mNumBands - 1); i++) { + biquads[i] = new Biquad(); + } + + /* The filtering is realized with 2nd order high shelf filters, and each band + * is realized as a transition relative to the previous band. The center point for + * each filter is actually between the bands. + * + * 1st band has no previous band, so it's just a fixed gain. + */ + double gain = Math.pow(10, mLevels[0] / 20); + for (int i = 0; i < biquads.length; i++) { + biquads[i].setHighShelf(mCenterFreqs[i], SAMPLING_RATE, mLevels[i + 1] - mLevels[i], 1); + } + + Path freqResponse = new Path(); + Complex[] zn = new Complex[biquads.length]; +// final int passes = 140; + for (int i = 0; i < mPasses+1; i ++) { + double freq = reverseProjectX(i / (float)mPasses); + double omega = freq / SAMPLING_RATE * Math.PI * 2; + Complex z = new Complex(Math.cos(omega), Math.sin(omega)); + + /* Evaluate the response at frequency z */ + /* Complex z1 = z.mul(gain); */ + double lin = gain; + for (int j = 0; j < biquads.length; j++) { + zn[j] = biquads[j].evaluateTransfer(z); + lin *= zn[j].rho(); + } + + /* Magnitude response, dB */ + double dB = lin2dB(lin); + float x = projectX(freq) * mWidth; + float y = projectY(dB) * (mHeight); + + /* Set starting point at first point */ + if (i == 0) { + freqResponse.moveTo(x, y); + } else { + freqResponse.lineTo(x, y); + } + } + + Path freqResponseBg = new Path(); + freqResponseBg.addPath(freqResponse); + freqResponseBg.offset(0, -4); + freqResponseBg.lineTo(mWidth, mHeight); + freqResponseBg.lineTo(0, mHeight); + freqResponseBg.close(); + canvas.drawPath(freqResponseBg, mFrequencyResponseBg); + +// canvas.drawPath(freqResponse, mFrequencyResponseHighlight); +// canvas.drawPath(freqResponse, mFrequencyResponseHighlight2); + + /* draw vertical lines */ +// for (float freq = mMinFreq; freq < mMaxFreq;) { +// float x = projectX(freq) * mWidth; +// canvas.drawLine(x, 0, x, mHeight - 1, mGridLines); +// if (freq < 100) { +// freq += 10; +// } else if (freq < 1000) { +// freq += 100; +// } else if (freq < 10000) { +// freq += 1000; +// } else { +// freq += 10000; +// } +// } + + /* draw horizontal lines */ + for (float dB = mMinDB + 3; dB <= mMaxDB - 3; dB += 3) { + float y = projectY(dB) * mHeight; +// canvas.drawLine(0, y, mWidth - 1, y, mGridLines); +// canvas.drawText(String.format("%+d", (int)dB), 1, (y - 1), mWhite); + } + + for (int i = 0; i < mNumBands; i ++) { + float freq = mCenterFreqs[i]; + float x = projectX(freq) * mWidth; + + float y = projectY(mLevels[i]) * (mHeight); + + Log.i("eqsurface", i + " level: " + mLevels[i] + ", y: " + y); + + String frequencyText = String.format(freq < 1000 ? "%.0f" : "%.0fk", + freq < 1000 ? freq : freq / 1000); + + int targetHeight = (mHeight); + + int halfX = mBarWidth/2; + if (y > targetHeight) { + int diff = (int) Math.abs(targetHeight - y); + canvas.drawRect(x-halfX, y+diff, x+halfX, targetHeight, mControlBar); + } else { + canvas.drawRect(x-halfX, y, x+halfX, targetHeight, mControlBar); + } + + canvas.drawText(frequencyText, x, mWhite.getTextSize(), mControlBarText); + canvas.drawText(String.format("%+1.1f", mLevels[i]), x, y-1, mControlBarText); + } + } + + public void registerBandUpdatedListener(BandUpdatedListener listener) { + mBandUpdatedListener = listener; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + if (!isEnabled()) { + return false; + } + + float x = event.getX(); + float y = event.getY(); + + /* Which band is closest to the position user pressed? */ + int band = findClosest(x); + + int wy = getHeight(); + float level = (y / wy) * (mMinDB - mMaxDB) - mMinDB; + if (level < mMinDB) { + level = mMinDB; + } else if (level > mMaxDB) { + level = mMaxDB; + } + + setBand(band, level); + + if (mBandUpdatedListener != null) { + mBandUpdatedListener.onBandUpdated(band, level); + } + + return true; + } + + private float projectX(double freq) { + double pos = Math.log(freq); + double minPos = Math.log(mMinFreq); + double maxPos = Math.log(mMaxFreq); + return (float) ((pos - minPos) / (maxPos - minPos)); + } + + private double reverseProjectX(float pos) { + double minPos = Math.log(mMinFreq); + double maxPos = Math.log(mMaxFreq); + return Math.exp(pos * (maxPos - minPos) + minPos); + } + + private float projectY(double dB) { + double pos = (dB - mMinDB) / (mMaxDB - mMinDB); + return (float) (1 - pos); + } + + private double lin2dB(double rho) { + return rho != 0 ? Math.log(rho) / Math.log(10) * 20 : -99.9; + } + + /** + * Find the closest control to given horizontal pixel for adjustment + * + * @param px + * @return index of best match + */ + public int findClosest(float px) { + int idx = 0; + float best = 1e9f; + for (int i = 0; i < mNumBands; i ++) { + float freq = mCenterFreqs[i]; + float cx = projectX(freq) * mWidth; + float distance = Math.abs(cx - px); + + if (distance < best) { + idx = i; + best = distance; + } + } + + return idx; + } +} diff --git a/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java b/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java new file mode 100644 index 0000000..d2d62e1 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/widget/InterceptableLinearLayout.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.cyanogenmod.audiofx.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; + +public class InterceptableLinearLayout extends LinearLayout { + private boolean mIntercept = true; + + public InterceptableLinearLayout(Context context) { + super(context); + } + + public InterceptableLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InterceptableLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return mIntercept; + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void setInterception(boolean intercept) { + mIntercept = intercept; + } +} |