麦克风: 外接输入设备热插拔跟随重建 + 音量UI改硬件调节提示

- MicActivator: 支持 restart/setPreferredDevice(API26), 输入路由切换后重建占用
- MainActivity: AudioDeviceCallback(API23+)/HEADSET广播监听, USB/外接麦插入后
  自动重建 AudioRecord 跟随当前输入设备
- 麦克风音量标准Android无输入增益API: 远程界面滑杆禁用并提示'由功放/混音器
  硬件调节', 本地调音台+/−按钮同样提示, 避免假调节无效果
This commit is contained in:
zijunchiang
2026-09-06 01:33:59 +08:00
parent 197b01f806
commit 61ad000daf
6 changed files with 125 additions and 20 deletions
@@ -1,5 +1,6 @@
package com.local.ktv
import android.media.AudioDeviceInfo
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
@@ -15,6 +16,11 @@ import android.util.Log
* 做法: 用一个低开销的 [AudioRecord] 常驻打开麦克风并循环读取丢弃数据,
* 保持输入通道占用(不写文件、不对外暴露声音)。与 MediaRecorder 录音互斥:
* 开始录音前先 pause, 结束录音后自动 resume。
*
* 外接麦克风(USB/耳机麦)热插拔时, 系统会把默认输入路由切到外接设备并使
* 原 AudioRecord 失效 —— 此时需重建 AudioRecord 并优先占用当前活动输入设备。
* 因此对外暴露 [restart] 供设备变化监听调用; [setPreferredInput] 支持 Android 8.0+
* 的 AudioRecord.setPreferredDevice 直接把输入锁定到指定麦克风(优先外接)。
*/
class MicActivator {
private val tag = "MicActivator"
@@ -23,6 +29,9 @@ class MicActivator {
@Volatile private var running = false
@Volatile private var paused = false
/** 期望优先使用的输入设备; null = 跟随系统默认(自动切到外接)。 */
@Volatile private var preferredInput: AudioDeviceInfo? = null
val isActive: Boolean get() = running && !paused
/** 打开麦克风并保持占用。失败静默(设备无麦克风时不影响 App 使用)。 */
@@ -31,6 +40,28 @@ class MicActivator {
if (running) return
running = true
paused = false
spawnThread()
}
/** 输入设备发生变化(外接麦克风插入/拔出等)时重建 AudioRecord, 跟随新路由。 */
@Synchronized
fun restart() {
if (!running || paused) return
releaseInternal()
thread?.interrupt()
thread = null
spawnThread()
}
/** 指定首选输入设备(传入 null 恢复跟随系统默认)。 */
@Synchronized
fun setPreferredInput(device: AudioDeviceInfo?) {
preferredInput = device
restart()
}
@Synchronized
private fun spawnThread() {
thread = Thread({ readLoop() }, "mic-activator").also { it.isDaemon = true; it.start() }
}
@@ -50,7 +81,7 @@ class MicActivator {
if (!running) return
if (!paused) return
paused = false
thread = Thread({ readLoop() }, "mic-activator").also { it.isDaemon = true; it.start() }
spawnThread()
}
/** 彻底停止(不再自动恢复)。 */
@@ -89,6 +120,11 @@ class MicActivator {
sleepQuietly(5000)
continue
}
// Android 8.0+: 指定首选输入设备(如刚插入的外接麦克风), 避免仍占用内建。
val target = preferredInput
if (target != null && android.os.Build.VERSION.SDK_INT >= 26) {
runCatching { recorder.setPreferredDevice(target) }
}
record = recorder
recorder.startRecording()
val buffer = ByteArray(minBuffer.coerceAtLeast(4096))
@@ -125,3 +161,4 @@ class MicActivator {
try { Thread.sleep(ms) } catch (ignored: InterruptedException) { Thread.currentThread().interrupt() }
}
}