完整示例代码

https://gitee.com/daizhufei/record-demo-with-noise

录音

class AudioRecorder {

    private var record = false

    private fun printLog(message: String) = Log.i(AudioRecorder::class.java.simpleName, message)

    @SuppressLint("MissingPermission")
    fun startRecord() {
        if (record) return
        val audioSource = MediaRecorder.AudioSource.MIC
        val sampleRateInHz = 44100
        val channelConfig = AudioFormat.CHANNEL_IN_MONO
        val audioFormat = AudioFormat.ENCODING_PCM_16BIT
        val minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
        val audioRecord = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, minBufferSize)
        if (audioRecord.state != AudioRecord.STATE_INITIALIZED) {
            printLog("录音失败:初始化AudioRecord失败")
            return
        }
        thread { startRecord(audioRecord, minBufferSize) }
    }

    fun stopRecord() {
        record = false
    }

    private fun startRecord(audioRecord: AudioRecord, minBufferSize: Int) {
        printLog("开始录音")
        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
        audioRecord.startRecording()
        val byteBuffer = ByteBuffer.allocateDirect(minBufferSize)
        val fileDir = Environment.getExternalStorageDirectory()
        val fileName = "${DateFormat.format("yyyy_MM_dd_HHmmss", System.currentTimeMillis())}.pcm"
        val file = File(fileDir, fileName)
        val fos = FileOutputStream(file)
        val channel = fos.channel
        record = true
        while (record && audioRecord.read(byteBuffer, minBufferSize) > 0) {
            channel.write(byteBuffer)
            byteBuffer.clear()
        }
        record = false
        audioRecord.stop()
        audioRecord.release()
        channel.close()
        fos.close()
        printLog("录音结束,文件:${file.absoluteFile}")
    }

}

播放PCM音频

使用AudioTrack进行音频播放,播放的是PCM流,示例代码如下:

const val STREAM_TYPE = AudioManager.STREAM_VOICE_CALL

class PcmPlayer(private val context: Context) {
   
    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private var play = false

    private fun printLog(message: String) = Log.i(NoisePlayer::class.java.simpleName, message)

    fun startPlay() {
        if (play) return

		if (STREAM_TYPE == AudioManager.STREAM_VOICE_CALL) {
            // 通话类型才有扬声器开或关的说法,音乐类型的效果相当于扬声器开,且无法关,且也无需要设置
            audioManager.isSpeakerphoneOn = true 
        }
        
        val maxVolume = audioManager.getStreamMaxVolume(STREAM_TYPE)
        audioManager.setStreamVolume(STREAM_TYPE, maxVolume, 0)

        val sampleRateInHz = 48000        
        val minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT)

        val audioTrack = AudioTrack(STREAM_TYPE, sampleRateInHz, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM)

        if (audioTrack.state != AudioTrack.STATE_INITIALIZED) {
            printLog("播放杂音失败:初始化AudioTrack失败")
            return
        }
        thread { startPlay(audioTrack) }
    }

    fun stopPlay() {
        play = false
    }

    private fun startPlay(audioTrack: AudioTrack) {
        printLog("开始播放杂音")        
        play = true
        Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)
        audioTrack.play()

        context.assets.open("baiyang.pcm").use { input ->
            val buf = ByteArray(1024)
            var len: Int
            while (input.read(buf).also { len = it } > 0 && play) {
                audioTrack.write(buf, 0, len)
            }
        }
        audioTrack.stop()
        audioTrack.release()
        printLog("播放结束")
    }
}

上面所用的AudioTrack的构造函数是过时的,新的方式如下:

const val STREAM_TYPE = AudioManager.STREAM_MUSIC

class PcmPlayer(private val context: Context) {

    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private var play = false

    private fun printLog(message: String) = Log.i(PcmPlayer::class.java.simpleName, message)

