fix(cw): stop the waterfall crashing and cut APK size by 87MB
三个真机实测暴露的问题:
1. 闪退 (给权限后 3-4 秒必崩, 且注入日志抓不到堆栈)
CwWaterfallState 用 mutableIntStateOf 记录重绘版本号, 却从音频采集线程
写入。Compose 快照状态只能在合成线程修改, 从后台线程写会在运行时崩溃 ——
崩在 Compose 内部, 所以业务类的日志注入抓不到。
改用 MutableStateFlow (本身线程安全), Canvas 侧 collectAsState 读取。
2. 同一状态的跨线程数据竞争
pushSamples 在采集线程写 rows/pending, snapshot() 在绘制线程读, 而
ArrayDeque 非线程安全 —— 并发 removeFirst()/toList() 会抛
ConcurrentModificationException 或 IndexOutOfBoundsException。
两侧统一加锁; FFT 计算放在锁外, 只有队列追加持锁。
3. APK 从 8.4MB 暴涨到 135MB
onnxruntime-android 的 AAR 自带 4 个架构原生库: arm64 28M + armv7 20M +
x86 33M + x86_64 34M = 115MB。上次提交移除 abiFilters 时把 x86 系列也
打包了进去 (仅模拟器需要)。
重新加上 abiFilters, 保留 arm64-v8a + armeabi-v7a 两个真机 ABI,
预计降至约 43MB。注意这与上次"恢复 64 位"不冲突: 那次删的是只留
armeabi-v7a 的限制, 这次是排除 x86 系列。
另外两处加固:
- CwDeepDecoder 的模型加载从 init{} 移入惰性 ensureLoaded(): 加载会触发
ONNX Runtime 原生库装载, 失败时抛 UnsatisfiedLinkError; 在构造函数中抛出
会连带崩掉创建它的 composable, try-catch 也救不回来。移到首次使用时执行,
失败经 errorMessage 上报给 UI。
- ONNX 会话限制 intraOp 线程数为 (核数-1) 且上限 4, 给音频采集和 UI 留出
余量, 默认行为会铺满所有核心。
验证:
./gradlew :feature:cw:compileDebugKotlin :core:data:compileDebugKotlin => 通过
./gradlew :core:domain:test => 79 个测试全绿
This commit is contained in:
1 parent
37300fb2a6
commit
5f297f9233
3 files changed
+92
-30
No files matched your search
@@ -84,10 +84,27 @@ class CwDeepDecoder(context: Context) : ICwDecoder {
|
||||
private var inputName = "spectrogram"
|
||||
private var outputName = "log_probs"
|
||||
|
||||
init {
|
||||
private val appContext = context.applicationContext
|
||||
private var loadAttempted = false
|
||||
|
||||
/**
|
||||
* Loads metadata and the ONNX session on first use.
|
||||
*
|
||||
* Deliberately not done in `init`: loading pulls in ONNX Runtime's native
|
||||
* library, and a failure there surfaces as [UnsatisfiedLinkError]. Thrown
|
||||
* from a constructor it would take down the whole composable that created
|
||||
* the decoder, so the work happens here where it can be reported through
|
||||
* [errorMessage] instead.
|
||||
*
|
||||
* @return true when the session is ready to run.
|
||||
*/
|
||||
private fun ensureLoaded(): Boolean {
|
||||
if (session != null) return true
|
||||
if (loadAttempted) return false
|
||||
loadAttempted = true
|
||||
try {
|
||||
val metadata = JSONObject(
|
||||
context.assets.open(METADATA_ASSET).bufferedReader().use { it.readText() }
|
||||
appContext.assets.open(METADATA_ASSET).bufferedReader().use { it.readText() }
|
||||
)
|
||||
val charArray = metadata.getJSONArray("chars")
|
||||
chars = List(charArray.length()) { charArray.getString(it) }
|
||||
@@ -95,18 +112,31 @@ class CwDeepDecoder(context: Context) : ICwDecoder {
|
||||
inputName = metadata.getString("onnx_input_name")
|
||||
outputName = metadata.getString("onnx_output_name")
|
||||
|
||||
val modelBytes = context.assets.open(MODEL_ASSET).use { it.readBytes() }
|
||||
environment = OrtEnvironment.getEnvironment()
|
||||
session = environment?.createSession(modelBytes, OrtSession.SessionOptions())
|
||||
val modelBytes = appContext.assets.open(MODEL_ASSET).use { it.readBytes() }
|
||||
val env = OrtEnvironment.getEnvironment()
|
||||
environment = env
|
||||
val options = OrtSession.SessionOptions().apply {
|
||||
// Keep a core free for audio capture and the UI; the default
|
||||
// would spread inference across every core on the device.
|
||||
val threads = (Runtime.getRuntime().availableProcessors() - 1).coerceIn(1, 4)
|
||||
setIntraOpNumThreads(threads)
|
||||
}
|
||||
session = env.createSession(modelBytes, options)
|
||||
Log.i(TAG, "DeepCW ready: ${modelBytes.size} bytes, ${chars.size} classes")
|
||||
return true
|
||||
} catch (t: Throwable) {
|
||||
// Catches UnsatisfiedLinkError (missing/mismatched .so) as well as
|
||||
// asset and session failures.
|
||||
Log.e(TAG, "DeepCW model failed to load", t)
|
||||
_errorMessage.value = "CW model failed to load: ${t.message ?: t.javaClass.simpleName}"
|
||||
_errorMessage.value =
|
||||
"CW model failed to load: ${t.message ?: t.javaClass.simpleName}"
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processBuffer(samples: FloatArray, sampleRate: Int) {
|
||||
if (session == null || samples.isEmpty()) return
|
||||
if (samples.isEmpty()) return
|
||||
if (!ensureLoaded()) return
|
||||
|
||||
val resampled = CwDeepSpectrogram.resampleLinear(
|
||||
samples, sampleRate, CwDeepSpectrogram.SAMPLE_RATE
|
||||
|
||||
Reference in new issue
Block a user