fix:android ui v3
This commit is contained in:
1 parent
8f021959ef
commit
357edf9a2d
6 files changed
+119
-29
No files matched your search
@@ -53,6 +53,7 @@ public final class AmplitudeView extends View {
|
|||||||
|
|
||||||
void setAmplitude(float amplitude) {
|
void setAmplitude(float amplitude) {
|
||||||
float level = Math.max(0, Math.min(1, amplitude));
|
float level = Math.max(0, Math.min(1, amplitude));
|
||||||
|
level = (float) Math.sqrt(level);
|
||||||
level = Math.max(level, previousLevel * 0.58f);
|
level = Math.max(level, previousLevel * 0.58f);
|
||||||
System.arraycopy(levels, 1, levels, 0, levels.length - 1);
|
System.arraycopy(levels, 1, levels, 0, levels.length - 1);
|
||||||
levels[levels.length - 1] = level;
|
levels[levels.length - 1] = level;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.github.transui.android;
|
||||||
|
|
||||||
|
final class AudioLevelMeter {
|
||||||
|
private double noiseFloorDecibels = Double.NaN;
|
||||||
|
|
||||||
|
float measure(short[] samples, int count) {
|
||||||
|
double squaredTotal = 0;
|
||||||
|
double peak = 0;
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
double normalized = samples[index] / (double) Short.MAX_VALUE;
|
||||||
|
squaredTotal += normalized * normalized;
|
||||||
|
peak = Math.max(peak, Math.abs(normalized));
|
||||||
|
}
|
||||||
|
double rootMeanSquare = Math.sqrt(squaredTotal / count);
|
||||||
|
double effectiveLevel = Math.max(rootMeanSquare, peak * 0.12);
|
||||||
|
if (effectiveLevel <= 0) return 0;
|
||||||
|
|
||||||
|
double decibels = 20 * Math.log10(effectiveLevel);
|
||||||
|
if (Double.isNaN(noiseFloorDecibels)) {
|
||||||
|
noiseFloorDecibels = decibels;
|
||||||
|
} else if (decibels < noiseFloorDecibels + 3) {
|
||||||
|
noiseFloorDecibels += (decibels - noiseFloorDecibels) * 0.05;
|
||||||
|
} else {
|
||||||
|
noiseFloorDecibels += (decibels - noiseFloorDecibels) * 0.001;
|
||||||
|
}
|
||||||
|
|
||||||
|
double relative = (decibels - noiseFloorDecibels - 1.5) / 22;
|
||||||
|
double calibratedFallback = ((decibels + 72) / 54) * 0.35;
|
||||||
|
return (float) Math.max(0, Math.min(1, Math.max(relative, calibratedFallback)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import android.os.Looper;
|
|||||||
import android.speech.SpeechRecognizer;
|
import android.speech.SpeechRecognizer;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -38,9 +39,42 @@ public final class TransuiInputMethodService extends InputMethodService {
|
|||||||
amplitude = view.findViewById(R.id.voice_amplitude);
|
amplitude = view.findViewById(R.id.voice_amplitude);
|
||||||
record = view.findViewById(R.id.voice_record);
|
record = view.findViewById(R.id.voice_record);
|
||||||
view.findViewById(R.id.voice_keyboard).setOnClickListener(button -> returnToKeyboard());
|
view.findViewById(R.id.voice_keyboard).setOnClickListener(button -> returnToKeyboard());
|
||||||
|
applySystemBarInsets(view);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applySystemBarInsets(View inputView) {
|
||||||
|
int originalLeft = inputView.getPaddingLeft();
|
||||||
|
int originalTop = inputView.getPaddingTop();
|
||||||
|
int originalRight = inputView.getPaddingRight();
|
||||||
|
int originalBottom = inputView.getPaddingBottom();
|
||||||
|
inputView.setOnApplyWindowInsetsListener((view, insets) -> {
|
||||||
|
int left;
|
||||||
|
int right;
|
||||||
|
int bottom;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
android.graphics.Insets navigation = insets.getInsets(
|
||||||
|
WindowInsets.Type.navigationBars());
|
||||||
|
android.graphics.Insets gestures = insets.getInsets(
|
||||||
|
WindowInsets.Type.mandatorySystemGestures());
|
||||||
|
left = Math.max(navigation.left, gestures.left);
|
||||||
|
right = Math.max(navigation.right, gestures.right);
|
||||||
|
bottom = Math.max(navigation.bottom, gestures.bottom);
|
||||||
|
} else {
|
||||||
|
left = insets.getSystemWindowInsetLeft();
|
||||||
|
right = insets.getSystemWindowInsetRight();
|
||||||
|
bottom = insets.getSystemWindowInsetBottom();
|
||||||
|
}
|
||||||
|
view.setPadding(
|
||||||
|
originalLeft + left,
|
||||||
|
originalTop,
|
||||||
|
originalRight + right,
|
||||||
|
originalBottom + bottom);
|
||||||
|
return insets;
|
||||||
|
});
|
||||||
|
inputView.post(inputView::requestApplyInsets);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartInputView(android.view.inputmethod.EditorInfo info, boolean restarting) {
|
public void onStartInputView(android.view.inputmethod.EditorInfo info, boolean restarting) {
|
||||||
super.onStartInputView(info, restarting);
|
super.onStartInputView(info, restarting);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ final class VoiceRecordingController {
|
|||||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||||
private final AtomicBoolean started = new AtomicBoolean();
|
private final AtomicBoolean started = new AtomicBoolean();
|
||||||
private final TranscriptionClient client = new TranscriptionClient();
|
private final TranscriptionClient client = new TranscriptionClient();
|
||||||
|
private final AudioLevelMeter levelMeter = new AudioLevelMeter();
|
||||||
|
|
||||||
VoiceRecordingController(Context context, AppConfig config, Listener listener) {
|
VoiceRecordingController(Context context, AppConfig config, Listener listener) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
@@ -129,7 +130,7 @@ final class VoiceRecordingController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
appendSamples(pcm, samples, count);
|
appendSamples(pcm, samples, count);
|
||||||
listener.onAmplitude(calculateAmplitude(samples, count));
|
listener.onAmplitude(levelMeter.measure(samples, count));
|
||||||
if (stopRequested.get()) break;
|
if (stopRequested.get()) break;
|
||||||
if (SystemClock.elapsedRealtime() - startedAt >= MAX_RECORDING_MS) break;
|
if (SystemClock.elapsedRealtime() - startedAt >= MAX_RECORDING_MS) break;
|
||||||
}
|
}
|
||||||
@@ -160,16 +161,4 @@ final class VoiceRecordingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float calculateAmplitude(short[] samples, int count) {
|
|
||||||
double squaredTotal = 0;
|
|
||||||
for (int index = 0; index < count; index++) {
|
|
||||||
double normalized = samples[index] / (double) Short.MAX_VALUE;
|
|
||||||
squaredTotal += normalized * normalized;
|
|
||||||
}
|
|
||||||
double rootMeanSquare = Math.sqrt(squaredTotal / count);
|
|
||||||
if (rootMeanSquare <= 0) return 0;
|
|
||||||
double decibels = 20 * Math.log10(rootMeanSquare);
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -39,11 +39,20 @@
|
|||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<io.github.transui.android.AmplitudeView
|
||||||
|
android:id="@+id/voice_amplitude"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="86dp"
|
android:layout_height="54dp"
|
||||||
android:gravity="center_vertical"
|
android:layout_marginStart="28dp"
|
||||||
android:orientation="horizontal"
|
android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginEnd="28dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingBottom="5dp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="82dp"
|
||||||
android:paddingStart="12dp"
|
android:paddingStart="12dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingBottom="10dp">
|
android:paddingBottom="10dp">
|
||||||
@@ -52,31 +61,22 @@
|
|||||||
android:id="@+id/voice_keyboard"
|
android:id="@+id/voice_keyboard"
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
android:background="@drawable/voice_secondary_button"
|
android:background="@drawable/voice_secondary_button"
|
||||||
android:contentDescription="@string/keyboard"
|
android:contentDescription="@string/keyboard"
|
||||||
android:padding="13dp"
|
android:padding="13dp"
|
||||||
android:src="@drawable/ic_keyboard"
|
android:src="@drawable/ic_keyboard"
|
||||||
android:tint="@color/ink" />
|
android:tint="@color/ink" />
|
||||||
|
|
||||||
<io.github.transui.android.AmplitudeView
|
|
||||||
android:id="@+id/voice_amplitude"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="58dp"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:paddingTop="7dp"
|
|
||||||
android:paddingBottom="7dp" />
|
|
||||||
|
|
||||||
<io.github.transui.android.PushToTalkButton
|
<io.github.transui.android.PushToTalkButton
|
||||||
android:id="@+id/voice_record"
|
android:id="@+id/voice_record"
|
||||||
android:layout_width="72dp"
|
android:layout_width="72dp"
|
||||||
android:layout_height="72dp"
|
android:layout_height="72dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
android:background="@drawable/voice_primary_button"
|
android:background="@drawable/voice_primary_button"
|
||||||
android:contentDescription="@string/stop"
|
android:contentDescription="@string/stop"
|
||||||
android:padding="21dp"
|
android:padding="21dp"
|
||||||
android:src="@drawable/ic_stop"
|
android:src="@drawable/ic_stop"
|
||||||
android:tint="@color/paper" />
|
android:tint="@color/paper" />
|
||||||
</LinearLayout>
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package io.github.transui.android;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public final class AudioLevelMeterTest {
|
||||||
|
@Test
|
||||||
|
public void silenceStaysAtZero() {
|
||||||
|
AudioLevelMeter meter = new AudioLevelMeter();
|
||||||
|
|
||||||
|
assertEquals(0, meter.measure(new short[1024], 1024), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void speechRisesAboveQuietMicrophoneInput() {
|
||||||
|
AudioLevelMeter meter = new AudioLevelMeter();
|
||||||
|
short[] quiet = alternatingSamples(8);
|
||||||
|
short[] speech = alternatingSamples(1200);
|
||||||
|
|
||||||
|
float quietLevel = meter.measure(quiet, quiet.length);
|
||||||
|
float speechLevel = meter.measure(speech, speech.length);
|
||||||
|
|
||||||
|
assertTrue(speechLevel > quietLevel + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static short[] alternatingSamples(int amplitude) {
|
||||||
|
short[] samples = new short[1024];
|
||||||
|
for (int index = 0; index < samples.length; index++) {
|
||||||
|
samples[index] = (short) (index % 2 == 0 ? amplitude : -amplitude);
|
||||||
|
}
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user