您的位置:首页 > 产品设计 > UI/UE

用JAVA编写MP3解码器——GUI

2016-08-11 17:25 447 查看
以下代码是开源(GPL)程序jmp123的一部分。

 
http://blog.csdn.net/ycb1689/article/details/17393135
(一)简单的GUI

 



在jmp123.jar所在目录为当前目录启动jmp123.jar,启动时自动加载default.m3u、bk1.jpg、bk2.jpg;
为方便测试MP3解码器,简体中文环境时播放器有网络搜索MP3功能,出于对某MP3网站的尊重,源代码中未附上搜索功能的源代码,请谅解。请勿对程序反相查看源代码,请自觉遵守:)
(二)解码速度测试 完全解码但不播放输出:

Java -cp jmp123.jar jmp123.test.Test1 <MP3文件名>

这个纯JAVA解码器的速度是很快的。即将放出的下一个版本0.2采用帧间并行运算针对多核心的CPU运行优化 。

 

 

(三)频谱显示

 

1.捕获音频输出

  将音乐可视化首先要获取音乐数据,可以从音频输出捕获PCM数据。如果频谱显示是内置在音频解码器中,这一步就可以省略,取而代之的是直接从解码器复制PCM数据,这样占用的资源少而且速度快。

[java]
view plain
copy





/* 
 * WaveIn.java 
 * 捕获音频输出 
 */  
import javax.sound.sampled.AudioFormat;  
import javax.sound.sampled.AudioSystem;  
import javax.sound.sampled.DataLine;  
import javax.sound.sampled.TargetDataLine;  
  
public class WaveIn {  
    private AudioFormat af;  
    private DataLine.Info dli;  
    private TargetDataLine tdl;  
  
    /** 
     * 打开音频目标数据行。从中读取音频数据格式为:采样率32kHz,每个样本16位,单声道,有符号的,little-endian。 
     * @return 成功打开返回true,否则false。 
     */  
    public boolean open() {  
        af = new AudioFormat(32000, 16, 1, true, false);  
        dli = new DataLine.Info(TargetDataLine.class, af);  
        try {  
            tdl = (TargetDataLine) AudioSystem.getLine(dli);  
            tdl.open(af, FFT.FFT_N << 1);  
        } catch (Exception e) {  
            e.printStackTrace();  
            return false;  
        }  
  
        return true;  
    }  
  
    public void close() {  
        tdl.close();  
    }  
  
    public void start() {  
        tdl.start();  
    }  
  
    public void stop() {  
        tdl.stop();  
    }  
  
    public int read(byte[] b, int len) {  
        return tdl.read(b, 0, len);  
    }  
  
    private double phase0 = 0;  
    /** 
     * 产生频率264Hz,采样率为44.1kHz,幅值为0x7fff,每个样本16位的PCM。 
     * @param b 接收PCM样本。 
     * @param len PCM样本字节数。 
     */  
    public void getWave264(byte[] b, int len) {  
        double dt = 2 * 3.14159265358979323846 * 264 / 44100;  
        int i, pcmi;  
        len >>= 1;  
        for (i = 0; i < len; i++) {  
            pcmi = (short) (0x7fff * Math.sin(i * dt + phase0));  
            b[2 * i] = (byte) pcmi;  
            b[2 * i + 1] = (byte) (pcmi >>> 8);  
        }  
        phase0 += i * dt;  
    }  
}  

 

2.将时域PCM数据变换到频域

  用FFT完成PCM数据从
时域 到频域 的变换,这本是本文技术含量最高的活儿,想必大家对FFT都很熟悉了吧,对FFT方法本身就不多说了。

  时域PCM数据是16位的short类型,取值范围是-32768..32767。对于频谱显示用512点FFT就足够了,我们知道音频数据的截止频率是由其采样率决定的,如果采样率为32kHz,截止频率为16kHz。可以计算出FFT后频率间隔为16*1024/(512/2)=64Hz,即经过FFT后下文源代码中realIO得到256个值:realIO[i]是64*i至64*(i+1)Hz频率范围内的“幅值”(这里不是真正的幅值,是复数模的平方再乘以512,如果要得到幅值,需要开方后再除以512)。

  为了减少不必要的浮点运算,这里淘汰了“幅值”较小的输出,直接将它的值置零。依据的原理是:如果FFT后得到的复数的模太小,除以512后取整为零,干脆先将这样的值置零。

[java]
view plain
copy





/* 
 * FFT.java 
 * 用于频谱显示的快速傅里叶变换 
 * http://jmp123.sf.net/ 
 */  
