Android 编写喇叭老化测试程序
公司最近研发新产品需要一个老化测试喇叭的Demo,但是本人对音频这一块并没有关注,于是有了这篇吃百家饭的总结,便于以后自己查找。
需求是可以选择音频频率的喇叭老化测试程序,网上找了一圈有没有直接用的软件结果不是下架就是下架。索性自己写一个简易的测试程序好了。
首先编写一个简易的布局代码,便于梳理需要实现的功能,如下图所示:

从布局代码中就能知道需要实现的内容,实际就只有三个,1.生成指定的音频的音频并循环播放。2.控制音量大小。3.计时器。
接下来开始实现:
需要用到android.permission.RECORD_AUDIO权限一定要在配置文件中申请。
1. 生成指定音频
因为只是单纯用来进行老化测试所以对音频没有要求也不需要保存音频资源,所以我打算直接生成正弦波音频,代码如下:
1 /** 2 * 生成正弦音频 3 * @param frequency 频率 4 * return 音频数据数组 5 */ 6 fun generateSineWave(frequency: Int): ShortArray { 7 val duration = 1000*10//音频时长 8 val sampleRate = 44100//采样率 9 val amplitude = 0.5//振幅 10 val numSamples = (sampleRate * duration/1000)//采样点数 11 val buffer = ShortArray(numSamples)//音频数据 12 13 //生成正弦音频 14 for (i in 0 until numSamples) { 15 val angle = 2.0 * PI * frequency * i / sampleRate//计算角度 16 buffer[i] = (sin(angle) * amplitude * Short.MAX_VALUE).toInt().toShort()//将角度转换为音频数据 17 } 18 return buffer 19 }
2. 播放音频
Android原生播放音频最常见的有MediaPlayer、AudioTrack、SoundPool等技术,得到的音频数据是一个Short类型的数组因为只需要进行循环播放并没有其他复杂的功能,所以可以直接使用AudioTrack直接播放,如果使用MediaPlayer播放需要将音频数据转换成临时文件,需要多一个步骤。所以我这边直接使用AudioTrack进行音频的播放。代码如下:
1 /** 2 * 循环播放音频 3 * @param audioData 音频数据 4 * @param sampleRate 采样率 5 */
6 fun loopPlayAudio(audioData: ShortArray, sampleRate: Int = 44100) { 7 val audioTrack = AudioTrack( 8 AudioAttributes.Builder() 9 .setUsage(AudioAttributes.USAGE_MEDIA)//设置音频用途 10 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)//设置音频内容类型 11 .build(), 12 AudioFormat.Builder() 13 .setEncoding(AudioFormat.ENCODING_PCM_16BIT)//设置音频编码格式 14 .setSampleRate(sampleRate)//设置采样率 15 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)//设置声道数 16 .build(), 17 audioData.size * 2,//设置音频数据大小 18 AudioTrack.MODE_STATIC,//设置播放模式 19 0)//创建AudioTrack对象 20 audioTrack.write(audioData, 0, audioData.size)//写入音频数据 21 audioTrack.setLoopPoints(0, audioData.size - 1, -1)//设置循环播放 22 audioTrack.play()//播放音频 23 24 loopingAudioTrack = audioTrack//保存AudioTrack对象 25 }
由于需要循环播放所以使用loopingAudioTrack变量保存AudioTrack的引用便于后续对音频的操作。
有播放音频自然需要停止播放,关闭时一定要及时释放音频资源,防止引发其他问题。代码如下:
1 /** 2 * 停止播放 3 */ 4 fun stopPlaying() { 5 loopingAudioTrack?.apply { 6 if (this.playState == AudioTrack.PLAYSTATE_PLAYING) { 7 stop()//停止播放 8 } 9 release()//释放资源 10 Log.d(TAG, "释放资源") 11 } 12 loopingAudioTrack = null//置空 13 }
3. 控制音量
我这边采用的单例模式去获取到AudioManager对象,代码如下:
1 companion object{ 2 @Volatile 3 private var instance: AudioManagerHelper? = null 4 fun getInstance(context: Context): AudioManagerHelper {//获取单例 5 return instance ?: synchronized(this) { 6 instance ?: AudioManagerHelper(context).also { 7 instance = it 8 } 9 } 10 } 11 } 12 //音频管理器 13 private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager 14 15 16 /** 17 * 设置音量 18 * @param volume 音量 19 */ 20 fun setVolume(volume: Int) = audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0) 21 22 /** 23 * 获取最大音量 24 */ 25 fun getMaxVolume() = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) 26 27 /** 28 * 获取当前音量 29 */ 30 fun getCurrentVolume() = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
代码非常简单,通过Context获取到AudioManager对象就可以控制音量,这里由于只要对喇叭进行老化测试所以控制的是音乐流,如果需要控制其他的流类型只需要将AudioManager.STREAM_MUSIC常量更换成需要控制的流即可。这里贴出常见的一些流,需要的可以自己查看:
1 AudioManager.STREAM_MUSIC - 音乐流,用于大多数音频播放 2 AudioManager.STREAM_VOICE_CALL - 通话流,用于电话通话 3 AudioManager.STREAM_SYSTEM - 系统流,用于系统提示音 4 AudioManager.STREAM_RING - 铃声流,用于来电铃声 5 AudioManager.STREAM_ALARM - 闹钟流,用于闹钟声音 6 AudioManager.STREAM_NOTIFICATION - 通知流,用于通知提示音 7 AudioManager.STREAM_BLUETOOTH_SCO - 蓝牙SCO流,用于蓝牙设备通话 8 AudioManager.STREAM_DTMF - DTMF流,用于拨号键盘音 9 AudioManager.STREAM_ACCESSIBILITY - 辅助功能流,用于无障碍服务
4.计时器
由于老化测试一般都是按小时计算,需要长时间的定时任务,所以我用的是Android自带的Chronometer控件,这个空间可以很轻松的实现计时功能,不需要太多的代码也不用考虑线程的问题。先在xml文件中加入该控件:
1 <Chronometer 2 android:id="@+id/chronometer" 3 android:layout_width="match_parent" 4 android:layout_height="wrap_content" 5 android:textSize="18sp"/>
xml部分非常简单,还有些其他的常见属性在这边贴出来以便后续便于查找:
android:countDown - 是否倒计时,默认false。
android:format - 设置显示时间格式。如果指定,第一个 「"%s"」 替换为"MM:SS"或"H:MM:SS"形式的当前计时器值。
常用的方法如下:
start():开始计时 stop():停止计时 setBase(long):设置计时器起始时间。 setFormat(String):设置显示时间格式 setCountDown(boolean):设置是否是倒计时(SDK版本大于23)。 setOnChronometerTickListener(OnChronometerTickListener):为计时器绑定事件监听,当计时器改变时触发该监听器。
由于Chronometer控件默认的计时格式并不是我想要的所以改变了一下显示时间格式,具体代码如下:
1 /** 2 * 格式化时间 3 * @param time 时间 4 * @return 格式化后的时间 5 */ 6 private fun formatTime(time: Long): String { 7 val totalSeconds = time / 1000//总秒数 8 val hours = (totalSeconds / 3600).toInt()//小时数 9 val minutes = ((totalSeconds % 3600) / 60).toInt()//分钟数 10 val seconds = (totalSeconds % 60).toInt()//秒数 11 return String.format("%02d:%02d:%02d", hours, minutes, seconds) 12 } 13 14 binding.chronometer.setOnChronometerTickListener { chronometer -> 15 val elapsedTime = SystemClock.elapsedRealtime() - chronometer.base//计时器当前时间 16 val formattedTime = formatTime(elapsedTime)//格式化时间 17 chronometer.text = formattedTime//更新UI 18 }
接下来就是启动计时器即可:
binding.chronometer.base = SystemClock.elapsedRealtime()//设置计时器 binding.chronometer.start()//开始计时
计时完毕之后也别忘了调用stop()停止计时。最后附上完整代码:
xml
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:tools="http://schemas.android.com/tools" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:orientation="vertical" 8 android:padding="20dp" 9 android:gravity="center_vertical" 10 tools:context=".MainActivity"> 11 12 <!--频率显示--> 13 <LinearLayout 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:gravity="center_horizontal" 17 android:orientation="horizontal"> 18 <TextView 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" 21 android:textSize="18sp" 22 android:text="当前频率:"/> 23 <TextView 24 android:id="@+id/frequencyTV" 25 android:layout_width="wrap_content" 26 android:layout_height="wrap_content" 27 android:textSize="18sp" 28 android:text="100Hz"/> 29 </LinearLayout> 30 31 <!--频率按钮选择--> 32 <LinearLayout 33 android:layout_width="match_parent" 34 android:layout_height="wrap_content" 35 android:gravity="center_horizontal" 36 android:orientation="horizontal"> 37 <Button 38 android:id="@+id/frequency100Btn" 39 android:layout_width="wrap_content" 40 android:layout_height="wrap_content" 41 android:text="100Hz"/> 42 <Button 43 android:id="@+id/frequency500Btn" 44 android:layout_width="wrap_content" 45 android:layout_height="wrap_content" 46 android:text="500Hz"/> 47 <Button 48 android:id="@+id/frequency1000Btn" 49 android:layout_width="wrap_content" 50 android:layout_height="wrap_content" 51 android:text="1000Hz"/> 52 </LinearLayout> 53 54 <!--音量选择--> 55 <LinearLayout 56 android:layout_width="match_parent" 57 android:layout_height="wrap_content" 58 android:gravity="center_horizontal" 59 android:orientation="horizontal"> 60 <TextView 61 android:layout_width="wrap_content" 62 android:layout_height="wrap_content" 63 android:text="音量:"/> 64 <SeekBar 65 android:id="@+id/volumeSeekBar" 66 android:layout_width="match_parent" 67 android:layout_height="wrap_content" 68 /> 69 </LinearLayout> 70 71 <!--测试时间--> 72 <LinearLayout 73 android:layout_width="match_parent" 74 android:layout_height="wrap_content" 75 android:gravity="center_horizontal" 76 android:orientation="horizontal"> 77 <TextView 78 android:layout_width="wrap_content" 79 android:layout_height="wrap_content" 80 android:text="测试时长:"/> 81 <EditText 82 android:id="@+id/timeET" 83 android:layout_width="wrap_content" 84 android:layout_height="wrap_content" 85 android:hint="小时" 86 android:inputType="number" 87 android:maxLength="2" 88 android:text=""/> 89 </LinearLayout> 90 91 <Button 92 android:id="@+id/startBtn" 93 android:layout_width="match_parent" 94 android:layout_height="wrap_content" 95 android:text="开始测试"/> 96 97 <!--计时--> 98 <LinearLayout 99 android:layout_width="match_parent" 100 android:layout_height="wrap_content" 101 android:orientation="horizontal"> 102 <TextView 103 android:layout_width="wrap_content" 104 android:layout_height="wrap_content" 105 android:textSize="18sp" 106 android:text="计时:"/> 107 <Chronometer 108 android:id="@+id/chronometer" 109 android:layout_width="match_parent" 110 android:layout_height="wrap_content" 111 android:textSize="18sp"/> 112 </LinearLayout> 113 114 </LinearLayout>
kotlin部分:
MainActivity:
1 import androidx.appcompat.app.AppCompatActivity 2 import android.os.Bundle 3 import android.os.SystemClock 4 import android.util.Log 5 import android.view.View 6 import android.widget.SeekBar 7 import android.widget.Toast 8 import com.example.speakeragingtest.databinding.ActivityMainBinding 9 import kotlin.concurrent.thread 10 11 class MainActivity : AppCompatActivity() , View.OnClickListener { 12 13 private val TAG = javaClass.simpleName 14 private lateinit var binding: ActivityMainBinding 15 private var isPlaying = false//是否播放中 16 private val audioManagerHelper by lazy { 17 AudioManagerHelper.getInstance(this) 18 }//音频管理器帮助类 19 20 override fun onCreate(savedInstanceState: Bundle?) { 21 super.onCreate(savedInstanceState) 22 binding = ActivityMainBinding.inflate(layoutInflater) 23 setContentView(binding.root) 24 25 initView()//初始化视图 26 } 27 28 /** 29 * 初始化视图 30 */ 31 private fun initView() { 32 //绑定点击事件 33 binding.frequency100Btn.setOnClickListener(this) 34 binding.frequency500Btn.setOnClickListener(this) 35 binding.frequency1000Btn.setOnClickListener(this) 36 binding.startBtn.setOnClickListener(this) 37 38 //控制音量 39 binding.volumeSeekBar.max = audioManagerHelper.getMaxVolume()//设置进度条最大值 40 Log.d(TAG, "最大音量值 = ${binding.volumeSeekBar.max}") 41 binding.volumeSeekBar.progress = audioManagerHelper.getCurrentVolume()//设置进度条为当前音量 42 Log.d(TAG, "当前音量 = ${binding.volumeSeekBar.progress}") 43 //拖动音量 44 binding.volumeSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 45 override fun onProgressChanged(seekBar: SeekBar?, p1: Int, p2: Boolean) { 46 Log.d(TAG, "拖动前音量值 = $p1") 47 } 48 49 /** 50 * 开始拖动 51 */ 52 override fun onStartTrackingTouch(seekBar: SeekBar?) { 53 Log.d(TAG, "拖动中音量值 = ${seekBar?.progress}") 54 } 55 56 /** 57 * 停止拖动 58 */ 59 override fun onStopTrackingTouch(seekBar: SeekBar?) { 60 audioManagerHelper.setVolume(seekBar?.progress!!)//设置音量 61 Log.d(TAG, "拖动后音量值 = ${seekBar.progress}") 62 } 63 64 }) 65 66 //监控计时 67 binding.chronometer.setOnChronometerTickListener { chronometer -> 68 val elapsedTime = SystemClock.elapsedRealtime() - chronometer.base//计时器当前时间 69 val formattedTime = formatTime(elapsedTime)//格式化时间 70 chronometer.text = formattedTime//更新UI 71 72 //查看是否达到设定时间 73 val setTime = binding.timeET.text.toString().toInt() 74 if (elapsedTime/(1000 * 60 * 60) >= setTime) { 75 chronometer.stop()//停止计时 76 audioManagerHelper.stopPlaying()//停止播放 77 binding.startBtn.text = "开始测试"//更新UI 78 isPlaying = false 79 } 80 } 81 82 } 83 84 /** 85 * 格式化时间 86 * @param time 时间 87 * @return 格式化后的时间 88 */ 89 private fun formatTime(time: Long): String { 90 val totalSeconds = time / 1000//总秒数 91 val hours = (totalSeconds / 3600).toInt()//小时数 92 val minutes = ((totalSeconds % 3600) / 60).toInt()//分钟数 93 val seconds = (totalSeconds % 60).toInt()//秒数 94 return String.format("%02d:%02d:%02d", hours, minutes, seconds) 95 } 96 97 override fun onClick(view: View?) { 98 when ( view?.id) {//点击事件 99 R.id.frequency100Btn -> binding.frequencyTV.text = "100Hz"//更新UI 100 R.id.frequency500Btn -> binding.frequencyTV.text = "500Hz"//更新UI 101 R.id.frequency1000Btn -> binding.frequencyTV.text = "1000Hz"//更新UI 102 R.id.startBtn -> {//点击开始按钮 103 if (isPlaying) {//播放中 104 binding.startBtn.text = "开始测试"//更新UI 105 binding.chronometer.stop()//停止计时 106 audioManagerHelper.stopPlaying()//停止播放 107 isPlaying = false 108 } else {//未播放 109 //验证是否输入时间 110 if (binding.timeET.text.toString().isEmpty()) { 111 Toast.makeText(this, "请输入测试时间", Toast.LENGTH_SHORT).show()//提示输入时间 112 return 113 } 114 binding.startBtn.text = "停止测试"//更新UI 115 binding.chronometer.base = SystemClock.elapsedRealtime()//设置计时器 116 binding.chronometer.start()//开始计时 117 val frequency = when (binding.frequencyTV.text) {//获取频率 118 "100Hz" -> 100 119 "500Hz" -> 500 120 "1000Hz" -> 1000 121 else -> 100 122 } 123 Log.d(TAG, "当前频率 = $frequency Hz") 124 val audioData = audioManagerHelper.generateSineWave(frequency)//生成正弦音频 125 thread { audioManagerHelper.loopPlayAudio(audioData)}//循环播放音频 126 isPlaying = true 127 } 128 } 129 } 130 } 131 132 override fun onDestroy() { 133 super.onDestroy() 134 audioManagerHelper.stopPlaying()//停止播放 135 } 136 }
AudioManagerHelper:
1 import android.content.Context 2 import android.media.AudioAttributes 3 import android.media.AudioFormat 4 import android.media.AudioManager 5 import android.media.AudioTrack 6 import android.util.Log 7 import kotlin.math.PI 8 import kotlin.math.sin 9 10 /** 11 * 音频管理器帮助类 12 */ 13 class AudioManagerHelper private constructor( context: Context) { 14 private val TAG = javaClass.simpleName 15 private var loopingAudioTrack: AudioTrack ?= null 16 companion object{//静态内部类 17 @Volatile 18 private var instance: AudioManagerHelper? = null 19 fun getInstance(context: Context): AudioManagerHelper {//获取单例 20 return instance ?: synchronized(this) { 21 instance ?: AudioManagerHelper(context).also { 22 instance = it 23 } 24 } 25 } 26 } 27 //音频管理器 28 private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager 29 30 31 /** 32 * 设置音量 33 * @param volume 音量 34 */ 35 fun setVolume(volume: Int) = audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0) 36 37 /** 38 * 获取最大音量 39 */ 40 fun getMaxVolume() = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) 41 42 /** 43 * 获取当前音量 44 */ 45 fun getCurrentVolume() = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) 46 47 /** 48 * 生成正弦音频 49 * @param frequency 频率 50 * return 音频数据数组 51 */ 52 fun generateSineWave(frequency: Int): ShortArray { 53 val duration = 1000*10//音频时长 54 val sampleRate = 44100//采样率 55 val amplitude = 0.5//振幅 56 val numSamples = (sampleRate * duration/1000)//采样点数 57 val buffer = ShortArray(numSamples)//音频数据 58 59 //生成正弦音频 60 for (i in 0 until numSamples) { 61 val angle = 2.0 * PI * frequency * i / sampleRate//计算角度 62 buffer[i] = (sin(angle) * amplitude * Short.MAX_VALUE).toInt().toShort()//将角度转换为音频数据 63 } 64 return buffer 65 } 66 67 /** 68 * 循环播放音频 69 * @param audioData 音频数据 70 * @param sampleRate 采样率 71 */ 72 fun loopPlayAudio(audioData: ShortArray, sampleRate: Int = 44100) { 73 val audioTrack = AudioTrack( 74 AudioAttributes.Builder() 75 .setUsage(AudioAttributes.USAGE_MEDIA)//设置音频用途 76 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)//设置音频内容类型 77 .build(), 78 AudioFormat.Builder() 79 .setEncoding(AudioFormat.ENCODING_PCM_16BIT)//设置音频编码格式 80 .setSampleRate(sampleRate)//设置采样率 81 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)//设置声道数 82 .build(), 83 audioData.size * 2,//设置音频数据大小 84 AudioTrack.MODE_STATIC,//设置播放模式 85 0)//创建AudioTrack对象 86 audioTrack.write(audioData, 0, audioData.size)//写入音频数据 87 audioTrack.setLoopPoints(0, audioData.size - 1, -1)//设置循环播放 88 audioTrack.play()//播放音频 89 90 loopingAudioTrack = audioTrack//保存AudioTrack对象 91 } 92 93 /** 94 * 停止播放 95 */ 96 fun stopPlaying() { 97 loopingAudioTrack?.apply { 98 if (this.playState == AudioTrack.PLAYSTATE_PLAYING) { 99 stop()//停止播放 100 } 101 release()//释放资源 102 Log.d(TAG, "释放资源") 103 } 104 loopingAudioTrack = null//置空 105 } 106 }

浙公网安备 33010602011771号