    fun startPlay() {
        if (play) return

        val isVoiceCall = STREAM_TYPE == AudioManager.STREAM_VOICE_CALL
        if (isVoiceCall) {
            // 通话类型才有扬声器开或关的说法,音乐类型的效果相当于扬声器开,且无法关,且也无需要设置
            audioManager.isSpeakerphoneOn = true
        }

        val maxVolume = audioManager.getStreamMaxVolume(STREAM_TYPE)
        audioManager.setStreamVolume(STREAM_TYPE, maxVolume, 0)

        val sampleRateInHz = 48000

        val attributes = AudioAttributes.Builder()
            .setUsage(if (isVoiceCall) AudioAttributes.USAGE_VOICE_COMMUNICATION else AudioAttributes.USAGE_MEDIA)
            .setContentType(if (isVoiceCall) AudioAttributes.CONTENT_TYPE_SPEECH else AudioAttributes.CONTENT_TYPE_MUSIC)
            .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
            .build()

        val format = AudioFormat.Builder()
            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
            .setSampleRate(sampleRateInHz)
            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
            .build()

        val sessionId = audioManager.generateAudioSessionId()
        val minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT)

        val audioTrack = AudioTrack(attributes, format, minBufferSize, AudioTrack.MODE_STREAM, sessionId)

        if (audioTrack.state != AudioTrack.STATE_INITIALIZED) {
            printLog("播放杂音失败:初始化AudioTrack失败")
            return
        }
        thread { startPlay(audioTrack) }
    }

    fun stopPlay() {
        play = false
    }

    private fun startPlay(audioTrack: AudioTrack) {
        printLog("开始播放杂音")
        Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)
        audioTrack.play()
        play = true

        context.assets.open("baiyang.pcm").use { input ->
            val buf = ByteArray(1024)
            var len: Int
            while (input.read(buf).also { len = it } > 0 && play) {
                audioTrack.write(buf, 0, len)
            }
        }
        audioTrack.stop()
        audioTrack.release()
        printLog("播放结束")
    }
}

调用

val pcmPlayer = PcmPlayer(this)
pcmPlayer.startPlay()
pcmPlayer.stopPlay()

这只是一个简单的Demo,真实使用时,需要把采样率、声音类型、要播放的pcm文件名等一些参数通过startPlay()方法参数传进来,这样才会比较通用。

需要注意一些点:

  • 上面的示例代码中,我们使用的声音类型为:AudioManager.STREAM_VOICE_CALL,当播放的时候,我们按手机上的音量加减,发现无法修改声音大小,因为默认音量大小调整的是AudioManager.STREAM_MUSIC类型的声音。在Activvity中,通过调用API来告诉系统当按音量加减时要调整的声音类型为通话类型:volumeControlStream = AudioManager.STREAM_VOICE_CALL,如果是音乐类型可以不设置这个,因为默认就是调整音乐类型的,但是规范一点即便是音乐类型我们也应该设置一下。

  • AudioManager.STREAM_MUSIC类型的声音在调小的时候,可以调到很小很小,直到完全静音,AudioManager.STREAM_VOICE_CALL类型的声音则无法调到静音,因为是通话类型,打电话的时候静音不太合理,只能是调小到一个相对较小的值。这两种类型的声音大小范围,可以通过函数获取:

    val musicMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
    val musicMinVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
    val voiceCallMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL)
    val voiceCallMinVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
    

    注:获取最小音量值的api是Android9.0才出的,使用时需要注意。

    在公司一台测试手机上运行,结果如下:

    musicMaxVolume: 15
    musicMinVolume: 0
    voiceCallMaxVolume: 7
    voiceCallMinVolume: 1
    

    所以,音乐类型的声音大小可以调到为0,即静音,而通话类型最小只能调到1,无法静音。音乐类型的声音大小范围为0 ~ 15,而通话类型为1 ~ 7,所以音乐类型的声音大小在调整时可以比较细腻,过渡比较平滑,比如从小最按到最大需要按15次音量加,而通话类型的话,从最小按到最大只需要按6次,所以它的过渡是没这么平滑的,所以相当的音量值,它们代表的声音大小是不一样的,这也是为什么通话类型的音量大小1比音乐类型大小1的声音要大声的原因。

  • 扬声器开或关:audioManager.isSpeakerphoneOn = true/false,调用这个设置需要权限:MODIFY_AUDIO_SETTINGS,如果不给权限,设置时它不会报错,就只是不生效而已,通话类型的声音进行播放时默认是关扬声器的效果。而音乐类型的声音在播放时是开扬声器的效果,而且它没有关扬声器的效果,也就是说,通话类型才有开关扬声器的说法,音乐类型就只能用扬声器,没有关扬声器的说法,你也没听说过有人用听筒来听歌的吧,听歌肯定都是用扬声器来听的了!

  • 关于音量最大化,如果是通话类型,有听筒音量和扬声器音量,默认扬声器是关的,所以此时设置音最最大化是设置的听筒的音量,打开扬声器后再设置音量最大化时才是设置的扬声器的音量。如果希望把听筒和扬声器的音量都设置为最大化,则在打开扬声器的前后都需要设置一次音量最大化。如果只需要扬声器最大化,则必须是在打开扬声器后再调用设置音量最大化的代码。有时候我们发现打开扬声器了,为什么音量没有最大化,是因为你设置最大化音量的代码在打开扬声器之前。

