diff --git a/android/app/src/main/java/com/tsmobile/app/voice/VoiceService.kt b/android/app/src/main/java/com/tsmobile/app/voice/VoiceService.kt index 068af3e..8aa3662 100644 --- a/android/app/src/main/java/com/tsmobile/app/voice/VoiceService.kt +++ b/android/app/src/main/java/com/tsmobile/app/voice/VoiceService.kt @@ -55,6 +55,7 @@ class VoiceService( private var playbackGeneration = 0L private var activePlaybackTrack: AudioTrack? = null @Volatile private var receiveVolume = 1.0f + @Volatile private var receiveGain = 2.5f private val audioManager = context.getSystemService(AudioManager::class.java) private var audioFocusRequest: AudioFocusRequest? = null @@ -129,6 +130,11 @@ class VoiceService( } } + /** 设置 PCM 预增益(1.0 = 原始音量,2.5 = 默认) */ + fun setReceiveGain(gain: Float) { + receiveGain = gain.coerceIn(0.5f, 5.0f) + } + fun enableReceivePlayback() { if (speakerEnabled) forceLoudspeaker() } @@ -176,10 +182,12 @@ class VoiceService( synchronized(playbackLock) { activePlaybackTrack = track } } val activeTrack = track ?: continue + // 应用 PCM 预增益 + val gainedFrame = applyPcmGain(frame, receiveGain) if (acceptedFrames <= 5 || acceptedFrames % 250L == 0L) { - Log.i(TAG, "Writing mixed PCM bytes=${frame.size} trackState=${activeTrack.state} playState=${activeTrack.playState}") + Log.i(TAG, "Writing mixed PCM bytes=${gainedFrame.size} gain=$receiveGain trackState=${activeTrack.state}") } - val written = activeTrack.write(frame, 0, frame.size, AudioTrack.WRITE_BLOCKING) + val written = activeTrack.write(gainedFrame, 0, gainedFrame.size, AudioTrack.WRITE_BLOCKING) if (written != frame.size) { trackWriteFailures++ Log.w(TAG, "AudioTrack.write incomplete: $written/${frame.size}") @@ -341,4 +349,21 @@ class VoiceService( selfSpeakingJob?.cancel(); selfSpeakingJob = null Log.d(TAG, "VoiceService destroyed") } + + /** + * 对 PCM16 字节数组应用增益,clamp 到 int16 范围防止溢出。 + * PCM 格式:小端序 interleaved stereo。 + */ + private fun applyPcmGain(pcm: ByteArray, gain: Float): ByteArray { + if (gain == 1.0f) return pcm + val result = pcm.copyOf() + for (i in result.indices step 2) { + // 小端序:低字节在前 + val sample = ((result[i + 1].toInt() shl 8) or (result[i].toInt() and 0xFF)).toShort() + val gained = (sample.toFloat() * gain).toInt().coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()) + result[i] = (gained and 0xFF).toByte() + result[i + 1] = (gained shr 8).toByte() + } + return result + } }