fix:android ui v3
Android / build-android (push) Successful in 2m31s
Android / build-android (release) Successful in 2m31s
Build / build-amd64 (release) Successful in 35s
Build / build-arm64 (release) Canceled after 0s

This commit is contained in:
intelligence committed 2026-08-29 16:45:21 +02:00
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));
}
} }
+16 -16
View File
@@ -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;
}
}