Android 编写喇叭老化测试程序

公司最近研发新产品需要一个老化测试喇叭的Demo,但是本人对音频这一块并没有关注,于是有了这篇吃百家饭的总结,便于以后自己查找。

需求是可以选择音频频率的喇叭老化测试程序,网上找了一圈有没有直接用的软件结果不是下架就是下架。索性自己写一个简易的测试程序好了。

首先编写一个简易的布局代码,便于梳理需要实现的功能,如下图所示:

image

 从布局代码中就能知道需要实现的内容,实际就只有三个,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 }

 

posted @ 2026-01-15 16:55  是小杰哦  阅读(0)  评论(0)    收藏  举报