Add isolated Android AudioTrack and AAudio voice output probe
This commit is contained in:
1 parent
9d948d399e
commit
11a3a20655
6 files changed
+408
No files matched your search
@@ -0,0 +1,21 @@
|
||||
plugins { id("com.android.application") }
|
||||
|
||||
android {
|
||||
namespace = "com.utun.audioprobe"
|
||||
compileSdk = 36
|
||||
ndkVersion = "29.0.14206865"
|
||||
defaultConfig {
|
||||
applicationId = "com.utun.audioprobe"
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1"
|
||||
ndk { abiFilters += "arm64-v8a" }
|
||||
externalNativeBuild { cmake { arguments("-DANDROID_STL=c++_static") } }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<application android:label="uTun audio probe" android:theme="@android:style/Theme.Material.Light.NoActionBar" android:allowBackup="false">
|
||||
<activity android:name=".ProbeActivity" android:exported="true" android:configChanges="orientation|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(call_audio_probe LANGUAGES C CXX)
|
||||
get_filename_component(UTUN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../.." ABSOLUTE)
|
||||
add_library(call_audio_probe SHARED probe.cpp "${UTUN_ROOT}/lib/debug_config.c")
|
||||
target_include_directories(call_audio_probe PRIVATE "${UTUN_ROOT}/lib")
|
||||
target_compile_features(call_audio_probe PRIVATE cxx_std_17)
|
||||
target_compile_options(call_audio_probe PRIVATE -Wall -Wextra -fvisibility=hidden -ffunction-sections -fdata-sections)
|
||||
target_link_options(call_audio_probe PRIVATE -Wl,--gc-sections)
|
||||
target_link_libraries(call_audio_probe aaudio log dl)
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "debug_config.h"
|
||||
#include <aaudio/AAudio.h>
|
||||
#include <android/log.h>
|
||||
#include <jni.h>
|
||||
#include <dlfcn.h>
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
/* Один worker Java владеет stream; callbacks только заполняют тишину и публикуют счётчики. */
|
||||
struct Probe {
|
||||
AAudioStream* stream = nullptr;
|
||||
int channels = 0;
|
||||
std::atomic<int> error{0}, callbacks{0}, callbackFrames{0};
|
||||
int previousError = 0;
|
||||
};
|
||||
static Probe probe;
|
||||
|
||||
static void log_hook(int level, const char*, const char* message) {
|
||||
int priority = level == DEBUG_LEVEL_ERROR ? ANDROID_LOG_ERROR : level == DEBUG_LEVEL_WARN ? ANDROID_LOG_WARN : ANDROID_LOG_INFO;
|
||||
__android_log_write(priority, "AudioProbe", message);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM*, void*) {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_enable_console(0); debug_set_log_hook(log_hook);
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
static aaudio_data_callback_result_t data_callback(AAudioStream*, void* context, void* output, int32_t frames) {
|
||||
auto* p = static_cast<Probe*>(context);
|
||||
std::memset(output, 0, size_t(frames) * p->channels * sizeof(int16_t));
|
||||
p->callbackFrames.store(frames, std::memory_order_relaxed);
|
||||
p->callbacks.fetch_add(1, std::memory_order_relaxed);
|
||||
return AAUDIO_CALLBACK_RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
static void error_callback(AAudioStream*, void* context, aaudio_result_t error) {
|
||||
static_cast<Probe*>(context)->error.store(error, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static bool check_result(const char* operation, aaudio_result_t result) {
|
||||
if (result >= 0) return true;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "probe operation=%s result=%d reason=%s", operation, result, AAudio_convertResultToText(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Только после выхода callback: requestStop/close выполняет worker, не callback ошибки. */
|
||||
extern "C" JNIEXPORT void JNICALL Java_com_utun_audioprobe_ProbeActivity_closeNative(JNIEnv*, jclass) {
|
||||
if (!probe.stream) return;
|
||||
check_result("stop", AAudioStream_requestStop(probe.stream));
|
||||
check_result("close", AAudioStream_close(probe.stream));
|
||||
probe.stream = nullptr;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "probe closed");
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_com_utun_audioprobe_ProbeActivity_openNative(
|
||||
JNIEnv*, jclass, jboolean exclusive, jint channels) {
|
||||
if (probe.stream || (channels != 1 && channels != 2)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "probe invalid open stream=%p channels=%d", probe.stream, channels);
|
||||
return JNI_FALSE;
|
||||
}
|
||||
probe.channels = channels; probe.error = 0; probe.callbacks = 0; probe.callbackFrames = 0; probe.previousError = 0;
|
||||
AAudioStreamBuilder* builder = nullptr;
|
||||
if (!check_result("builder", AAudio_createStreamBuilder(&builder))) return JNI_FALSE;
|
||||
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
|
||||
AAudioStreamBuilder_setUsage(builder, AAUDIO_USAGE_VOICE_COMMUNICATION);
|
||||
AAudioStreamBuilder_setContentType(builder, AAUDIO_CONTENT_TYPE_SPEECH);
|
||||
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16);
|
||||
AAudioStreamBuilder_setSampleRate(builder, 48000);
|
||||
AAudioStreamBuilder_setChannelCount(builder, channels);
|
||||
AAudioStreamBuilder_setSharingMode(builder, exclusive ? AAUDIO_SHARING_MODE_EXCLUSIVE : AAUDIO_SHARING_MODE_SHARED);
|
||||
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
|
||||
AAudioStreamBuilder_setDataCallback(builder, data_callback, &probe);
|
||||
AAudioStreamBuilder_setErrorCallback(builder, error_callback, &probe);
|
||||
aaudio_result_t opened = AAudioStreamBuilder_openStream(builder, &probe.stream);
|
||||
check_result("delete builder", AAudioStreamBuilder_delete(builder));
|
||||
if (!check_result("open", opened)) return JNI_FALSE;
|
||||
if (AAudioStream_getFormat(probe.stream) != AAUDIO_FORMAT_PCM_I16 || AAudioStream_getChannelCount(probe.stream) != channels) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "probe unexpected format/channel count");
|
||||
check_result("close invalid", AAudioStream_close(probe.stream)); probe.stream = nullptr;
|
||||
return JNI_FALSE;
|
||||
}
|
||||
int burst = AAudioStream_getFramesPerBurst(probe.stream);
|
||||
int rate = AAudioStream_getSampleRate(probe.stream);
|
||||
int wanted = std::max(rate / 50, burst * 2);
|
||||
int actual = AAudioStream_setBufferSizeInFrames(probe.stream, wanted);
|
||||
check_result("buffer", actual);
|
||||
// Экспорт доступен не во всех версиях NDK; неизвестность не выдаём за отказ MMAP.
|
||||
auto is_mmap = reinterpret_cast<bool (*)(AAudioStream*)>(dlsym(RTLD_DEFAULT, "AAudioStream_isMMapUsed"));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "probe opened exclusiveRequested=%d sharingActual=%d channels=%d rate=%d "
|
||||
"performance=%d mmap=%d burst=%d bufferRequested=%d bufferActual=%d capacity=%d device=%d",
|
||||
exclusive, AAudioStream_getSharingMode(probe.stream), channels, rate,
|
||||
AAudioStream_getPerformanceMode(probe.stream), is_mmap ? int(is_mmap(probe.stream)) : -1,
|
||||
burst, wanted, actual, AAudioStream_getBufferCapacityInFrames(probe.stream), AAudioStream_getDeviceId(probe.stream));
|
||||
if (!check_result("start", AAudioStream_requestStart(probe.stream))) {
|
||||
check_result("close failed start", AAudioStream_close(probe.stream)); probe.stream = nullptr;
|
||||
return JNI_FALSE;
|
||||
}
|
||||
return JNI_TRUE;
|
||||
}
|
||||
|
||||
/* Timestamp опрашивается вне realtime callback. Java одинаково агрегирует AAudio и AudioTrack. */
|
||||
extern "C" JNIEXPORT void JNICALL Java_com_utun_audioprobe_ProbeActivity_sampleNative(JNIEnv* env, jclass, jlongArray output) {
|
||||
if (!probe.stream || env->GetArrayLength(output) != 10) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "probe invalid sample"); return;
|
||||
}
|
||||
auto* s = probe.stream;
|
||||
int64_t frame = -1, stamp = -1;
|
||||
int64_t written = AAudioStream_getFramesWritten(s);
|
||||
int result = AAudioStream_getTimestamp(s, CLOCK_MONOTONIC, &frame, &stamp);
|
||||
int error = probe.error.load(std::memory_order_relaxed);
|
||||
if (error && error != probe.previousError) {
|
||||
check_result("callback", error); probe.previousError = error;
|
||||
}
|
||||
jlong values[10] = {written, frame, stamp, result, AAudioStream_getSampleRate(s), AAudioStream_getXRunCount(s),
|
||||
AAudioStream_getDeviceId(s), error, probe.callbacks.load(), probe.callbackFrames.load()};
|
||||
env->SetLongArrayRegion(output, 0, 10, values);
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package com.utun.audioprobe;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.AudioTimestamp;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/** Изолированная проба: только тишина на выходе, захват сразу отбрасывается. Основное приложение не вызывается. */
|
||||
public final class ProbeActivity extends Activity {
|
||||
private static final String TAG = "AudioProbe";
|
||||
static { System.loadLibrary("call_audio_probe"); }
|
||||
private static native boolean openNative(boolean exclusive, int channels);
|
||||
private static native void sampleNative(long[] out);
|
||||
private static native void closeNative();
|
||||
private volatile boolean cancelled;
|
||||
private AudioManager audio;
|
||||
private TextView status;
|
||||
private final AtomicInteger captureError = new AtomicInteger();
|
||||
private final AtomicLong captured = new AtomicLong();
|
||||
|
||||
@Override public void onCreate(Bundle saved) {
|
||||
super.onCreate(saved);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
status = new TextView(this); status.setTextSize(18); status.setPadding(30, 100, 30, 30); setContentView(status);
|
||||
status.setText("uTun audio probe\nSilent output; microphone data discarded.\nResults: adb logcat -s AudioProbe");
|
||||
audio = getSystemService(AudioManager.class);
|
||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.e(TAG, "RECORD_AUDIO permission required; install debug APK with adb install -g");
|
||||
status.append("\nMicrophone permission missing."); return;
|
||||
}
|
||||
new Thread(this::runProbe, "audio-probe-owner").start();
|
||||
}
|
||||
|
||||
@Override protected void onStop() { cancelled = true; super.onStop(); }
|
||||
|
||||
private void show(String text) {
|
||||
Log.i(TAG, text);
|
||||
runOnUiThread(() -> { if (!isDestroyed()) status.setText(text + "\nResults: adb logcat -s AudioProbe"); });
|
||||
}
|
||||
|
||||
private void runProbe() {
|
||||
int previousMode = audio.getMode();
|
||||
if (previousMode != AudioManager.MODE_NORMAL) {
|
||||
Log.e(TAG, "probe refused: audio mode=" + previousMode + "; finish other calls first"); return;
|
||||
}
|
||||
AudioRecord record = null;
|
||||
Thread capture = null;
|
||||
try {
|
||||
audio.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
int min = AudioRecord.getMinBufferSize(48000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
||||
if (min <= 0) throw new IllegalStateException("record minimum=" + min);
|
||||
record = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, 48000,
|
||||
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(min * 2, 3840));
|
||||
if (record.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException("record not initialized");
|
||||
record.startRecording();
|
||||
if (record.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) throw new IllegalStateException("record not started");
|
||||
AudioRecord input = record;
|
||||
capture = new Thread(() -> {
|
||||
short[] discard = new short[960];
|
||||
while (!cancelled) {
|
||||
int n = input.read(discard, 0, discard.length, AudioRecord.READ_BLOCKING);
|
||||
if (n <= 0) {
|
||||
if (!cancelled) { captureError.set(n == 0 ? -1 : n); Log.e(TAG, "capture read=" + n); }
|
||||
break;
|
||||
}
|
||||
captured.addAndGet(n);
|
||||
}
|
||||
}, "audio-probe-capture");
|
||||
capture.start();
|
||||
String api = getIntent().getStringExtra("api");
|
||||
int channels = getIntent().getIntExtra("channels", 0);
|
||||
if (api == null || api.equals("track")) trial("track-mono", 0, 1);
|
||||
if (api == null || api.equals("aaudio")) {
|
||||
for (int c : new int[]{1, 2}) {
|
||||
if (channels != 0 && channels != c) continue;
|
||||
for (int mode : new int[]{1, 2}) trial("aaudio-" + (mode == 1 ? "shared" : "exclusive") + "-ch" + c, mode, c);
|
||||
}
|
||||
}
|
||||
show("probe complete captured=" + captured.get() + " captureError=" + captureError.get() + " cancelled=" + cancelled);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "probe failed", e); show("probe failed: " + e);
|
||||
} finally {
|
||||
cancelled = true;
|
||||
if (record != null) {
|
||||
try { record.stop(); } catch (Exception e) { Log.e(TAG, "capture stop failed", e); }
|
||||
if (capture != null) {
|
||||
try { capture.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.e(TAG, "capture join", e); }
|
||||
}
|
||||
record.release();
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) audio.clearCommunicationDevice();
|
||||
else audio.setSpeakerphoneOn(false);
|
||||
audio.setMode(previousMode);
|
||||
Log.i(TAG, "probe resources released mode=" + audio.getMode());
|
||||
}
|
||||
}
|
||||
|
||||
private void route(int type) throws InterruptedException {
|
||||
if (cancelled) return;
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
AudioDeviceInfo wanted = null;
|
||||
for (AudioDeviceInfo d : audio.getAvailableCommunicationDevices()) if (d.getType() == type) wanted = d;
|
||||
if (wanted == null || !audio.setCommunicationDevice(wanted)) throw new IllegalStateException("route unavailable type=" + type);
|
||||
long deadline = SystemClock.elapsedRealtime() + 2000;
|
||||
while (!cancelled && SystemClock.elapsedRealtime() < deadline) {
|
||||
AudioDeviceInfo actual = audio.getCommunicationDevice();
|
||||
if (actual != null && actual.getId() == wanted.getId()) {
|
||||
Log.i(TAG, "route confirmed type=" + type + " id=" + actual.getId() + " mode=" + audio.getMode());
|
||||
Thread.sleep(500); return;
|
||||
}
|
||||
Thread.sleep(50);
|
||||
}
|
||||
throw new IllegalStateException("route confirmation timeout type=" + type);
|
||||
}
|
||||
audio.setSpeakerphoneOn(type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
private interface Output extends AutoCloseable {
|
||||
void sample(long[] out);
|
||||
void close();
|
||||
}
|
||||
|
||||
private Output open(int mode, int channels) {
|
||||
if (mode == 0) return new TrackOutput();
|
||||
if (!openNative(mode == 2, channels)) throw new IllegalStateException("AAudio open rejected");
|
||||
return new Output() {
|
||||
public void sample(long[] out) { sampleNative(out); }
|
||||
public void close() { closeNative(); }
|
||||
};
|
||||
}
|
||||
|
||||
private void trial(String name, int mode, int channels) throws InterruptedException {
|
||||
if (cancelled) return;
|
||||
show("trial " + name);
|
||||
try {
|
||||
route(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
|
||||
try (Output output = open(mode, channels)) {
|
||||
measure(output, name + "/speaker-new");
|
||||
route(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
|
||||
measure(output, name + "/earpiece-existing");
|
||||
}
|
||||
if (!cancelled) try (Output output = open(mode, channels)) { measure(output, name + "/earpiece-new"); }
|
||||
} catch (RuntimeException e) { Log.e(TAG, "trial failed name=" + name, e); }
|
||||
}
|
||||
|
||||
private void measure(Output output, String label) throws InterruptedException {
|
||||
long start = System.nanoTime(), report = start, captureStart = captured.get();
|
||||
long duration = Math.max(4, Math.min(30, getIntent().getIntExtra("seconds", 8))) * 1_000_000_000L;
|
||||
long[] row = new long[10];
|
||||
long low = Long.MAX_VALUE, high = Long.MIN_VALUE, sum = 0;
|
||||
int count = 0, invalid = 0;
|
||||
long underStart = -1, previousUnder = -1;
|
||||
while (!cancelled && System.nanoTime() - start < duration) {
|
||||
output.sample(row);
|
||||
long now = System.nanoTime();
|
||||
if (underStart < 0) underStart = row[5];
|
||||
if (previousUnder >= 0 && row[5] > previousUnder) Log.w(TAG, "underrun label=" + label + " delta=" + (row[5] - previousUnder));
|
||||
previousUnder = row[5];
|
||||
if (row[7] != 0 || captureError.get() != 0) {
|
||||
Log.e(TAG, "stream failed label=" + label + " output=" + row[7] + " capture=" + captureError.get()); break;
|
||||
}
|
||||
long delay = row[4] > 0 ? (row[2] - now) / 1000 + (row[0] - row[1]) * 1_000_000 / row[4] : -1;
|
||||
boolean valid = row[3] == 0 && row[1] >= 0 && row[1] <= row[0] && row[2] > 0 &&
|
||||
now - row[2] >= -200_000_000 && now - row[2] <= 500_000_000 && delay >= 0 && delay <= 2_000_000;
|
||||
if (now - start >= 2_000_000_000L) {
|
||||
if (valid) { low = Math.min(low, delay); high = Math.max(high, delay); sum += delay; count++; }
|
||||
else invalid++;
|
||||
}
|
||||
if (now - report >= 1_000_000_000L) {
|
||||
Log.i(TAG, "sample label=" + label + " outputUs=" + (valid ? delay : -1) + " written=" + row[0] +
|
||||
" stamp=" + row[1] + "@" + row[2] + " status=" + row[3] + " underruns=" + row[5] +
|
||||
" device=" + row[6] + " callbacks=" + row[8] + " callbackFrames=" + row[9] + " captureFrames=" + captured.get());
|
||||
report = now;
|
||||
}
|
||||
Thread.sleep(50);
|
||||
}
|
||||
Log.i(TAG, "RESULT label=" + label + " samples=" + count + " invalid=" + invalid + " outputUs=" +
|
||||
(count == 0 ? "unknown" : low + "/" + sum / count + "/" + high) + " underrunDelta=" + (row[5] - underStart) +
|
||||
" captureFrames=" + (captured.get() - captureStart) + " streamError=" + row[7]);
|
||||
}
|
||||
|
||||
private final class TrackOutput implements Output {
|
||||
private final AudioTrack track;
|
||||
private final Thread writer;
|
||||
private final AtomicLong written = new AtomicLong();
|
||||
private final AtomicInteger error = new AtomicInteger();
|
||||
private final AudioTimestamp stamp = new AudioTimestamp();
|
||||
private volatile boolean running = true;
|
||||
TrackOutput() {
|
||||
int min = AudioTrack.getMinBufferSize(48000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
||||
if (min <= 0) throw new IllegalStateException("track minimum=" + min);
|
||||
track = new AudioTrack.Builder()
|
||||
.setAudioAttributes(new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build())
|
||||
.setAudioFormat(new AudioFormat.Builder().setSampleRate(48000).setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build())
|
||||
.setBufferSizeInBytes(Math.max(min * 2, 3840)).setTransferMode(AudioTrack.MODE_STREAM)
|
||||
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY).build();
|
||||
int actual = track.setBufferSizeInFrames(960);
|
||||
if (Build.VERSION.SDK_INT >= 31) track.setStartThresholdInFrames(actual);
|
||||
Log.i(TAG, "track opened requested=1 performance=" + track.getPerformanceMode() + " buffer=" + actual);
|
||||
track.play();
|
||||
writer = new Thread(() -> {
|
||||
short[] silence = new short[960];
|
||||
while (running && !cancelled) {
|
||||
int n = track.write(silence, 0, silence.length, AudioTrack.WRITE_BLOCKING);
|
||||
if (n <= 0) {
|
||||
if (running && !cancelled) { error.set(n == 0 ? -1 : n); Log.e(TAG, "track write=" + n); }
|
||||
break;
|
||||
}
|
||||
written.addAndGet(n);
|
||||
}
|
||||
}, "audio-probe-track");
|
||||
writer.start();
|
||||
}
|
||||
public void sample(long[] out) {
|
||||
out[0] = written.get();
|
||||
boolean ok = track.getTimestamp(stamp);
|
||||
out[1] = ok ? stamp.framePosition : -1; out[2] = ok ? stamp.nanoTime : -1; out[3] = ok ? 0 : -1;
|
||||
out[4] = 48000; out[5] = track.getUnderrunCount();
|
||||
AudioDeviceInfo route = track.getRoutedDevice(); out[6] = route == null ? -1 : route.getId();
|
||||
out[7] = error.get(); out[8] = -1; out[9] = 960;
|
||||
}
|
||||
public void close() {
|
||||
running = false; track.stop();
|
||||
try { writer.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.e(TAG, "track join", e); }
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,3 +16,4 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "chatgui-android"
|
||||
include(":app")
|
||||
include(":audio-probe")
|
||||
Reference in new issue
Block a user