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

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

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 plaincopy
  1. realIO[i] = (b[j + 1] << 8) | (b[j] & 0xff);

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

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

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

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

4.测试

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

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

5.其它

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

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

http://jmp123.sf.net/

3
0

用JAVA编写MP3解码器——GUI相关推荐

  1. (附)用JAVA编写MP3解码器——GUI

    2019独角兽企业重金招聘Python工程师标准>>> 以下代码是开源(GPL)程序jmp123的一部分. (一)简单的GUI 在jmp123.jar所在目录为当前目录启动jmp12 ...

  2. (十八)用JAVA编写MP3解码器——迷你播放器

    2019独角兽企业重金招聘Python工程师标准>>> 1.定义解码一帧的接口   ILayer123 Layer1.Layer2和Layer3这三个类都实现了ILayer123的d ...

  3. (十七)用JAVA编写MP3解码器——解码Layer1

    2019独角兽企业重金招聘Python工程师标准>>> Layer1的编码更简单,解码端的代码也就比Layer2还简单不少.网络上还有部分老歌是采用Layer2压缩的,但Layer1 ...

  4. 用JAVA编写MP3解码器

    [内容提要] <用JAVA编写MP3解码器>系列文章将对JAVA实现MP3解码的技术细节作介绍,本着开源的精神,文中给出完整的MPEG 1.0/2.0/2.5 Audio Layer I/ ...

  5. java做mp3_用java编写MP3播放器

    展开全部 作业其实还是自己写的好.要用到JMF包啊,到网上下载一个JMF包,照着说明安装上. 以下是我写e68a84e8a2ad62616964757a686964616f31333332643336 ...

  6. Java播放MP3——JLayer

    目录 1 简介 2 下载 3 事件与监听器 4 播放器 4.1  Player播放器 4.2  jlp极简播放器 4.3  jlap简易播放器 4.4  AdvancedPlayer高级播放器 1 简 ...

  7. 使用Java播放MP3或Wav音频

    JavaSound是一个小巧的低层应用程序接口(API),它支持数字音频和乐器数字接口(MIDI)数据的记录和回放.在JDK 1.3.0之前,JavaSound是一个标准的Java扩展API,但从Ja ...

  8. Java程序员必备 : Java反编译神器——“GUI” 资源分享

    GUI简介 当我们编写完成一个java类后,运行结果会得到一个class文件,这种二进制文件如果用普通记事本打开,就会出现各种乱码现象,令人十分头疼,当我们要查看.class文件的源代码时,可以通过反 ...

  9. Java SE 6之GUI:让界面更加绚丽(下)

    在上一篇中我介绍了Java SE 6在GUI上的部分改进.在这篇文章中我接着介绍另外几种新的GUI功能.这些功能是: 1.  带有排序和过滤功能的JTable. 2.  增强的JTabbedPane组 ...

最新文章

  1. Oracle 触发器 Update 不能操作本表的疑问
  2. 腾讯计费:助力游戏千亿级营收,覆盖180多个国家
  3. 自动运维_无Agent自动化运维平台spug
  4. JavaScript神奇的魔法代码
  5. 原生JS实现苹果菜单
  6. POJ 3461Oulipo KMP模板
  7. 如何把SQLServer数据库从高版本降级到低版本?
  8. pure-ftpd 配置
  9. 皮尔逊相关系数php,若两变量X和y之间的Pearson相关系数大于0.3且小于0.5,则说明()。...
  10. PX4固件飞行日志的采集设置以及数据读取
  11. 工作也能用 Tinder 配对?一家叫 Palaround 的公司就在做这门生意
  12. sketchb必备快捷键大全,sketch如何自定义快捷键
  13. 足球数据API接口 - 【实时指数2】API调用示例代码
  14. InstructGPT
  15. 2020年国考行测错题集(省级)
  16. 明道云与阿里1688对接案例
  17. 使用SecureFX内容显示中文乱码问题
  18. Windows提权方法
  19. VBA,如何使用类msgbox的效果,但是让窗口过几秒自动关闭? (未完成)
  20. Android开源实战:简单好用、含历史搜索记录的智能搜索框

热门文章

  1. QQ《穿越火线 》快速下载
  2. 苹果怎么测是原装屏_实心的就是原装屏,空心的就是国产屏?
  3. 49.Android中各种Span的用法
  4. 感谢那些曾经帮助过我的老师
  5. google 地图-转自(http://logfei.blogspot.com/)
  6. 用手机蓝牙锁定计算机,不用第三方软件,利用手机蓝牙,实现电脑人离即锁屏-电脑蓝牙怎么使用...
  7. 尼尔机械纪元游乐园怎么去机器人村_《尼尔机械纪元》图文流程攻略 主线剧情全收集及支线任务图文攻略...
  8. Windows XP系统下载
  9. php早午晚问候语_php 输出上午好,下午好等问候语的方法教程
  10. 在不同的时间段在页面上显示不同图片和不同的问候语。