1 parent
e4cf707d0a
commit
8f021959ef
6 files changed
+168
-44
No files matched your search
@@ -2,49 +2,131 @@ package io.github.transui.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Shader;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class AmplitudeView extends View {
|
||||
private static final int LEVEL_COUNT = 22;
|
||||
private static final int LEVEL_COUNT = 192;
|
||||
|
||||
private final float[] levels = new float[LEVEL_COUNT];
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint edgePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint centerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path waveform = new Path();
|
||||
private final LinearGradient fillGradient;
|
||||
private final Matrix gradientMatrix = new Matrix();
|
||||
private final float sampleSpacing;
|
||||
private final float minimumHalfHeight;
|
||||
private final int signalColor;
|
||||
private float previousLevel;
|
||||
|
||||
public AmplitudeView(Context context, AttributeSet attributes) {
|
||||
super(context, attributes);
|
||||
paint.setColor(context.getColor(R.color.signal));
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
sampleSpacing = 5 * density;
|
||||
minimumHalfHeight = 1.5f * density;
|
||||
signalColor = context.getColor(R.color.signal);
|
||||
|
||||
fillPaint.setStyle(Paint.Style.FILL);
|
||||
int transparentSignal = signalColor & 0x00ffffff;
|
||||
fillGradient = new LinearGradient(
|
||||
0, 0, 0, 1,
|
||||
new int[]{transparentSignal, signalColor, transparentSignal},
|
||||
new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
|
||||
fillPaint.setShader(fillGradient);
|
||||
edgePaint.setColor(signalColor);
|
||||
edgePaint.setAlpha(210);
|
||||
edgePaint.setStyle(Paint.Style.STROKE);
|
||||
edgePaint.setStrokeWidth(1.5f * density);
|
||||
edgePaint.setStrokeJoin(Paint.Join.ROUND);
|
||||
centerPaint.setColor(signalColor);
|
||||
centerPaint.setAlpha(70);
|
||||
centerPaint.setStrokeWidth(density);
|
||||
}
|
||||
|
||||
void setAmplitude(float amplitude) {
|
||||
float level = Math.max(0, Math.min(1, amplitude));
|
||||
level = Math.max(level, previousLevel * 0.58f);
|
||||
System.arraycopy(levels, 1, levels, 0, levels.length - 1);
|
||||
levels[levels.length - 1] = Math.max(0, Math.min(1, amplitude));
|
||||
levels[levels.length - 1] = level;
|
||||
previousLevel = level;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
Arrays.fill(levels, 0);
|
||||
previousLevel = 0;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||
float top = getPaddingTop();
|
||||
float contentHeight = Math.max(1, height - getPaddingTop() - getPaddingBottom());
|
||||
gradientMatrix.setScale(1, contentHeight);
|
||||
gradientMatrix.postTranslate(0, top);
|
||||
fillGradient.setLocalMatrix(gradientMatrix);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
float availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
float gap = availableWidth / (LEVEL_COUNT * 2 - 1);
|
||||
float strokeWidth = Math.max(2, gap);
|
||||
float availableWidth = Math.max(0, getWidth() - getPaddingLeft() - getPaddingRight());
|
||||
int visibleCount = Math.min(LEVEL_COUNT,
|
||||
Math.max(2, (int) (availableWidth / sampleSpacing) + 1));
|
||||
float step = availableWidth / (visibleCount - 1);
|
||||
float startX = getPaddingLeft();
|
||||
float centerY = getHeight() / 2f;
|
||||
float maximumHeight = Math.max(0, getHeight() - getPaddingTop() - getPaddingBottom());
|
||||
paint.setStrokeWidth(strokeWidth);
|
||||
float maximumHalfHeight = Math.max(minimumHalfHeight,
|
||||
(getHeight() - getPaddingTop() - getPaddingBottom()) / 2f);
|
||||
int firstLevel = LEVEL_COUNT - visibleCount;
|
||||
|
||||
for (int index = 0; index < levels.length; index++) {
|
||||
float height = Math.max(strokeWidth, levels[index] * maximumHeight);
|
||||
float x = getPaddingLeft() + strokeWidth / 2 + index * gap * 2;
|
||||
canvas.drawLine(x, centerY - height / 2, x, centerY + height / 2, paint);
|
||||
canvas.drawLine(startX, centerY, startX + availableWidth, centerY, centerPaint);
|
||||
waveform.reset();
|
||||
|
||||
float previousX = startX;
|
||||
float previousY = upperY(firstLevel, centerY, maximumHalfHeight);
|
||||
waveform.moveTo(previousX, previousY);
|
||||
for (int index = 1; index < visibleCount; index++) {
|
||||
float x = startX + index * step;
|
||||
float y = upperY(firstLevel + index, centerY, maximumHalfHeight);
|
||||
waveform.quadTo(previousX, previousY, (previousX + x) / 2, (previousY + y) / 2);
|
||||
previousX = x;
|
||||
previousY = y;
|
||||
}
|
||||
waveform.lineTo(previousX, previousY);
|
||||
|
||||
previousY = lowerY(firstLevel + visibleCount - 1, centerY, maximumHalfHeight);
|
||||
waveform.lineTo(previousX, previousY);
|
||||
for (int index = visibleCount - 2; index >= 0; index--) {
|
||||
float x = startX + index * step;
|
||||
float y = lowerY(firstLevel + index, centerY, maximumHalfHeight);
|
||||
waveform.quadTo(previousX, previousY, (previousX + x) / 2, (previousY + y) / 2);
|
||||
previousX = x;
|
||||
previousY = y;
|
||||
}
|
||||
waveform.lineTo(previousX, previousY);
|
||||
waveform.close();
|
||||
|
||||
canvas.drawPath(waveform, fillPaint);
|
||||
canvas.drawPath(waveform, edgePaint);
|
||||
}
|
||||
|
||||
private float upperY(int index, float centerY, float maximumHalfHeight) {
|
||||
return centerY - minimumHalfHeight
|
||||
- levels[index] * (maximumHalfHeight - minimumHalfHeight);
|
||||
}
|
||||
|
||||
private float lowerY(int index, float centerY, float maximumHalfHeight) {
|
||||
return centerY + minimumHalfHeight
|
||||
+ levels[index] * (maximumHalfHeight - minimumHalfHeight);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.widget.ImageButton;
|
||||
public final class PushToTalkButton extends ImageButton {
|
||||
private Runnable pressedAction;
|
||||
private Runnable releasedAction;
|
||||
private boolean trackingPress;
|
||||
|
||||
public PushToTalkButton(Context context, AttributeSet attributes) {
|
||||
super(context, attributes);
|
||||
@@ -16,25 +17,35 @@ public final class PushToTalkButton extends ImageButton {
|
||||
void setPushToTalkActions(Runnable pressedAction, Runnable releasedAction) {
|
||||
this.pressedAction = pressedAction;
|
||||
this.releasedAction = releasedAction;
|
||||
trackingPress = false;
|
||||
setPressed(false);
|
||||
setScaleX(1);
|
||||
setScaleY(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (pressedAction == null || releasedAction == null) return super.onTouchEvent(event);
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
trackingPress = true;
|
||||
pressedAction.run();
|
||||
setPressed(true);
|
||||
animate().scaleX(0.92f).scaleY(0.92f).setDuration(90).start();
|
||||
return true;
|
||||
}
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
releasedAction.run();
|
||||
if (trackingPress) releasedAction.run();
|
||||
trackingPress = false;
|
||||
setPressed(false);
|
||||
animate().scaleX(1).scaleY(1).setDuration(120).start();
|
||||
super.performClick();
|
||||
return true;
|
||||
}
|
||||
if (event.getAction() == MotionEvent.ACTION_CANCEL) {
|
||||
releasedAction.run();
|
||||
if (trackingPress) releasedAction.run();
|
||||
trackingPress = false;
|
||||
setPressed(false);
|
||||
animate().scaleX(1).scaleY(1).setDuration(120).start();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
@@ -42,10 +53,6 @@ public final class PushToTalkButton extends ImageButton {
|
||||
|
||||
@Override
|
||||
public boolean performClick() {
|
||||
if (pressedAction != null && releasedAction != null) {
|
||||
pressedAction.run();
|
||||
releasedAction.run();
|
||||
}
|
||||
return super.performClick();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.speech.RecognizerIntent;
|
||||
import android.speech.SpeechRecognizer;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -79,6 +80,7 @@ public final class SpeechRecognitionActivity extends Activity {
|
||||
|
||||
private void configureRecordButton() {
|
||||
record.setAlpha(1);
|
||||
resizeRecordButton(config.pushToTalk ? 80 : 64, config.pushToTalk ? 23 : 19);
|
||||
if (config.pushToTalk) {
|
||||
record.setImageResource(R.drawable.ic_mic);
|
||||
record.setContentDescription(getString(R.string.hold_to_talk));
|
||||
@@ -90,6 +92,17 @@ public final class SpeechRecognitionActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private void resizeRecordButton(int sizeDp, int paddingDp) {
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
int size = Math.round(sizeDp * density);
|
||||
int padding = Math.round(paddingDp * density);
|
||||
ViewGroup.LayoutParams parameters = record.getLayoutParams();
|
||||
parameters.width = size;
|
||||
parameters.height = size;
|
||||
record.setLayoutParams(parameters);
|
||||
record.setPadding(padding, padding, padding, padding);
|
||||
}
|
||||
|
||||
private void ensurePermissionAndStart() {
|
||||
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
prepareForInput();
|
||||
@@ -181,7 +194,7 @@ public final class SpeechRecognitionActivity extends Activity {
|
||||
public void onRecording() {
|
||||
runOnUiThread(() -> {
|
||||
status.setText(R.string.listening);
|
||||
record.setImageResource(R.drawable.ic_stop);
|
||||
if (!config.pushToTalk) record.setImageResource(R.drawable.ic_stop);
|
||||
recordingStartedAt = SystemClock.elapsedRealtime();
|
||||
elapsed.setText(R.string.elapsed_zero);
|
||||
handler.post(updateElapsed);
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.speech.SpeechRecognizer;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -22,6 +23,13 @@ public final class TransuiInputMethodService extends InputMethodService {
|
||||
private VoiceRecordingController controller;
|
||||
private boolean sessionActive;
|
||||
|
||||
@Override
|
||||
public boolean onEvaluateFullscreenMode() {
|
||||
// Voice input should stay attached to the calling app in landscape. Android's
|
||||
// default extract editor is the large, blank text area shown above the IME.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateInputView() {
|
||||
FrameLayout parent = new FrameLayout(this);
|
||||
@@ -67,6 +75,7 @@ public final class TransuiInputMethodService extends InputMethodService {
|
||||
record.setAlpha(1);
|
||||
record.setOnClickListener(null);
|
||||
record.setPushToTalkActions(null, null);
|
||||
resizeRecordButton(config.pushToTalk ? 76 : 64, config.pushToTalk ? 22 : 19);
|
||||
if (config.pushToTalk) {
|
||||
record.setImageResource(R.drawable.ic_mic);
|
||||
record.setContentDescription(getString(R.string.hold_to_talk));
|
||||
@@ -78,6 +87,17 @@ public final class TransuiInputMethodService extends InputMethodService {
|
||||
}
|
||||
}
|
||||
|
||||
private void resizeRecordButton(int sizeDp, int paddingDp) {
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
int size = Math.round(sizeDp * density);
|
||||
int padding = Math.round(paddingDp * density);
|
||||
ViewGroup.LayoutParams parameters = record.getLayoutParams();
|
||||
parameters.width = size;
|
||||
parameters.height = size;
|
||||
record.setLayoutParams(parameters);
|
||||
record.setPadding(padding, padding, padding, padding);
|
||||
}
|
||||
|
||||
private void startRecording() {
|
||||
if (sessionActive) return;
|
||||
sessionActive = true;
|
||||
@@ -122,7 +142,7 @@ public final class TransuiInputMethodService extends InputMethodService {
|
||||
public void onRecording() {
|
||||
handler.post(() -> {
|
||||
status.setText(R.string.listening);
|
||||
record.setImageResource(R.drawable.ic_stop);
|
||||
if (!config.pushToTalk) record.setImageResource(R.drawable.ic_stop);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,8 @@ final class VoiceRecordingController {
|
||||
}
|
||||
|
||||
ByteArrayOutputStream pcm = new ByteArrayOutputStream(SAMPLE_RATE * 8);
|
||||
short[] samples = new short[bufferBytes / 2];
|
||||
// Keep meter updates quick even when AudioRecord asks for a large internal buffer.
|
||||
short[] samples = new short[Math.min(bufferBytes / 2, 1024)];
|
||||
long startedAt = SystemClock.elapsedRealtime();
|
||||
try {
|
||||
recorder.startRecording();
|
||||
@@ -168,6 +169,7 @@ final class VoiceRecordingController {
|
||||
double rootMeanSquare = Math.sqrt(squaredTotal / count);
|
||||
if (rootMeanSquare <= 0) return 0;
|
||||
double decibels = 20 * Math.log10(rootMeanSquare);
|
||||
return (float) Math.max(0, Math.min(1, (decibels + 50) / 50));
|
||||
// Discard the microphone noise floor and reserve the top of the meter for loud speech.
|
||||
return (float) Math.max(0, Math.min(1, (decibels + 48) / 38));
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="20dp">
|
||||
android:paddingStart="18dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="18dp">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@color/signal" />
|
||||
|
||||
<TextView
|
||||
@@ -31,51 +31,51 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:text="@string/listening"
|
||||
android:textColor="@color/ink"
|
||||
android:textSize="16sp"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_height="86dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="8dp">
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voice_keyboard"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:background="@drawable/voice_secondary_button"
|
||||
android:contentDescription="@string/keyboard"
|
||||
android:padding="12dp"
|
||||
android:padding="13dp"
|
||||
android:src="@drawable/ic_keyboard"
|
||||
android:tint="@color/ink" />
|
||||
|
||||
<io.github.transui.android.AmplitudeView
|
||||
android:id="@+id/voice_amplitude"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_height="58dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp" />
|
||||
|
||||
<io.github.transui.android.PushToTalkButton
|
||||
android:id="@+id/voice_record"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="@drawable/voice_primary_button"
|
||||
android:contentDescription="@string/stop"
|
||||
android:padding="12dp"
|
||||
android:padding="21dp"
|
||||
android:src="@drawable/ic_stop"
|
||||
android:tint="@color/paper" />
|
||||
</LinearLayout>
|
||||
|
||||
Reference in new issue
Block a user