测试回声消除

回音含义:在两部手机进行通话时,假设是A和B两个人进行通话,A说话内容会发送到B那边并播放出来,播放出来后又会被B录进去,录进去后又发送到A那里,所以此时A能听到自己刚刚说的话,也就是A听到了自己的回声,这样的效果就不好了,所以需要把这个回声消除掉。

许多设备在硬件上就有回声消除的功能,如何测试呢?比较简单,无非就是一边播放一边录音嘛,所以,我们可以使用AudioTrack来播放一个PCM文件,然后同时用AudioRecord进行录制,录制的时候我们就数数,比如数1~10,然后停止录制,然后使用播放软件来播放刚刚录制的pcm,如果发现播放时只有自己说的1~10的声音,则证明麦克风硬件本身有回声消除功能,如果除了1~10的声音还有AudioTrack播放的声音,则说明硬件本身没有回声消除功能,此时还要听音质,如果能很清楚的听到自己说话的声音和AudioTrack播放的声音,则这样的音频丢给一些回声消除库进行回声消除时应该是可以消的比较干净的,还有一种情况,如果AudioTrack播放pcm时,如果音量非常大声,则此时录音的话会发现,自己的声音被AudioTrack播放的声音完全覆盖了,也就是说录出来的声音在播放时只有AudioTrack播放的声音了,你说话的声音完全没有了,这种情况下你把录到的声音丢到回声消除库做处理,结果就是回声是消掉了,但是你的声音也完全没有了,结果就是那段时间完全没声音,使用效果就是你和你朋友卡卡说了一大堆,结果他一个字也听不见,当然,要出现这种效果是在大家同时说话的情况下,这样才会出现一边播放声音一边录音的效果。如果同一时间只有一方说话,这是不会有问题的,因为录到的声音肯定就只有一方的说话声音,做回声消除时是不会有问题的。对于播放的声音比自己说话声音还大而导致自己的声音被覆盖的问题,只能是调小播放的声音了,或者关闭扬声器用听筒来听。但是有些情况是必须只能用扬声器,比如在吵杂的环境,如果用听筒会觉得声音不够大,完全听不清说话的内容,所以需要外放,然后如果外放声音太大又不行,就只能适当调小外放的音量。所以我们在测试一个录音设备在处理这种情况时录音的音质如何,可以用外放,并同时录音,且需要把外放音量调到适当的大小,不能太大了,也不能太小了,太小了就跟用听筒来听没什么区别了,所以需要调到一个适当的大小,如果在这种情况下录到的音频,发现回声消除很干净,说明麦克风硬件本身有回声消除,且不错!如果录到的音频能清楚听到自己说话的声音和播放的声音,两路声音都能听到很清楚,这表明麦克风的录音效果也很好,此时只需要做一个软件消除回声即可。如果录到的声音即听不清自己话说也听不清播放的声音,则你再把这个声音做回声消除,肯定也消不好,因为原始音频就已经很差了,回声消除肯定也无法把音频变清晰的!在真实开发中,有时候我们不知道是麦克风录音效果差,还是我们的软件回声消除没做好,此时就可以边播放边录,然后听录到的pcm的音质即可做出判断了!

有了这些知识,我们就可以知道:一般在开扬声器的时候才会有回音消除的问题,如果是关扬声器的话一般是不会有问题的,所以在打电话的时候或用一些音频通话软件的时候,如果发现有声音,但是听不清,可以尝试关闭扬声器,用听筒来听音质会好很多,或者确实需要外放的,可以尝试调小外放音量。

在我的真实开发中,服务器会有发送一个很小的白噪音,但是公司的一些执法仪设备在播放这些白噪音并录制时,发现录出来的声音音质就已经很差了,所以可想而知,这样的执法仪设备用来做音频通话效果肯定是不行的,对于这种播放一个很小的杂音和同时录音也在我的Demo中了:https://gitee.com/daizhufei/record-demo-with-noise

04-26 19:30