外貿(mào)網(wǎng)站制作百度純凈版首頁入口
? ? ? ? Visualizer 是使應(yīng)用程序能夠檢索當(dāng)前播放音頻的一部分以進(jìn)行可視化。它不是錄音接口,僅返回部分低質(zhì)量的音頻內(nèi)容。但是,為了保護(hù)某些音頻數(shù)據(jù)的隱私,使用 Visualizer 需要 android.permission.RECORD_AUDIO權(quán)限。傳遞給構(gòu)造函數(shù)的音頻會(huì)話 ID 指示應(yīng)可視化哪些音頻內(nèi)容:
- 如果會(huì)話為 0,則音頻輸出混合可視化
- 如果會(huì)話不為 0,則顯示來自特定會(huì)話
android.media.MediaPlayer
或使用此音頻會(huì)話的音頻android.media.AudioTrack
可以捕獲兩種類型的音頻內(nèi)容表現(xiàn)形式:
- 波形數(shù)據(jù):使用該
getWaveForm(byte[])
方法連續(xù)的8位(無符號)單聲道樣本 - 頻率數(shù)據(jù):采用8位幅度FFT
getFft(byte[])
方法
捕獲的長度可以通過分別調(diào)用getCaptureSize()
和setCaptureSize(int)
方法來檢索或指定。捕獲大小必須是返回范圍內(nèi)的 2 的冪getCaptureSizeRange()
。
1. 權(quán)限請求
需要在Manifest里面添加
<uses-permission android:name="android.permission.RECORD_AUDIO" />
RECORD_AUDIOAndroid 6.0后需要?jiǎng)討B(tài)請求權(quán)限
val audioPermission =ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)if (audioPermission != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(this,permissions,PERMISSION_REQUEST_CODE /* your request code */)}
2. 初始化Visualizer
首先初始化MediaPlayer,初始MediaPlayer是為了拿到audioSessionId
MusicPlayHelper.init(application, object : IMusicPlayListener {override fun loadMusicFinish(boolean: Boolean, position: Int) {MusicPlayHelper.play()}})
初始化Visualizer,musicId就是上面的audioSessionId,如果傳0是全局改變,但是有可能會(huì)報(bào)錯(cuò)。
if (mVisualizer == null) {mVisualizer = Visualizer(musicId)}mVisualizer?.enabled = falsemVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]mVisualizer?.setDataCaptureListener(captureListener,Visualizer.getMaxCaptureRate() / 2,true,true)// Enabled Visualizer and disable when we're done with the streammVisualizer?.enabled = true
setDataCaptureListener
?為可視化對象設(shè)置采樣監(jiān)聽數(shù)據(jù)的回調(diào),setDataCaptureListener的參數(shù)作用如下:
listener:回調(diào)對象
rate:采樣的頻率,其范圍是0~Visualizer.getMaxCaptureRate(),此處設(shè)置為最大值一半。
waveform:是否獲取波形信息
fft:是否獲取快速傅里葉變換后的數(shù)據(jù)
OnDataCaptureListener
中的兩個(gè)回調(diào)方法分別為:
onWaveFormDataCapture:波形數(shù)據(jù)回調(diào)
onFftDataCapture:傅里葉數(shù)據(jù)回調(diào),即頻率數(shù)據(jù)回調(diào)
3.設(shè)置Visualizer是否接收數(shù)據(jù)
enable為true正常接收,為false關(guān)閉
fun setVisualizerEnable(flag: Boolean) {mVisualizer?.enabled = flag
}
4. 釋放Visualizer
使用完需要調(diào)用release方法釋放
fun release() {mVisualizer?.enabled = falsemVisualizer?.release()mVisualizer = null}
完整VisualizerView代碼
package com.example.knowledgemanagement.visualizerimport android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.media.audiofx.Visualizer
import android.media.audiofx.Visualizer.OnDataCaptureListener
import android.util.AttributeSet
import android.view.View
import com.xing.commonlibrary.log.LogUtilsclass VisualizerView @JvmOverloads constructor(context: Context?,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) :View(context, attrs, defStyleAttr) {private val TAG = "VisualizerView"private var mBytes: ByteArray? = nullprivate var mFFTBytes: ByteArray? = nullprivate val mRect = Rect()private var mVisualizer: Visualizer? = nullprivate var mRenderers: MutableSet<Renderer> = HashSet()var left1 = 0var top1 = 0var right1 = 0var bottom1 = 0private var mCanvas: Canvas? = nullprivate var mAudioSamplingTime: Long = 0private var mFftSamplingTime: Long = 0private val mSamplingTime = 100 //數(shù)據(jù)采樣時(shí)間間隔private var isLink = falseinit {setLayerType(LAYER_TYPE_SOFTWARE, null) //禁止硬件加速init()}private fun init() {mBytes = nullmFFTBytes = null}override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {super.onLayout(changed, left, top, right, bottom)left1 = lefttop1 = topright1 = rightbottom1 = bottom}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)mCanvas = canvasmRect[0, 0, width] = heightmBytes?.let {// Render all audio renderersfor (r in mRenderers) {r.audioRender(canvas, it, mRect)}}mFFTBytes?.let {// Render all FFT renderersfor (r in mRenderers) {r.fftRender(canvas, it, mRect)}}}fun link(musicId: Int) {try {//使用前先釋放if (mVisualizer != null) {release()}if (mVisualizer == null && !isLink) {mVisualizer = Visualizer(musicId)isLink = true}LogUtils.e(TAG, "nsc =" + mVisualizer?.enabled)mVisualizer?.enabled = falsemVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]// Pass through Visualizer data to VisualizerViewval captureListener: OnDataCaptureListener = object : OnDataCaptureListener {override fun onWaveFormDataCapture(visualizer: Visualizer, bytes: ByteArray,samplingRate: Int) {val currentTimeMillis = System.currentTimeMillis()LogUtils.i(TAG, "onWaveFormDataCapture")if (currentTimeMillis - mAudioSamplingTime >= mSamplingTime) {mBytes = bytesinvalidate()mAudioSamplingTime = currentTimeMillis}}override fun onFftDataCapture(visualizer: Visualizer, bytes: ByteArray,samplingRate: Int) {LogUtils.i(TAG, "onFftDataCapture")val currentTimeMillis = System.currentTimeMillis()if (currentTimeMillis - mFftSamplingTime >= mSamplingTime) {mFFTBytes = bytesinvalidate()mFftSamplingTime = currentTimeMillis}}}mVisualizer?.setDataCaptureListener(captureListener,Visualizer.getMaxCaptureRate() / 2,true,true)// Enabled Visualizer and disable when we're done with the streammVisualizer?.enabled = true} catch (e: RuntimeException) {}}fun setVisualizerEnable(flag: Boolean) {mVisualizer?.enabled = flag}fun release() {mVisualizer?.enabled = falsemVisualizer?.release()mVisualizer = nullisLink = false}fun addRenderer(renderer: Renderer?) {if (renderer != null) {mRenderers.add(renderer)}}fun clearRenderers() {mRenderers.clear()}
}
5. 實(shí)現(xiàn)簡單的柱狀顯示
實(shí)現(xiàn)很簡單,就是將拿到的數(shù)據(jù)通過canvas.drawLines繪制出來,
class ColumnarRenderer( private val mPaint: Paint) : Renderer() {private val mSpectrumNum = 96override fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect) {}override fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect) {val baseX = rect.width() / mSpectrumNumval height = rect.height()for (i in 0 until mSpectrumNum) {val magnitude = (baseX * i + baseX / 2).toFloat()mFFTPoints?.let {it[i * 4] = magnitudeit[i * 4 + 1] = (height / 2).toFloat()it[i * 4 + 2] = magnitudeit[i * 4 + 3] = (height / 2 - data[i] * 4).toFloat()}}mFFTPoints?.let { canvas.drawLines(it, mPaint) }}
}
然后調(diào)用visualizerView添加到Renderer即可
visualizerView.addRenderer(columnarRenderer);
6.實(shí)現(xiàn)能量塊跳動(dòng)
代碼里面有詳細(xì)備注
package com.example.knowledgemanagement.visualizerimport android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import java.util.Random
import kotlin.math.abs
import kotlin.math.hypotclass EnergyBlockRenderer(private val mPaint: Paint) : Renderer() {companion object {private const val TAG = "EnergyBlockRenderer"private const val MAX_LEVEL = 30 //音量柱·音頻塊 - 最大個(gè)數(shù)private const val CYLINDER_NUM = 26 //音量柱 - 最大個(gè)數(shù)private const val DN_W = 470 //view寬度與單個(gè)音頻塊占比 - 正常480 需微調(diào)private const val DN_H = 300 //view高度與單個(gè)音頻塊占比private const val DN_SL = 10 //單個(gè)音頻塊寬度private const val DN_SW = 2 //單個(gè)音頻塊高度}private var mData = ByteArray(CYLINDER_NUM) //音量柱 數(shù)組private var hGap = 0private var vGap = 0private var levelStep = 0private var strokeWidth = 0fprivate var strokeLength = 0fvar mDataEn = trueinit {levelStep = 230 / MAX_LEVEL}fun onLayout(left: Int, top: Int, right: Int, bottom: Int) {val w: Float = (right - left).toFloat()val h: Float = (bottom - top).toFloat()val xr: Float = w / DN_W.toFloat()val yr: Float = h / DN_H.toFloat()strokeWidth = DN_SW * yrstrokeLength = DN_SL * xrhGap = ((w - strokeLength * CYLINDER_NUM) / (CYLINDER_NUM + 1)).toInt()vGap = (h / (MAX_LEVEL + 2)).toInt() //頻譜塊高度mPaint.strokeWidth = strokeWidth //設(shè)置頻譜塊寬度}//繪制頻譜塊和倒影private fun drawCylinder(canvas: Canvas, x: Float, value: Byte, rect: Rect) {var value = valueif (value.toInt() == 0) {value = 1} //最少有一個(gè)頻譜塊for (i in 0 until value) { //每個(gè)能量柱繪制value個(gè)能量塊val y = (rect.height() / 2 - i * vGap / 2 - vGap).toFloat() //計(jì)算y軸坐標(biāo)val y1 = (rect.height() / 2 + i * vGap / 2 + vGap).toFloat()//繪制頻譜塊mPaint.color = color //畫筆顏色canvas.drawLine(x, y, x + strokeLength, y, mPaint) //繪制頻譜塊//繪制音量柱倒影if (i <= 6 && value > 0) {mPaint.color = Color.WHITE //畫筆顏色mPaint.alpha = 100 - 100 / 6 * i //倒影顏色canvas.drawLine(x, y1, x + strokeLength, y1, mPaint) //繪制頻譜塊}}}private val color: Intprivate get() {val ranColor = intArrayOf(Color.RED, Color.YELLOW, Color.MAGENTA, Color.BLUE, Color.GREEN, Color.GRAY,Color.CYAN, Color.LTGRAY, Color.TRANSPARENT)val random = Random()val value = random.nextInt(ranColor.size - 1)return ranColor[value]}override fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect) {}override fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect) {val model = ByteArray(data.size / 2 + 1)if (mDataEn) {model[0] = abs(data[1].toInt()).toByte()var j = 1var i = 2while (i < data.size) {model[j] = hypot(data[i].toDouble(), data[i + 1].toDouble()).toInt().toByte()i += 2j++}} else {for (i in 0 until CYLINDER_NUM) {model[i] = 0}}for (i in 0 until CYLINDER_NUM) {val a = (abs(model[CYLINDER_NUM - i].toInt()) / levelStep).toByte()val b = mData[i]if (a > b) {mData[i] = a} else {if (b > 0) {mData[i]--}}}var j = -4for (i in 0 until CYLINDER_NUM / 2 - 4) {drawCylinder(canvas, strokeWidth / 2 + hGap + i * (hGap + strokeLength), mData[i], rect)}for (i in CYLINDER_NUM downTo CYLINDER_NUM / 2 - 4) {j++drawCylinder(canvas, strokeWidth / 2 + hGap + (CYLINDER_NUM / 2 + j - 1) * (hGap + strokeLength), mData[i - 1], rect)}}}
Render代碼
package com.example.knowledgemanagement.visualizerimport android.graphics.Canvas
import android.graphics.Rectabstract class Renderer {// Have these as members, so we don't have to re-create them each timevar mPoints: FloatArray? = nullvar mFFTPoints: FloatArray? = nullvar isPlaying = true// As the display of raw/FFT audio will usually look different, subclasses// will typically only implement one of the below methods/*** Implement this method to audioRender the audio data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/abstract fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect)/*** Implement this method to audioRender the FFT audio data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/abstract fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect)// These methods should actually be called for rendering/*** Render the audio data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/fun audioRender(canvas: Canvas, data: ByteArray, rect: Rect) {if (mPoints == null || mPoints!!.size < data.size * 4) {mPoints = FloatArray(data.size * 4)}onAudioRender(canvas, data, rect)}/*** Render the FFT data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/fun fftRender(canvas: Canvas, data: ByteArray, rect: Rect) {if (mFFTPoints == null || mFFTPoints!!.size < data.size * 4) {mFFTPoints = FloatArray(data.size * 4)}onFftRender(canvas, data, rect)}
}
7. 錯(cuò)誤原因跟解決辦法
下面錯(cuò)誤是因?yàn)闆]有獲取音樂的SessionId傳入,傳了0導(dǎo)致的問題。
The Visualizer initCheck failed -3 error typically occurs due to missing
permissions, invalid audio session IDs, hardware limitations, or timing issues.
By addressing these potential causes, you should be able to resolve the issue and
successfully initialize the Visualizer in your Android application.
想看更詳細(xì)的介紹可以看谷歌文檔:Visualizer ?|? Android Developers
代碼下載:https://download.csdn.net/download/u011324501/90038203