前言

使用Kotlin实现一个简易的本地音乐客户端,部分代码为Java语言,Kotlin可以完美调用Java,Enjoy it!

android 音乐识别 android听歌识曲开发_android 音乐识别

三方库Glide

Anko
BaseRecyclerViewAdapterHelper
Greendao
环境准备
项目使用的是最新的 Android Studio3.0 版本,新版的Studio只需要在项目新建时勾选“Include Kotlin support”即可导入Kotlin的支持,无需配置复杂环境。
效果图
项目目录
歌词显示
歌词显示主要实现逻辑件位于view文件夹下的ILrcBuilder、ILrcView、LrcRow、LrcView四个文件。在需要实现歌词显示的Activity中添加如下布局文件即可:
android:id="@+id/lrcView"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
其中LrcRow文件下可以设置歌词的相关属性参数,例如歌词文字的大小、行间距、正在播放歌词的颜色等等。private List mLrcRows;     // all lrc rows of one lrc file
private int mMinSeekFiredOffset = 10; // min offset for fire seek action, px;
private int mHignlightRow = 0;   // current singing row , should be highlighted.
private int mHignlightRowColor = Color.YELLOW;
private int mNormalRowColor = Color.WHITE;    private int mSeekLineColor = Color.CYAN;    private int mSeekLineTextColor = Color.CYAN;    private int mSeekLineTextSize = 20;    private int mMinSeekLineTextSize = 30;    private int mMaxSeekLineTextSize = 50;    private int mLrcFontSize = 50;     // font size of lrc
private int mMinLrcFontSize = 10;    private int mMaxLrcFontSize = 40;    private int mPaddingY = 43;      // padding of each row
private int mSeekLinePaddingX = 0; // Seek line padding x
private int mDisplayMode = DISPLAY_MODE_NORMAL;    private LrcViewListener mLrcViewListener;注:后续扩展在线歌词,歌词路径暂时写死在手机根目录,文件名为“林中鸟.lrc”。
扫描音乐
扫描音乐工具类
扫描音乐的实现逻辑如下,DISPLAY_NAME、TITLE、ARTIST、DATA、SIZE、DURATION分别对应了扫描音乐的外部可重命名的文件名、内部音乐文件名、歌手名称、文件所在路径、歌曲的大小、歌曲的时长信息。获取后可用于后续自定义扫描码、歌曲播放设置等处。isAudioControlPanelAvailable()方法是用来设置音效用的,后续会说到。object MusicUtils {    /**
* 扫描系统里面的音频文件,返回一个list集合
*/
fun getMusicData(context: Context): ArrayList {
val filter_size = PreferenceManager.getDefaultSharedPreferences(context).getString("filter_size", "0")
val filter_time = PreferenceManager.getDefaultSharedPreferences(context).getString("filter_time", "0")
val list = ArrayList()        // 媒体库查询语句(写一个工具类MusicUtils)
val cursor = context.contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.AudioColumns.IS_MUSIC)        if (cursor != null) {            while (cursor.moveToNext()) {
val music: Music? = Music()//                music!!.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME))
music!!.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE))
music.singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST))
music.path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))
music.size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE))
music.duration = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION))                if (music.size.toInt()/1000 > filter_size.toInt() || music.duration!!.toInt()/1000 > filter_time.toInt()) {                    // 注释部分是切割标题,分离出歌曲名和歌手 (本地媒体库读取的歌曲信息不规范)
if (music.song!!.contains("-")) {
val str = music.song!!.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
music.singer = str[0]
music.song = str[1]
}
list.add(music)
}
}            // 释放资源
cursor.close()
}        return list
}    fun isAudioControlPanelAvailable(context: Context): Boolean {        return isIntentAvailable(context, Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL))
}    private fun isIntentAvailable(context: Context, intent: Intent): Boolean {        return context.packageManager.resolveActivity(intent, PackageManager.GET_RESOLVED_FILTER) != null
}
}
扫描音乐动画
点击扫描音乐可实现雷达动画,音乐扫描完毕动画结束,实现扫描代码:fun scanMusic() {
val degrees = ObjectAnimator.ofInt(sv, "degrees", 0, 360)
degrees.interpolator = LinearInterpolator()
degrees.duration = 1000
degrees.repeatCount = ValueAnimator.INFINITE
degrees.start()
async({
MyApplication.instance!!.getMusicDao().deleteAll()            for (i in 0..getMusicData(MyApplication.instance!!).size - 1) {
val list = Music(i.toLong(), getMusicData(MyApplication.instance!!).get(i).song, getMusicData(MyApplication.instance!!).get(i).singer, getMusicData(MyApplication.instance!!).get(i).path)
MyApplication.instance!!.getMusicDao().insertInTx(list)
}
runOnUiThread {
degrees.cancel()
Toast.makeText(MyApplication.instance!!,"已扫描完毕",Toast.LENGTH_SHORT).show()
}
})
}
动画效果代码如下:public class ShaderView extends View {    /**
* 绘制扫描圈的笔
*/
private Paint mSweepPaint;    /**
* 绘制背景bitmap的笔
*/
private Paint mBitmapPaint;    /**
* 这个自定义View的宽度,就是你在xml布局里面设置的宽度(目前不支持)
*/
private int mWidth;    /**
* 背景图片
*/
private Bitmap mBitmap;    /**
* 雷达扫描旋转角度
*/
private int degrees = 0;    /**
* 用于控制扫描圈的矩阵
*/
Matrix mSweepMatrix = new Matrix();    /**
* 用于控制背景Bitmap的矩阵
*/
Matrix mBitmapMatrix = new Matrix();    /**
* 着色器---生成扫描圈
*/
private SweepGradient mSweepGradient;    /**
* 图片着色器
*/
private BitmapShader mBitmapShader;    private float mScale;    public ShaderView(Context context) {        super(context);
init();
}    public ShaderView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);
init();
}    /**
* 属性动画,必须有setXxx方法,才可以针对这个属性实现动画
*
* @param degrees
*/
public void setDegrees(int degrees) {        this.degrees = degrees;
postInvalidate();//在主线程里执行OnDraw
}    private void init() {//    1.准备好画笔
mSweepPaint = new Paint();
mBitmapPaint = new Paint();//    2.图片着色器
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scan_music);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);//    3.将图片着色器设置给画笔
mBitmapPaint.setShader(mBitmapShader);
}    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);//    获取这个自定义view的宽高,注意在onMeasure里获取,在构造函数里得到的是0
mWidth = getMeasuredWidth();//    根据你所设置的view的尺寸和bitmap的尺寸计算一个缩放比例,否则的话,得到的图片是一个局部,而不是一整张图片
mScale = (float) mWidth / (float) mBitmap.getWidth();//    4.梯度扫描着色器
mSweepGradient = new SweepGradient(mWidth / 2, mWidth / 2, new int[]{Color.argb(50, 0, 0, 100), Color.argb(10, 30, 0, 0)}, null);//    5.将梯度扫描着色器设置给另外一支画笔
mSweepPaint.setShader(mSweepGradient);
}    @Override
protected void onDraw(Canvas canvas) {        super.onDraw(canvas);//    迫不得已的时候,才在onDraw方法写代码,能提前准备的要在之前去准备,//    不要写在onDraw里面,因为onDraw会不停地刷新绘制,写的代码越多,越影响效率//    将图片缩放至你指定的自定义View的宽高
mBitmapMatrix.setScale(mScale, mScale);
mBitmapShader.setLocalMatrix(mBitmapMatrix);//   设置扫描圈旋转角度
mSweepMatrix.setRotate(degrees, mWidth / 2, mWidth / 2);
mSweepGradient.setLocalMatrix(mSweepMatrix);//    5. 使用设置好图片着色器的画笔,画圆,先画出下层的背景图片,在画出上层的扫描图片
canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2, mBitmapPaint);
canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2, mSweepPaint);
}
}

