用JAVA编写MP3解码器——GUI

以下代码是开源(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数据,这样占用的资源少而且速度快。

  1. /* 
  2.  * WaveIn.java 
  3.  * 捕获音频输出 
  4.  */  
  5. import javax.sound.sampled.AudioFormat;  
  6. import javax.sound.sampled.AudioSystem;  
  7. import javax.sound.sampled.DataLine;  
  8. import javax.sound.sampled.TargetDataLine;  
  9.   
  10. public class WaveIn {  
  11.     private AudioFormat af;  
  12.     private DataLine.Info dli;  
  13.     private TargetDataLine tdl;  
  14.   
  15.     /** 
  16.      * 打开音频目标数据行。从中读取音频数据格式为:采样率32kHz,每个样本16位,单声道,有符号的,little-endian。 
  17.      * @return 成功打开返回true,否则false。 
  18.      */  
  19.     public boolean open() {  
  20.         af = new AudioFormat(32000161truefalse);  
  21.         dli = new DataLine.Info(TargetDataLine.class, af);  
  22.         try {  
  23.             tdl = (TargetDataLine) AudioSystem.getLine(dli);  
  24.             tdl.open(af, FFT.FFT_N << 1);  
  25.         } catch (Exception e) {  
  26.             e.printStackTrace();  
  27.             return false;  
  28.         }  
  29.   
  30.         return true;  
  31.     }  
  32.   
  33.     public void close() {  
  34.         tdl.close();  
  35.     }  
  36.   
  37.     public void start() {  
  38.         tdl.start();  
  39.     }  
  40.   
  41.     public void stop() {  
  42.         tdl.stop();  
  43.     }  
  44.   
  45.     public int read(byte[] b, int len) {  
  46.         return tdl.read(b, 0, len);  
  47.     }  
  48.   
  49.     private double phase0 = 0;  
  50.     /** 
  51.      * 产生频率264Hz,采样率为44.1kHz,幅值为0x7fff,每个样本16位的PCM。 
  52.      * @param b 接收PCM样本。 
  53.      * @param len PCM样本字节数。 
  54.      */  
  55.     public void getWave264(byte[] b, int len) {  
  56.         double dt = 2 * 3.14159265358979323846 * 264 / 44100;  
  57.         int i, pcmi;  
  58.         len >>= 1;  
  59.         for (i = 0; i < len; i++) {  
  60.             pcmi = (short) (0x7fff * Math.sin(i * dt + phase0));  
  61.             b[2 * i] = (byte) pcmi;  
  62.             b[2 * i + 1] = (byte) (pcmi >>> 8);  
  63.         }  
  64.         phase0 += i * dt;  
  65.     }  
  66. }  


 

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后取整为零,干脆先将这样的值置零。

  1. /* 
  2.  * FFT.java 
  3.  * 用于频谱显示的快速傅里叶变换 
  4.  * http://jmp123.sf.net/ 
  5.  */  
  6. public class FFT {  
  7.     public static final int FFT_N_LOG = 9// FFT_N_LOG <= 13  
  8.     public static final int FFT_N = 1 << FFT_N_LOG;  
  9.     private static final float MINY = (float) ((FFT_N << 2) * Math.sqrt(2)); //(*)  
  10.     private float[] real, imag, sintable, costable;  
  11.     private int[] bitReverse;  
  12.   
  13.     public FFT() {  
  14.         real = new float[FFT_N];  
  15.         imag = new float[FFT_N];  
  16.         sintable = new float[FFT_N >> 1];  
  17.         costable = new float[FFT_N >> 1];  
  18.         bitReverse = new int[FFT_N];  
  19.   
  20.         int i, j, k, reve;  
  21.         for (i = 0; i < FFT_N; i++) {  
  22.             k = i;  
  23.             for (j = 0, reve = 0; j != FFT_N_LOG; j++) {  
  24.                 reve <<= 1;  
  25.                 reve |= (k & 1);  
  26.                 k >>>= 1;  
  27.             }  
  28.             bitReverse[i] = reve;  
  29.         }  
  30.   
  31.         double theta, dt = 2 * 3.14159265358979323846 / FFT_N;  
  32.         for (i = 0; i < (FFT_N >> 1); i++) {  
  33.             theta = i * dt;  
  34.             costable[i] = (float) Math.cos(theta);  
  35.             sintable[i] = (float) Math.sin(theta);  
  36.         }  
  37.     }  
  38.   
  39.     /** 
  40.      * 用于频谱显示的快速傅里叶变换 
  41.      * @param realIO 输入FFT_N个实数,也用它暂存fft后的FFT_N/2个输出值(复数模的平方)。 
  42.      */  
  43.     public void calculate(float[] realIO) {  
  44.         int i, j, k, ir, exchanges = 1, idx = FFT_N_LOG - 1;  
  45.         float cosv, sinv, tmpr, tmpi;  
  46.         for (i = 0; i != FFT_N; i++) {  
  47.             real[i] = realIO[bitReverse[i]];  
  48.             imag[i] = 0;  
  49.         }  
  50.   
  51.         for (i = FFT_N_LOG; i != 0; i--) {  
  52.             for (j = 0; j != exchanges; j++) {  
  53.                 cosv = costable[j << idx];  
  54.                 sinv = sintable[j << idx];  
  55.                 for (k = j; k < FFT_N; k += exchanges << 1) {  
  56.                     ir = k + exchanges;  
  57.                     tmpr = cosv * real[ir] - sinv * imag[ir];  
  58.                     tmpi = cosv * imag[ir] + sinv * real[ir];  
  59.                     real[ir] = real[k] - tmpr;  
  60.                     imag[ir] = imag[k] - tmpi;  
  61.                     real[k] += tmpr;  
  62.                     imag[k] += tmpi;  
  63.                 }  
  64.             }  
  65.             exchanges <<= 1;  
  66.             idx--;  
  67.         }  
  68.   
  69.         j = FFT_N >> 1;  
  70.         /* 
  71.          * 输出模的平方(的FFT_N倍): 
  72.          * for(i = 1; i <= j; i++) 
  73.          *  realIO[i-1] = real[i] * real[i] +  imag[i] * imag[i]; 
  74.          *  
  75.          * 如果FFT只用于频谱显示,可以"淘汰"幅值较小的而减少浮点乘法运算. MINY的值 
  76.          * 和Spectrum.Y0,Spectrum.logY0对应. 
  77.          */  
  78.         sinv = MINY;  
  79.         cosv = -MINY;  
  80.         for (i = j; i != 0; i--) {  
  81.             tmpr = real[i];  
  82.             tmpi = imag[i];  
  83.             if (tmpr > cosv && tmpr < sinv && tmpi > cosv && tmpi < sinv)  
  84.                 realIO[i - 1] = 0;  
  85.             else  
  86.                 realIO[i - 1] = tmpr * tmpr + tmpi * tmpi;  
  87.         }  
  88.     }  
  89.   
  90. }  


 

3.频谱显示

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

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

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


 

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

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

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

  1. /* 
  2.  * Spectrum.java 
  3.  * 频谱显示 
  4.  * http://jmp123.sf.net/ 
  5.  */  
  6. import java.awt.Color;  
  7. import java.awt.Dimension;  
  8. import java.awt.GradientPaint;  
  9. import java.awt.Graphics;  
  10. import java.awt.Graphics2D;  
  11. import java.awt.image.BufferedImage;  
  12.   
  13. import javax.swing.JComponent;  
  14.   
  15. public class Spectrum extends JComponent implements Runnable {  
  16.     private static final long serialVersionUID = 1L;  
  17.     private static final int maxColums = 128;  
  18.     private static final int Y0 = 1 << ((FFT.FFT_N_LOG + 3) << 1);  
  19.     private static final double logY0 = Math.log10(Y0); //lg((8*FFT_N)^2)  
  20.     private int band;  
  21.     private int width, height;  
  22.     private int[] xplot, lastPeak, lastY;  
  23.     private int deltax;  
  24.     private long lastTimeMillis;  
  25.     private BufferedImage spectrumImage, barImage;  
  26.     private Graphics spectrumGraphics;  
  27.     private boolean isAlive;  
  28.   
  29.     public Spectrum() {  
  30.         isAlive = true;  
  31.         band = 64;      //64段  
  32.         width = 383;    //频谱窗口 383x124  
  33.         height = 124;  
  34.         lastTimeMillis = System.currentTimeMillis();  
  35.         xplot = new int[maxColums + 1];  
  36.         lastPeak = new int[maxColums];  
  37.         lastY = new int[maxColums];  
  38.         spectrumImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);  
  39.         spectrumGraphics = spectrumImage.getGraphics();  
  40.         setPreferredSize(new Dimension(width, height));  
  41.         setPlot();  
  42.         barImage = new BufferedImage(deltax - 1, height, BufferedImage.TYPE_3BYTE_BGR);  
  43.   
  44.         setColor(0x7f7f7f0xff00000xffff000x7f7fff);  
  45.     }  
  46.   
  47.     public void setColor(int rgbPeak, int rgbTop, int rgbMid, int rgbBot) {  
  48.         Color crPeak = new Color(rgbPeak);  
  49.         spectrumGraphics.setColor(crPeak);  
  50.   
  51.         spectrumGraphics.setColor(Color.gray);  
  52.         Graphics2D g = (Graphics2D)barImage.getGraphics();  
  53.         Color crTop = new Color(rgbTop);  
  54.         Color crMid = new Color(rgbMid);  
  55.         Color crBot = new Color(rgbBot);  
  56.         GradientPaint gp1 = new GradientPaint(00, crTop,deltax - 1,height/2,crMid);  
  57.         g.setPaint(gp1);  
  58.         g.fillRect(00, deltax - 1, height/2);  
  59.         GradientPaint gp2 = new GradientPaint(0, height/2, crMid,deltax - 1,height,crBot);  
  60.         g.setPaint(gp2);  
  61.         g.fillRect(0, height/2, deltax - 1, height);  
  62.         gp1 = gp2 = null;  
  63.         crPeak = crTop = crMid = crBot = null;  
  64.     }  
  65.   
  66.     private void setPlot() {  
  67.         deltax = (width - band + 1) / band + 1;  
  68.   
  69.         // 0-16kHz分划为band个频段,各频段宽度非线性划分。  
  70.         for (int i = 0; i <= band; i++) {  
  71.             xplot[i] = 0;  
  72.             xplot[i] = (int) (0.5 + Math.pow(FFT.FFT_N >> 1, (double) i   / band));  
  73.             if (i > 0 && xplot[i] <= xplot[i - 1])  
  74.                 xplot[i] = xplot[i - 1] + 1;  
  75.         }  
  76.     }  
  77.   
  78.     /** 
  79.      * 绘制"频率-幅值"直方图并显示到屏幕。 
  80.      * @param amp amp[0..FFT.FFT_N/2-1]为频谱"幅值"(用复数模的平方)。 
  81.      */  
  82.     private void drawHistogram(float[] amp) {  
  83.         spectrumGraphics.clearRect(00, width, height);  
  84.   
  85.         long t = System.currentTimeMillis();  
  86.         int speed = (int)(t - lastTimeMillis) / 30//峰值下落速度  
  87.         lastTimeMillis = t;  
  88.   
  89.         int i = 0, x = 0, y, xi, peaki, w = deltax - 1;  
  90.         float maxAmp;  
  91.         for (; i != band; i++, x += deltax) {  
  92.             // 查找当前频段的最大"幅值"  
  93.             maxAmp = 0; xi = xplot[i]; y = xplot[i + 1];  
  94.             for (; xi < y; xi++) {  
  95.                 if (amp[xi] > maxAmp)  
  96.                     maxAmp = amp[xi];  
  97.             }  
  98.   
  99.             /* 
  100.              * maxAmp转换为用对数表示的"分贝数"y: 
  101.              * y = (int) Math.sqrt(maxAmp); 
  102.              * y /= FFT.FFT_N; //幅值 
  103.              * y /= 8;  //调整 
  104.              * if(y > 0) y = (int)(Math.log10(y) * 20 * 2); 
  105.              *  
  106.              * 为了突出幅值y显示时强弱的"对比度",计算时作了调整。未作等响度修正。 
  107.              */  
  108.             y = (maxAmp > Y0) ? (int) ((Math.log10(maxAmp) - logY0) * 20) : 0;  
  109.   
  110.             // 使幅值匀速度下落  
  111.             lastY[i] -= speed << 2;  
  112.             if(y < lastY[i]) {  
  113.                 y = lastY[i];  
  114.                 if(y < 0) y = 0;  
  115.             }  
  116.             lastY[i] = y;  
  117.   
  118.             if(y >= lastPeak[i]) {  
  119.                 lastPeak[i] = y;  
  120.             } else {  
  121.                 // 使峰值匀速度下落  
  122.                 peaki = lastPeak[i] - speed;  
  123.                 if(peaki < 0)  
  124.                     peaki = 0;  
  125.                 lastPeak[i] = peaki;  
  126.                 peaki = height - peaki;  
  127.                 spectrumGraphics.drawLine(x, peaki, x + w - 1, peaki);  
  128.             }  
  129.   
  130.             // 画当前频段的直方图  
  131.             y = height - y;  
  132.             spectrumGraphics.drawImage(barImage, x, y, x+w, height, 0, y, w, height, null);  
  133.         }  
  134.   
  135.         // 刷新到屏幕  
  136.         repaint(00, width, height);  
  137.     }  
  138.   
  139.     public void paintComponent(Graphics g) {  
  140.         g.drawImage(spectrumImage, 00null);  
  141.     }  
  142.   
  143.     public void run() {  
  144.         WaveIn wi = new WaveIn();  
  145.         wi.open();  
  146.         wi.start();  
  147.   
  148.         FFT fft = new FFT();  
  149.         byte[] b = new byte[FFT.FFT_N << 1];  
  150.         float realIO[] = new float[FFT.FFT_N];  
  151.         int i, j;  
  152.   
  153.         try {  
  154.             while (isAlive) {  
  155.                 Thread.sleep(80);// 延时不准确,这不重要  
  156.   
  157.                 // 从混音器录制数据并转换为short类型的PCM  
  158.                 wi.read(b, FFT.FFT_N << 1);  
  159.                 //wi.getWave264(b, FFT.FFT_N << 1);//debug  
  160.                 for (i = j = 0; i != FFT.FFT_N; i++, j += 2)  
  161.                     realIO[i] = (b[j + 1] << 8) | (b[j] & 0xff); //signed short  
  162.   
  163.                 // 时域PCM数据变换到频域,取回频域幅值  
  164.                 fft.calculate(realIO);  
  165.   
  166.                 // 绘制  
  167.                 drawHistogram(realIO);  
  168.             }  
  169.   
  170.             wi.close();  
  171.         } catch (InterruptedException e) {  
  172.             // e.printStackTrace();  
  173.         }  
  174.     }  
  175.   
  176.     public void stop() {  
  177.         isAlive = false;  
  178.     }  
  179. }  


 

4.测试

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

  1. import javax.swing.JFrame;  
  2.   
  3. public class SpectrumTest {  
  4.     public static void main(String[] args) {  
  5.         JFrame frame = new JFrame();  
  6.         final Spectrum spec = new Spectrum();  
  7.         frame.getContentPane().add(spec);  
  8.         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  
  9.         frame.setTitle("Audio Spectrum");  
  10.         frame.setResizable(false);  
  11.         frame.pack();  
  12.         //frame.setAlwaysOnTop(true);  
  13.         frame.setVisible(true);  
  14.         //com.sun.awt.AWTUtilities.setWindowOpacity(frame, 0.8f);  
  15.         new Thread(spec).start();  
  16.     }  
  17.   
  18. }  


 

5.其它

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

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

http://jmp123.sf.net/

 

 

3
0
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值