public class FFT {  
    public static final int FFT_N_LOG = 9; // FFT_N_LOG <= 13  
    public static final int FFT_N = 1 << FFT_N_LOG;  
    private static final float MINY = (float) ((FFT_N << 2) * Math.sqrt(2)); //(*)  
    private float[] real, imag, sintable, costable;  
    private int[] bitReverse;  
  
    public FFT() {  
        real = new float[FFT_N];  
        imag = new float[FFT_N];  
        sintable = new float[FFT_N >> 1];  
        costable = new float[FFT_N >> 1];  
        bitReverse = new int[FFT_N];  
  
        int i, j, k, reve;  
        for (i = 0; i < FFT_N; i++) {  
            k = i;  
            for (j = 0, reve = 0; j != FFT_N_LOG; j++) {  
                reve <<= 1;  
                reve |= (k & 1);  
                k >>>= 1;  
            }  
            bitReverse[i] = reve;  
        }  
  
        double theta, dt = 2 * 3.14159265358979323846 / FFT_N;  
        for (i = 0; i < (FFT_N >> 1); i++) {  
            theta = i * dt;  
            costable[i] = (float) Math.cos(theta);  
            sintable[i] = (float) Math.sin(theta);  
        }  
    }  
  
    /** 
     * 用于频谱显示的快速傅里叶变换 
     * @param realIO 输入FFT_N个实数,也用它暂存fft后的FFT_N/2个输出值(复数模的平方)。 
     */  
    public void calculate(float[] realIO) {  
        int i, j, k, ir, exchanges = 1, idx = FFT_N_LOG - 1;  
        float cosv, sinv, tmpr, tmpi;  
        for (i = 0; i != FFT_N; i++) {  
            real[i] = realIO[bitReverse[i]];  
            imag[i] = 0;  
        }  
  
        for (i = FFT_N_LOG; i != 0; i--) {  
            for (j = 0; j != exchanges; j++) {  
                cosv = costable[j << idx];  
                sinv = sintable[j << idx];  
                for (k = j; k < FFT_N; k += exchanges << 1) {  
                    ir = k + exchanges;  
                    tmpr = cosv * real[ir] - sinv * imag[ir];  
                    tmpi = cosv * imag[ir] + sinv * real[ir];  
                    real[ir] = real[k] - tmpr;  
                    imag[ir] = imag[k] - tmpi;  
                    real[k] += tmpr;  
                    imag[k] += tmpi;  
                }  
            }  
            exchanges <<= 1;  
            idx--;  
        }  
  
        j = FFT_N >> 1;  
        /* 
         * 输出模的平方(的FFT_N倍): 
         * for(i = 1; i <= j; i++) 
         *  realIO[i-1] = real[i] * real[i] +  imag[i] * imag[i]; 
         *  
         * 如果FFT只用于频谱显示,可以"淘汰"幅值较小的而减少浮点乘法运算. MINY的值 
         * 和Spectrum.Y0,Spectrum.logY0对应. 
         */  
        sinv = MINY;  
        cosv = -MINY;  
        for (i = j; i != 0; i--) {  
            tmpr = real[i];  
            tmpi = imag[i];  
            if (tmpr > cosv && tmpr < sinv && tmpi > cosv && tmpi < sinv)  
                realIO[i - 1] = 0;  
            else  
                realIO[i - 1] = tmpr * tmpr + tmpi * tmpi;  
        }  
    }  
  
}  

 

3.频谱显示

  (1).频段量化。512点FFT的输出为线性的,即0到音频截止频率(例如16kHz)等分为256个频段,频谱显示时至多可以显示256段。其实我们用不着显示这么多段,32段足矣,这里采用64段。研究表明人耳对频率的感知不是线性的,即频率升高一倍我们感知到的不是一倍,所以这里将256个频段非线性对应到64个频段内。这里采用指数方式作非线性划分,为什么用指数方式不用别的呢?我也不太清楚,我记得书上大概是这么说的吧。想想也合理,人耳朵对低频的感知较为不灵敏,所以一些音响对低频段作了提升,使得低频的能量远高于高频段,我们离音响比较远的时候,只听见低频段的声音,不是因为低频段的穿透性强,重要原因是其幅值大。频段量化见Spectrum的setPlot方法。

  (2).音频数据抽取。频谱显示看起来是“实时”显示的,其实怎么可能呢?一是我们只是作了512点FFT(16kHz时频率分辨率为64Hz,比较粗略);二是显示的时候每秒显示10多帧就足够了,即使每秒显示100帧以上,我们看得过来吗?所以我们只需要对音频数据间隔一段时间抽取一些出来分析、显示,这用Spectrum的run方法里的延时语句实现。解释一下run方法里的这一语句:

[java]
view plain
copy





realIO[i] = (b[j + 1] << 8) | (b[j] & 0xff);  

 