实现音效设置

效果图:

android 音乐识别 android听歌识曲开发_android 音乐识别

上文提到的isAudioControlPanelAvailable()方法在设置中的音效处调用。AudioSessionId获取当前播放的音乐sessionid,如果将AudioSessionId设为0,则所有在播放的音频都会被影响到。

调用代码:private fun startEqualizer() {            if (MusicUtils.isAudioControlPanelAvailable(activity)) {
val intent = Intent()
val packageName = activity.packageName
intent.action = AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, AudioSessionId)                try {
startActivityForResult(intent, 1)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
Toast.makeText(context, "设备不支持", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "设备不支持", Toast.LENGTH_SHORT).show()
}
}
定时停止播放
音乐播放器一般会实现定时停止播放功能,向TimeStop()方法中输入需要定时的参数。
代码如下:fun TimeStop(time: Long) {        if (time.toInt() == 0 && job == null) {
Toast.makeText(this, "停止播放功能未开启", Toast.LENGTH_SHORT).show()
} else if (time.toInt() != 0) {
val time1 = time * 60000
Toast.makeText(this, "$time 分钟后停止播放", Toast.LENGTH_SHORT).show()
job = launch(CommonPool) {
delay(time1, TimeUnit.MILLISECONDS)
playService!!.pause()
}
} else {
job!!.cancel()
Toast.makeText(this, "已取消定时停止播放", Toast.LENGTH_SHORT).show()
}
}
歌曲控制
暂停播放等不在此赘述,具体可看源码,控制上一首下一首使用了Greendao数据库,扫码歌曲时,数据填入数据库中,下面是返回上一首的实现代码,下一首实现功能类似,使用Greendao主要是用来练练手,没有什么特别的原因。fun previous() {        if (position == 0) {
num = MyApplication.instance!!.getMusicDao().queryBuilder().list().size.plus(-1)
path = MyApplication.instance!!.getMusicDao().queryBuilder().where(MusicDao.Properties.Id.eq(num)).list()
} else {
num = position!!.plus(-1)
path = MyApplication.instance!!.getMusicDao().queryBuilder().where(MusicDao.Properties.Id.eq(num)).list()
}
prepare((path as MutableList?)!!.get(0).path)
start()
position = num
val intent = Intent("com.nilin.etherealmusic.play")
intent.putExtra("song", (path as MutableList?)!!.get(0).song)
intent.putExtra("singer", (path as MutableList?)!!.get(0).singer)
sendBroadcast(intent)
val editor = MyApplication.instance!!.getSharedPreferences("music_pref", Context.MODE_PRIVATE).edit()
editor.putString("song", (path as MutableList?)!!.get(0).song)
editor.putInt("position", position!!)
editor.apply()
}

启动引导页动画、搜索后音乐列表显示、音乐进度条拖拽快进、扫描音乐条件控制等不多赘述。功能实现并不复杂,为后续添加网络音乐功能做准备,部分功能暂未实现,后续会更新完善。