音乐播放器之示波器和读取专辑图片
来源:互联网 发布:手机维修o2o源码 编辑:程序博客网 时间:2024/04/30 12:00
Android api中提供了Visualizer来读取波形。至于读取专辑图片,mp3的ID3V2标签里包含了作者,作曲,专辑等信息,专辑图片可以从中读取,但也不是一定会有。
demo效果图
从MP3文件的ID3V2标签里读取图片
首先了解一下mp3的文件结构,MP3 文件大体分为三部分:TAG_V2(ID3V2),音频数据,TAG_V1(ID3V1)
ID3V2 在文件开始的位置,包含了作者,作曲,专辑等信息,长度不固定,扩展了ID3V1 的信息量。
一系列的音频数据的帧,在文件的中间位置,个数由文件大小和帧长决定;
每个帧的长度可能不固定,也可能固定,由位率bitrate决定
每个帧又分为帧头和数据实体两部分
帧头记录了mp3 的位率,采样率,版本等信息,每个帧之间相互独立 。ID3V1在文件结尾的位置,包含了作者,作曲,专辑等信息,长度为128Byte。
ID3V2.3
ID3V2.3标签一般包含一个标签头和若干个标签帧。
ID3V2.3{ header{ header; reVersion; flag; size; } frames[ frame{ header{ frameID; size; flag; } data; } ...... ]}
标签头
文件开始的10个字节就是标签头,顺序下来的结构如下:
public byte[] header = new byte[3]; /* 字符串 "ID3" */public byte version; /* 版本号ID3V2.3 就记录3 */public byte reVersion; /* 副版本号此版本记录为0 */public byte flag; /* 存放标志的字节,这个版本只定义了三位,很少用到,可以忽略 */public byte[] size = new byte[4];/* 大小,除了标签头的10 个字节的标签帧的大小大小为四个字节,但每个字节只用低7位,最高位不使用,恒为0,其格式如下: 0xxxxxxx 0xxxxxxx 0xxxxxxx 0xxxxxxx大小为: (size[0] & 0x7F) * 0x200000 + (size[1] & 0x7F) * 0x4000 + (size[2] & 0x7F) * 0x80 + (size[3] & 0x7F);*/
标签帧
从10个字节的标签头后开始,长度为标签头里定义的size,里面包含了多个标签帧。
标签帧的结构也类似,一个10字节的帧头,后面接着帧头里定义的size长度的帧数据。
帧头的结构如下:
public byte[] frameID = new byte[4];public int[] size = new int[4]; /*4个字节的长度,这次每个字节都用全8位,0-255,java的byte是-128-127*/public byte[] flag = new byte[2];
frameID
用四个字符标识一个帧,说明一个帧的内容含义,常用的对照如下:
TIT2=标题表示内容为这首歌的标题,下同
TPE1=作者
TALB=专集
TRCK=音轨格式:N/M 其中N 为专集中的第N 首,M为专集中共M 首,N和M 为ASCII 码表示的数字
APIC=专辑图片size
4个字节的长度,这次每个字节都用全8位,0-255。
大小值为:size[0] << 24 | size[1] << 16 | size[2] << 8 | size[3]
实现
我们只需找到APIC对应的帧数据,从数据中读取图片数据。但是这个帧数据不单单就是jpg或png图片数据,里面还有图片格式,专辑人等其他数据,所以还要从中节选出来。
上图有3中不同的情况,黑色的是10字节帧头,红框是图片数据。一般都是中间那种,1字节的标识(0)+图片格式(image/jpeg)+3字节东西(没搞懂是什么)+图片数据。由于没弄懂规则,所以只能用暴力点的方法,用jpeg/jpg的开头”FFD8FF”和png的开头”89504E”去找,还好离开头很近,没多少个字节就可以找到。
public Bitmap getMp3Album(InputStream inputStream) { try { ID3V2Header id3V2Header = new ID3V2Header(inputStream); String header = new String(id3V2Header.header); Log.d("Mp3Album", "ID3V2Header.header = " + header); if ("ID3".equals(header) && id3V2Header.version == 3) { int readed = ID3V2Header.BYTE_COUNT; int tagSize = id3V2Header.getTagSize(); while (true) { //超出范围还没读到"APIC" if ((readed - ID3V2Header.BYTE_COUNT) >= tagSize) break; ID3V2Frame frame = new ID3V2Frame(inputStream); readed += ID3V2Frame.BYTE_COUNT; String frameID = new String(frame.frameID); Log.d("Mp3Album", "ID3V2Frame.frameID = " + frameID); int frameSize = frame.getFrameSize(); Log.d("Mp3Album", "ID3V2Frame.FrameSize = " + frameSize); if ("APIC".equals(frameID)) { /*图片格式前一位是0的话,图片数据一般跟图片格式隔3字节, * 1的话一般后面还有一段描述,不过也有例外的,没搞懂,所以直接找jpg和png的开头匹配*/ int flag = inputStream.read(); readed = 1; //后面是图片格式 StringBuilder sb = new StringBuilder(); int c; while ((c = (byte) inputStream.read()) != 0) { sb.append((char) c); readed++; } readed++; //while最后一次 Log.d("Mp3Album", "图片格式:" + sb.toString()); byte[] buf = new byte[frameSize - readed]; if (inputStream.read(buf) == -1) break; int offset = getImageDataStart(buf); if (offset == -1) return null; Bitmap bm = BitmapFactory.decodeByteArray(buf, offset, buf.length - offset); Log.d("Mp3Album", "Bitmap大小:" + bm.getByteCount()); inputStream.close(); return bm; } else { //跳到下一帧 inputStream.skip(frameSize); readed += frameSize; } } } } catch (Exception e) { e.printStackTrace(); } try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } return null; } public static final int[] JPG_HEARD = new int[]{0xFF, 0xD8, 0xFF}; public static final int[] PNG_HEARD = new int[]{0x89, 0x50, 0x4E}; private int getImageDataStart(byte[] buf) { int l = buf.length - 2; for (int i = 0; i < l; i++) { if (buf[i] == (byte) JPG_HEARD[0]) { if (buf[i + 1] == (byte) JPG_HEARD[1]) { if (buf[i + 2] == (byte) JPG_HEARD[2]) { return i; } } } else if (buf[i] == (byte) PNG_HEARD[0]) { if (buf[i + 1] == (byte) PNG_HEARD[1]) { if (buf[i + 2] == (byte) PNG_HEARD[2]) { return i; } } } } return -1; }
visualizer示波器
使用Visualizer获取wave和FFT
private void setupVisualizerFxAndUI() { final int maxCR = Visualizer.getMaxCaptureRate(); final int rate = maxCR/4; Toast.makeText(this,"采样频率"+rate, Toast.LENGTH_SHORT).show(); // 实例化Visualizer,参数SessionId可以通过MediaPlayer的对象获得 mVisualizer = new Visualizer(mMediaPlayer.getAudioSessionId()); // 设置需要转换的音乐内容长度,专业的说这就是采样,该采样值一般为2的指数倍 mVisualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[0]); // 接下来就好理解了设置一个监听器来监听不断而来的所采集的数据。一共有4个参数,第一个是监听者,第二个单位是毫赫兹,表示的是采集的频率,第三个是是否采集波形,第四个是是否采集频率 mVisualizer.setDataCaptureListener( // 这个回调应该采集的是波形数据 new Visualizer.OnDataCaptureListener() { byte[] wave; byte[] fft; public void onWaveFormDataCapture(Visualizer visualizer, byte[] wave, int samplingRate) { this.wave = wave; //System.out.println("Wave数-->"+wave.length); } // 这个回调应该采集的是快速傅里叶变换有关的数据 public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { this.fft = fft; //System.out.println("Fft数-->"+fft.length); //System.out.println("samplingRate-->"+samplingRate); visualizerView.updateWithAmin(rate, this.fft, this.wave); } }, rate, true, true); }
WAVE
android api中关于得到的byte[] wave的描述如下:
The capture consists in a number of consecutive 8-bit (unsigned) mono PCM samples equal to the capture size returned by getCaptureSize().
我的FFT的处理代码如下:
private void setToWave(byte[] waveform) { if (toWave == null) { toWave = new byte[waveform.length]; nowWave = new byte[waveform.length]; drawWave = new byte[waveform.length]; step = waveform.length/dataLength; } for (int i = 0; i < waveform.length; i++) { toWave[i] = (byte) (waveform[i] + 128); Log.d(this.getClass().getName(), "wave:"+waveform[i]+"->"+toWave[i]); } lastWave = nowWave.clone(); }
FFT
android api中关于得到的byte[] fft的描述如下:
The capture is an 8-bit magnitude FFT, the frequency range covered being 0 (DC) to half of the sampling rate returned by getSamplingRate(). The capture returns the real and imaginary parts of a number of frequency points equal to half of the capture size plus one.
Note: only the real part is returned for the first point (DC) and the last point (sampling frequency / 2).
The layout in the returned byte array is as follows:
- n is the capture size returned by getCaptureSize()
- Rfk, Ifk are respectively the real and imaginary parts of the kth frequency component
- If Fs is the sampling frequency retuned by getSamplingRate() the kth frequency is: (k*Fs)/(n/2)
除了第一个和最后一个frequency component只有real parts,位于数组的前2位,其它的都有一个real parts和imaginary parts。
我的FFT的处理代码如下:
private void setToFft(byte[] fft) { int l = fft.length/2+1; if (toFFT == null) { toFFT = new int[l]; nowFFT = new int[l]; drawFFT = new int[l]; step = fft.length/dataLength; } toFFT[0] = Math.abs(fft[0]); toFFT[l-1] = Math.abs(fft[1]); for (int i = 2,j = 1; i < fft.length; i+=2,j++) { Log.d(this.getClass().getName(), "Fft:"+fft[i]+","+fft[i+1]); toFFT[j] = (int) Math.hypot(fft[i],fft[i+1]); } lastFFT = nowFFT.clone(); }
demo
- 音乐播放器之示波器和读取专辑图片
- iOS开发之音乐播放器专辑图片旋转动画
- 仿QQ音乐播放器播放音乐时专辑图片的圆形和旋转
- Android仿虾米音乐播放器之专辑图片模糊处理
- android 音乐播放器----获取专辑封面图片
- android 音乐播放器----获取专辑封面图片
- 音乐播放器问题:专辑封面和锁屏图…
- android音乐播放器_专辑列表
- IOS锁屏状态播放音乐时显示专辑信息和图片
- IOS的后台模式播放音乐,显示专辑和图片信息
- IOS锁屏状态播放音乐时显示专辑信息和图片
- IOS的后台模式播放音乐,显示专辑和图片信息
- Android应用--简、美音乐播放器获取专辑图片(自定义列表适配器)
- Android开发本地及网络Mp3音乐播放器(五)实现专辑封面图片
- GL音乐播放器<三>--界面设计之专辑照片的实现
- Android自定义View——绘制音乐播放器示波器
- android音乐播放器_专辑下歌曲列表
- IOS播放器(后台播放,锁屏状态显示专辑图片和信息
- java中+=和=的区别
- C++拷贝引用构造函数
- 读书笔记-装饰模式
- javaweb登录验证码的实现
- Problem
- 音乐播放器之示波器和读取专辑图片
- C++中类成员的访问规则
- Axis2+Tomcat+WebService
- echarts 使用配置
- 将Putty生成的PrivateKey转换为SecureCRT所需的PublicKey
- React中ES6事件绑定相关事项
- 前段时间写代码总结
- JAVA 使用Dom4j 解析XML
- 公积金查询api