麦克风: 外接输入设备热插拔跟随重建 + 音量UI改硬件调节提示
- MicActivator: 支持 restart/setPreferredDevice(API26), 输入路由切换后重建占用 - MainActivity: AudioDeviceCallback(API23+)/HEADSET广播监听, USB/外接麦插入后 自动重建 AudioRecord 跟随当前输入设备 - 麦克风音量标准Android无输入增益API: 远程界面滑杆禁用并提示'由功放/混音器 硬件调节', 本地调音台+/−按钮同样提示, 避免假调节无效果
This commit is contained in:
@@ -15,3 +15,5 @@
|
|||||||
.rank-pager button:disabled{opacity:.35;box-shadow:none}
|
.rank-pager button:disabled{opacity:.35;box-shadow:none}
|
||||||
.rank-pager b{min-width:64px;text-align:center;color:#b9a9c9;font-size:11px}
|
.rank-pager b{min-width:64px;text-align:center;color:#b9a9c9;font-size:11px}
|
||||||
.empty.compact{padding:42px 15px}
|
.empty.compact{padding:42px 15px}
|
||||||
|
|
||||||
|
.audio-panel label.mic-hw{opacity:.55}.audio-panel label.mic-hw>small{grid-column:1/3;color:#9f8eb0;font-size:10px;margin-top:2px}.audio-panel label.mic-hw input[type=range]{background:rgba(255,255,255,.14)}
|
||||||
@@ -75,7 +75,9 @@ async function refresh() {
|
|||||||
$("#musicVolume").value = data.musicVolume ?? 70;
|
$("#musicVolume").value = data.musicVolume ?? 70;
|
||||||
$("#micVolume").value = data.micVolume ?? 70;
|
$("#micVolume").value = data.micVolume ?? 70;
|
||||||
$("#musicValue").textContent = data.musicVolume ?? 70;
|
$("#musicValue").textContent = data.musicVolume ?? 70;
|
||||||
$("#micValue").textContent = data.micVolume ?? 70;
|
// 麦克风音量由功放/混音器硬件调节(滑杆已禁用), 仅保留显示值。
|
||||||
|
const micHw = $("#micValue");
|
||||||
|
if (micHw) micHw.textContent = data.micVolume ?? 70;
|
||||||
$("#original").classList.toggle("active", data.vocalMode === "original");
|
$("#original").classList.toggle("active", data.vocalMode === "original");
|
||||||
$("#accompany").classList.toggle("active", data.vocalMode !== "original");
|
$("#accompany").classList.toggle("active", data.vocalMode !== "original");
|
||||||
$("#nextSong").textContent = data.next?.title || "队列为空";
|
$("#nextSong").textContent = data.next?.title || "队列为空";
|
||||||
@@ -203,10 +205,9 @@ $("#searchForm").addEventListener("submit", (event) => { event.preventDefault();
|
|||||||
$("#more").addEventListener("click", () => search(false));
|
$("#more").addEventListener("click", () => search(false));
|
||||||
$("#hotPrev").addEventListener("click", () => loadHot(uiState.hotPage - 1));
|
$("#hotPrev").addEventListener("click", () => loadHot(uiState.hotPage - 1));
|
||||||
$("#hotNext").addEventListener("click", () => loadHot(uiState.hotPage + 1));
|
$("#hotNext").addEventListener("click", () => loadHot(uiState.hotPage + 1));
|
||||||
["music", "mic"].forEach((kind) => {
|
// 音乐音量可调; 麦克风音量由功放硬件控制, 不再向设备发送无效果请求。
|
||||||
$("#" + kind + "Volume").addEventListener("input", (event) => { $("#" + kind + "Value").textContent = event.target.value; });
|
$("#musicVolume").addEventListener("input", (event) => { $("#musicValue").textContent = event.target.value; });
|
||||||
$("#" + kind + "Volume").addEventListener("change", (event) => updateAudio({ [kind + "Volume"]: Number(event.target.value) }));
|
$("#musicVolume").addEventListener("change", (event) => updateAudio({ musicVolume: Number(event.target.value) }));
|
||||||
});
|
|
||||||
document.addEventListener("visibilitychange", () => { uiState.visible = !document.hidden; if (uiState.visible) refresh(); });
|
document.addEventListener("visibilitychange", () => { uiState.visible = !document.hidden; if (uiState.visible) refresh(); });
|
||||||
window.setInterval(() => { if (uiState.visible) refresh(); }, 1000);
|
window.setInterval(() => { if (uiState.visible) refresh(); }, 1000);
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="panel-title"><div><span class="tiny-icon">♪</span><b>声音控制</b></div><small>即时生效</small></div>
|
<div class="panel-title"><div><span class="tiny-icon">♪</span><b>声音控制</b></div><small>即时生效</small></div>
|
||||||
<div class="seg"><button id="accompany" data-vocal="accompany"><span>伴</span>伴唱</button><button id="original" data-vocal="original"><span>原</span>原唱</button></div>
|
<div class="seg"><button id="accompany" data-vocal="accompany"><span>伴</span>伴唱</button><button id="original" data-vocal="original"><span>原</span>原唱</button></div>
|
||||||
<label><span><i class="volume-icon">♪</i>音乐音量</span><b><em id="musicValue">70</em>%</b><input id="musicVolume" type="range" min="0" max="100" value="70"></label>
|
<label><span><i class="volume-icon">♪</i>音乐音量</span><b><em id="musicValue">70</em>%</b><input id="musicVolume" type="range" min="0" max="100" value="70"></label>
|
||||||
<label><span><i class="volume-icon mic">●</i>麦克风音量</span><b><em id="micValue">70</em>%</b><input id="micVolume" type="range" min="0" max="100" value="70"></label>
|
<label class="mic-hw"><span><i class="volume-icon mic">●</i>麦克风音量</span><small>由功放/混音器硬件调节</small><input id="micVolume" type="range" min="0" max="100" value="70" disabled></label>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="next-card"><div class="next-number">02</div><div><small>下一首</small><b id="nextSong">队列为空</b></div><button data-page="queue">查看队列 <span>›</span></button></article>
|
<article class="next-card"><div class="next-number">02</div><div><small>下一首</small><b id="nextSong">队列为空</b></div><button data-page="queue">查看队列 <span>›</span></button></article>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.app.AlertDialog
|
|||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.DialogInterface.OnShowListener
|
import android.content.DialogInterface.OnShowListener
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@@ -512,9 +515,72 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
micActivator.start()
|
micActivator.start()
|
||||||
|
registerMicInputWatcher()
|
||||||
Log.i(TAG, "麦克风输入已激活(演唱输出通道占用)")
|
Log.i(TAG, "麦克风输入已激活(演唱输出通道占用)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听输入设备变化: 外接麦克风(USB 声卡/3.5mm 麦)插入时系统会把默认输入路由切到外接,
|
||||||
|
* 原 AudioRecord 随之失效。检测到变化后重建占用, 让演唱输出通道跟随当前麦克风。
|
||||||
|
*/
|
||||||
|
private fun registerMicInputWatcher() {
|
||||||
|
val manager = audioManager ?: return
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
manager.registerAudioDeviceCallback(micDeviceCallback, null)
|
||||||
|
} else {
|
||||||
|
runCatching {
|
||||||
|
val filter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||||
|
registerReceiver(micHotplugReceiver, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterMicInputWatcher() {
|
||||||
|
val manager = audioManager ?: return
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
manager.unregisterAudioDeviceCallback(micDeviceCallback)
|
||||||
|
} else {
|
||||||
|
runCatching { unregisterReceiver(micHotplugReceiver) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val micDeviceCallback = object : android.media.AudioDeviceCallback() {
|
||||||
|
override fun onAudioDevicesAdded(addedDevices: Array<out android.media.AudioDeviceInfo>) {
|
||||||
|
handleMicInputDevicesChanged(addedDevices.any { isMicInputDevice(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudioDevicesRemoved(removedDevices: Array<out android.media.AudioDeviceInfo>) {
|
||||||
|
handleMicInputDevicesChanged(removedDevices.any { isMicInputDevice(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val micHotplugReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
handleMicInputDevicesChanged(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为可用的麦克风输入设备(内置/耳机/USB 等)。 */
|
||||||
|
private fun isMicInputDevice(device: android.media.AudioDeviceInfo): Boolean {
|
||||||
|
if (!device.isSource) return false
|
||||||
|
return when (device.type) {
|
||||||
|
android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC,
|
||||||
|
android.media.AudioDeviceInfo.TYPE_USB_HEADSET,
|
||||||
|
android.media.AudioDeviceInfo.TYPE_USB_DEVICE,
|
||||||
|
android.media.AudioDeviceInfo.TYPE_HDMI,
|
||||||
|
android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
||||||
|
android.media.AudioDeviceInfo.TYPE_LINE_ANALOG,
|
||||||
|
-> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入设备增删后: 重新打开麦克风占用, 跟随系统当前路由(优先外接)。 */
|
||||||
|
private fun handleMicInputDevicesChanged(_changed: Boolean) {
|
||||||
|
// 延迟少许等系统完成路由切换, 再重建 AudioRecord。
|
||||||
|
main.postDelayed({ micActivator.restart() }, 300L)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initializeDatabase() {
|
private fun initializeDatabase() {
|
||||||
if (databaseBootstrapRunning) return
|
if (databaseBootstrapRunning) return
|
||||||
databaseBootstrapRunning = true
|
databaseBootstrapRunning = true
|
||||||
@@ -603,6 +669,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
main.removeCallbacks(remoteNetworkMonitor)
|
main.removeCallbacks(remoteNetworkMonitor)
|
||||||
main.removeCallbacks(qrAutoHideMonitor)
|
main.removeCallbacks(qrAutoHideMonitor)
|
||||||
stopRecording(false)
|
stopRecording(false)
|
||||||
|
unregisterMicInputWatcher()
|
||||||
micActivator.stop()
|
micActivator.stop()
|
||||||
releaseVocalPlayer()
|
releaseVocalPlayer()
|
||||||
if (::playbackEngine.isInitialized) playbackEngine.release()
|
if (::playbackEngine.isInitialized) playbackEngine.release()
|
||||||
@@ -2644,15 +2711,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
})
|
})
|
||||||
root.findViewById<View?>(R.id.btn_mic_down)
|
root.findViewById<View?>(R.id.btn_mic_down)
|
||||||
.setOnClickListener(View.OnClickListener { v: View? ->
|
.setOnClickListener(View.OnClickListener { v: View? ->
|
||||||
micVolume = max(0, micVolume - 5)
|
toast("麦克风音量由功放/混音器硬件调节")
|
||||||
micValue.setText(micVolume.toString())
|
|
||||||
saveState()
|
|
||||||
})
|
})
|
||||||
root.findViewById<View?>(R.id.btn_mic_up)
|
root.findViewById<View?>(R.id.btn_mic_up)
|
||||||
.setOnClickListener(View.OnClickListener { v: View? ->
|
.setOnClickListener(View.OnClickListener { v: View? ->
|
||||||
micVolume = min(100, micVolume + 5)
|
toast("麦克风音量由功放/混音器硬件调节")
|
||||||
micValue.setText(micVolume.toString())
|
|
||||||
saveState()
|
|
||||||
})
|
})
|
||||||
bindAtmospherePreset(root, R.id.btn_preset_pop, "流行")
|
bindAtmospherePreset(root, R.id.btn_preset_pop, "流行")
|
||||||
bindAtmospherePreset(root, R.id.btn_preset_lyric, "抒情")
|
bindAtmospherePreset(root, R.id.btn_preset_lyric, "抒情")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.local.ktv
|
package com.local.ktv
|
||||||
|
|
||||||
|
import android.media.AudioDeviceInfo
|
||||||
import android.media.AudioFormat
|
import android.media.AudioFormat
|
||||||
import android.media.AudioRecord
|
import android.media.AudioRecord
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
@@ -15,6 +16,11 @@ import android.util.Log
|
|||||||
* 做法: 用一个低开销的 [AudioRecord] 常驻打开麦克风并循环读取丢弃数据,
|
* 做法: 用一个低开销的 [AudioRecord] 常驻打开麦克风并循环读取丢弃数据,
|
||||||
* 保持输入通道占用(不写文件、不对外暴露声音)。与 MediaRecorder 录音互斥:
|
* 保持输入通道占用(不写文件、不对外暴露声音)。与 MediaRecorder 录音互斥:
|
||||||
* 开始录音前先 pause, 结束录音后自动 resume。
|
* 开始录音前先 pause, 结束录音后自动 resume。
|
||||||
|
*
|
||||||
|
* 外接麦克风(USB/耳机麦)热插拔时, 系统会把默认输入路由切到外接设备并使
|
||||||
|
* 原 AudioRecord 失效 —— 此时需重建 AudioRecord 并优先占用当前活动输入设备。
|
||||||
|
* 因此对外暴露 [restart] 供设备变化监听调用; [setPreferredInput] 支持 Android 8.0+
|
||||||
|
* 的 AudioRecord.setPreferredDevice 直接把输入锁定到指定麦克风(优先外接)。
|
||||||
*/
|
*/
|
||||||
class MicActivator {
|
class MicActivator {
|
||||||
private val tag = "MicActivator"
|
private val tag = "MicActivator"
|
||||||
@@ -23,6 +29,9 @@ class MicActivator {
|
|||||||
@Volatile private var running = false
|
@Volatile private var running = false
|
||||||
@Volatile private var paused = false
|
@Volatile private var paused = false
|
||||||
|
|
||||||
|
/** 期望优先使用的输入设备; null = 跟随系统默认(自动切到外接)。 */
|
||||||
|
@Volatile private var preferredInput: AudioDeviceInfo? = null
|
||||||
|
|
||||||
val isActive: Boolean get() = running && !paused
|
val isActive: Boolean get() = running && !paused
|
||||||
|
|
||||||
/** 打开麦克风并保持占用。失败静默(设备无麦克风时不影响 App 使用)。 */
|
/** 打开麦克风并保持占用。失败静默(设备无麦克风时不影响 App 使用)。 */
|
||||||
@@ -31,6 +40,28 @@ class MicActivator {
|
|||||||
if (running) return
|
if (running) return
|
||||||
running = true
|
running = true
|
||||||
paused = false
|
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() }
|
thread = Thread({ readLoop() }, "mic-activator").also { it.isDaemon = true; it.start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +81,7 @@ class MicActivator {
|
|||||||
if (!running) return
|
if (!running) return
|
||||||
if (!paused) return
|
if (!paused) return
|
||||||
paused = false
|
paused = false
|
||||||
thread = Thread({ readLoop() }, "mic-activator").also { it.isDaemon = true; it.start() }
|
spawnThread()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 彻底停止(不再自动恢复)。 */
|
/** 彻底停止(不再自动恢复)。 */
|
||||||
@@ -89,6 +120,11 @@ class MicActivator {
|
|||||||
sleepQuietly(5000)
|
sleepQuietly(5000)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Android 8.0+: 指定首选输入设备(如刚插入的外接麦克风), 避免仍占用内建。
|
||||||
|
val target = preferredInput
|
||||||
|
if (target != null && android.os.Build.VERSION.SDK_INT >= 26) {
|
||||||
|
runCatching { recorder.setPreferredDevice(target) }
|
||||||
|
}
|
||||||
record = recorder
|
record = recorder
|
||||||
recorder.startRecording()
|
recorder.startRecording()
|
||||||
val buffer = ByteArray(minBuffer.coerceAtLeast(4096))
|
val buffer = ByteArray(minBuffer.coerceAtLeast(4096))
|
||||||
@@ -125,3 +161,4 @@ class MicActivator {
|
|||||||
try { Thread.sleep(ms) } catch (ignored: InterruptedException) { Thread.currentThread().interrupt() }
|
try { Thread.sleep(ms) } catch (ignored: InterruptedException) { Thread.currentThread().interrupt() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,12 +97,12 @@
|
|||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 麦克风 -->
|
<!-- 麦克风(增益/效果由功放混音器硬件调节, 滑杆仅作指示) -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="2dp"
|
||||||
android:text="麦克风音量"
|
android:text="麦克风音量(功放/混音器硬件调节)"
|
||||||
android:textColor="#FFE0E0FF"
|
android:textColor="#FFE0E0FF"
|
||||||
android:textSize="13sp" />
|
android:textSize="13sp" />
|
||||||
|
|
||||||
@@ -121,7 +121,8 @@
|
|||||||
android:max="100"
|
android:max="100"
|
||||||
android:progress="60"
|
android:progress="60"
|
||||||
android:progressTint="#FF27CCA4"
|
android:progressTint="#FF27CCA4"
|
||||||
android:thumbTint="#FF27CCA4" />
|
android:thumbTint="#FF27CCA4"
|
||||||
|
android:enabled="false" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_mic_volume_value"
|
android:id="@+id/text_mic_volume_value"
|
||||||
@@ -136,8 +137,8 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="2dp"
|
||||||
android:text="麦克风效果"
|
android:text="麦克风效果(功放/混音器硬件调节)"
|
||||||
android:textColor="#FFE0E0FF"
|
android:textColor="#FFE0E0FF"
|
||||||
android:textSize="13sp" />
|
android:textSize="13sp" />
|
||||||
|
|
||||||
@@ -156,7 +157,8 @@
|
|||||||
android:max="100"
|
android:max="100"
|
||||||
android:progress="50"
|
android:progress="50"
|
||||||
android:progressTint="#FF27CCA4"
|
android:progressTint="#FF27CCA4"
|
||||||
android:thumbTint="#FF27CCA4" />
|
android:thumbTint="#FF27CCA4"
|
||||||
|
android:enabled="false" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_mic_effect_value"
|
android:id="@+id/text_mic_effect_value"
|
||||||
|
|||||||
Reference in New Issue
Block a user