从混音器捕获到的数据是byte类型,需要转换为PCM的16位符号整数,高字节b[j+1]的符号确定了PCM数据的符号。JAVA的数据类型转换那是相当的麻烦,好在频谱显示不是真正意义上的“实时”的,所以尽管要进行FFT等这样大量运算,采用延时一段时间抽取数据出来分析使得整体的运算量不大。

  (3).绘制“频率-幅值”直方图。采用内存作图,绘制好一帧后刷到屏幕上去。
直方图中柱体的长度代表该频段的幅值,这个幅值用对数量化,据说人耳朵对音频幅值(能量)的感知也是非线性的,呈对数函数特性的非线性。另外,本来应该对高频段的柱体长度作等响度修正,这样呈现在屏幕上的频谱直方图看起来才符合我们感知到的音乐,可是等响度修正系数没找到免费的供我们用用,人家申请得有专利,要¥或$或那个什么来着,那就算啦。

  (4).对经过FFT后得到的频域数据作怎样的处理使它呈现到屏幕上,并无定势,以上只是我的一个方法,你可以根据自己的喜好修改。

[java]
view plain
copy





/* 
 * Spectrum.java 
 * 频谱显示 
 * http://jmp123.sf.net/ 
 */  
import java.awt.Color;  
import java.awt.Dimension;  
import java.awt.GradientPaint;  
import java.awt.Graphics;  
import java.awt.Graphics2D;  
import java.awt.image.BufferedImage;  
  
import javax.swing.JComponent;  
  
public class Spectrum extends JComponent implements Runnable {  
    private static final long serialVersionUID = 1L;  
    private static final int maxColums = 128;  
    private static final int Y0 = 1 << ((FFT.FFT_N_LOG + 3) << 1);  
    private static final double logY0 = Math.log10(Y0); //lg((8*FFT_N)^2)  
    private int band;  
    private int width, height;  
    private int[] xplot, lastPeak, lastY;  
    private int deltax;  
    private long lastTimeMillis;  
    private BufferedImage spectrumImage, barImage;  
    private Graphics spectrumGraphics;  
    private boolean isAlive;  
  
    public Spectrum() {  
        isAlive = true;  
        band = 64;      //64段  
        width = 383;    //频谱窗口 383x124  
        height = 124;  
        lastTimeMillis = System.currentTimeMillis();  
        xplot = new int[maxColums + 1];  
        lastPeak = new int[maxColums];  
        lastY = new int[maxColums];  
        spectrumImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);  
        spectrumGraphics = spectrumImage.getGraphics();  
        setPreferredSize(new Dimension(width, height));  
        setPlot();  
        barImage = new BufferedImage(deltax - 1, height, BufferedImage.TYPE_3BYTE_BGR);  
  
