音乐播放器之示波器和读取专辑图片

来源:互联网 发布:手机维修o2o源码 编辑:程序博客网 时间:2024/04/30 12:00

Android api中提供了Visualizer来读取波形。至于读取专辑图片,mp3的ID3V2标签里包含了作者,作曲,专辑等信息,专辑图片可以从中读取,但也不是一定会有。
demo效果图
这里写图片描述

从MP3文件的ID3V2标签里读取图片

首先了解一下mp3的文件结构,MP3 文件大体分为三部分:TAG_V2(ID3V2),音频数据,TAG_V1(ID3V1)

  1. ID3V2 在文件开始的位置,包含了作者,作曲,专辑等信息,长度不固定,扩展了ID3V1 的信息量。

  2. 一系列的音频数据的帧,在文件的中间位置,个数由文件大小和帧长决定;
    每个帧的长度可能不固定,也可能固定,由位率bitrate决定
    每个帧又分为帧头和数据实体两部分
    帧头记录了mp3 的位率,采样率,版本等信息,每个帧之间相互独立 。

  3. 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];
  1. frameID

    用四个字符标识一个帧,说明一个帧的内容含义,常用的对照如下:
    TIT2=标题表示内容为这首歌的标题,下同
    TPE1=作者
    TALB=专集
    TRCK=音轨格式:N/M 其中N 为专集中的第N 首,M为专集中共M 首,N和M 为ASCII 码表示的数字
    APIC=专辑图片

  2. 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)
Index

0

1

2

3

4

5

n - 2

n - 1

Data

Rf0

Rf(n/2)

Rf1

If1

Rf2

If2

Rf(n-1)/2

If(n-1)/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

0 0
原创粉丝点击