        setColor(0x7f7f7f, 0xff0000, 0xffff00, 0x7f7fff);  
    }  
  
    public void setColor(int rgbPeak, int rgbTop, int rgbMid, int rgbBot) {  
        Color crPeak = new Color(rgbPeak);  
        spectrumGraphics.setColor(crPeak);  
  
        spectrumGraphics.setColor(Color.gray);  
        Graphics2D g = (Graphics2D)barImage.getGraphics();  
        Color crTop = new Color(rgbTop);  
        Color crMid = new Color(rgbMid);  
        Color crBot = new Color(rgbBot);  
        GradientPaint gp1 = new GradientPaint(0, 0, crTop,deltax - 1,height/2,crMid);  
        g.setPaint(gp1);  
        g.fillRect(0, 0, deltax - 1, height/2);  
        GradientPaint gp2 = new GradientPaint(0, height/2, crMid,deltax - 1,height,crBot);  
        g.setPaint(gp2);  
        g.fillRect(0, height/2, deltax - 1, height);  
        gp1 = gp2 = null;  
        crPeak = crTop = crMid = crBot = null;  
    }  
  
    private void setPlot() {  
        deltax = (width - band + 1) / band + 1;  
  
        // 0-16kHz分划为band个频段,各频段宽度非线性划分。  
        for (int i = 0; i <= band; i++) {  
            xplot[i] = 0;  
            xplot[i] = (int) (0.5 + Math.pow(FFT.FFT_N >> 1, (double) i   / band));  
            if (i > 0 && xplot[i] <= xplot[i - 1])  
                xplot[i] = xplot[i - 1] + 1;  
        }  
    }  
  
    /** 
     * 绘制"频率-幅值"直方图并显示到屏幕。 
     * @param amp amp[0..FFT.FFT_N/2-1]为频谱"幅值"(用复数模的平方)。 
     */  
    private void drawHistogram(float[] amp) {  
        spectrumGraphics.clearRect(0, 0, width, height);  
  
        long t = System.currentTimeMillis();  
        int speed = (int)(t - lastTimeMillis) / 30; //峰值下落速度  
        lastTimeMillis = t;  
  
        int i = 0, x = 0, y, xi, peaki, w = deltax - 1;  
        float maxAmp;  
        for (; i != band; i++, x += deltax) {  
            // 查找当前频段的最大"幅值"  
            maxAmp = 0; xi = xplot[i]; y = xplot[i + 1];  
            for (; xi < y; xi++) {  
                if (amp[xi] > maxAmp)  
                    maxAmp = amp[xi];  
            }  
  
            /* 
             * maxAmp转换为用对数表示的"分贝数"y: 
             * y = (int) Math.sqrt(maxAmp); 
             * y /= FFT.FFT_N; //幅值 
             * y /= 8;  //调整 
             * if(y > 0) y = (int)(Math.log10(y) * 20 * 2); 
             *  
             * 为了突出幅值y显示时强弱的"对比度",计算时作了调整。未作等响度修正。 
             */  
            y = (maxAmp > Y0) ? (int) ((Math.log10(maxAmp) - logY0) * 20) : 0;  
  
            // 使幅值匀速度下落  
            lastY[i] -= speed << 2;  
            if(y < lastY[i]) {  
                y = lastY[i];  
                if(y < 0) y = 0;  
            }  
            lastY[i] = y;  
  
            if(y >= lastPeak[i]) {  
                lastPeak[i] = y;  
            } else {  
                // 使峰值匀速度下落  
                peaki = lastPeak[i] - speed;  
                if(peaki < 0)  
                    peaki = 0;  
                lastPeak[i] = peaki;  
                peaki = height - peaki;  
                spectrumGraphics.drawLine(x, peaki, x + w - 1, peaki);  
            }  
  
            // 画当前频段的直方图  
            y = height - y;  
            spectrumGraphics.drawImage(barImage, x, y, x+w, height, 0, y, w, height, null);  
        }  
  
        // 刷新到屏幕  
        repaint(0, 0, width, height);  
    }  
  
    public void paintComponent(Graphics g) {  
        g.drawImage(spectrumImage, 0, 0, null);  
    }  
  
    public void run() {  
        WaveIn wi = new WaveIn();  
        wi.open();  
        wi.start();  
  
        FFT fft = new FFT();  
        byte[] b = new byte[FFT.FFT_N << 1];  
        float realIO[] = new float[FFT.FFT_N];  
        int i, j;  
  
        try {  
            while (isAlive) {  
                Thread.sleep(80);// 延时不准确,这不重要  
  
                // 从混音器录制数据并转换为short类型的PCM  
                wi.read(b, FFT.FFT_N << 1);  
                //wi.getWave264(b, FFT.FFT_N << 1);//debug  
                for (i = j = 0; i != FFT.FFT_N; i++, j += 2)  
                    realIO[i] = (b[j + 1] << 8) | (b[j] & 0xff); //signed short  
  
                // 时域PCM数据变换到频域,取回频域幅值  
                fft.calculate(realIO);  
  
                // 绘制  
                drawHistogram(realIO);  
            }  
  
            wi.close();  
        } catch (InterruptedException e) {  
            // e.printStackTrace();  
        }  
    }  
  
    public void stop() {  
        isAlive = false;  
    }  
}  

 

4.测试

  你坐得这么直看了这么久,不demo一下下,说不过去。



[java]
view plain
copy





import javax.swing.JFrame;  
  
public class SpectrumTest {  
    public static void main(String[] args) {  
        JFrame frame = new JFrame();  
        final Spectrum spec = new Spectrum();  
        frame.getContentPane().add(spec);  
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  
        frame.setTitle("Audio Spectrum");  
        frame.setResizable(false);  
        frame.pack();  
        //frame.setAlwaysOnTop(true);  
        frame.setVisible(true);  
        //com.sun.awt.AWTUtilities.setWindowOpacity(frame, 0.8f);  
        new Thread(spec).start();  
    }  
  
}  

 

5.其它

  (1 ).WaveIn要从混音器的“立体声混音器”获取音频数据,要打开音频属性调节->录音->选择立体声混音器,并将立体声混音器的音量推到最大。调节不来的喊我,顺便蹭顿饭吃吃:)   

  (2).以上代码实现了从音频输出捕获数据并显示其频谱直方图,直接从音频输出捕获数据的优点是与程序其它模块之间没有依赖性,缺点是资源占用较大,效率较低。内置在解码器里的频谱显示使程序模块之间耦合性增大,但运行效率高。我写了一个播放器,内置了频谱显示。下载地址:

http://jmp123.sf.net/

 

 

顶3踩0
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: