以下代码是开源(GPL)程序jmp123的一部分。
(一)简单的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数据,这样占用的资源少而且速度快。
/* * 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后取整为零,干脆先将这样的值置零。
/* * 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方法里的这一语句:
realIO[i] = (b[j + 1] << 8) | (b[j] & 0xff);
从混音器捕获到的数据是byte类型,需要转换为PCM的16位符号整数,高字节b[j+1]的符号确定了PCM数据的符号。JAVA的数据类型转换那是相当的麻烦,好在频谱显示不是真正意义上的“实时”的,所以尽管要进行FFT等这样大量运算,采用延时一段时间抽取数据出来分析使得整体的运算量不大。
(3).绘制“频率-幅值”直方图。采用内存作图,绘制好一帧后刷到屏幕上去。 直方图中柱体的长度代表该频段的幅值,这个幅值用对数量化,据说人耳朵对音频幅值(能量)的感知也是非线性的,呈对数函数特性的非线性。另外,本来应该对高频段的柱体长度作等响度修正,这样呈现在屏幕上的频谱直方图看起来才符合我们感知到的音乐,可是等响度修正系数没找到免费的供我们用用,人家申请得有专利,要¥或$或那个什么来着,那就算啦。
(4).对经过FFT后得到的频域数据作怎样的处理使它呈现到屏幕上,并无定势,以上只是我的一个方法,你可以根据自己的喜好修改。
/* * 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一下下,说不过去。
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).以上代码实现了从音频输出捕获数据并显示其频谱直方图,直接从音频输出捕获数据的优点是与程序其它模块之间没有依赖性,缺点是资源占用较大,效率较低。内置在解码器里的频谱显示使程序模块之间耦合性增大,但运行效率高。我写了一个播放器,内置了频谱显示。下载地址:
相关推荐
用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作方法及源代码 1个目标文件 摘要:Java源码,初学实例,波浪文字 Java波浪文字,一个利用...
【Java课程设计——电子音乐盒】是一个以Java技术为基础,利用Java Media Framework(JMF)开发的音乐播放软件。这个项目旨在让学生掌握Java编程语言在多媒体应用中的实践,特别是音频处理和用户界面设计方面的能力...
用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作方法及源代码 1个目标文件 摘要:Java源码,初学实例,波浪文字 Java波浪文字,一个利用...
用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作方法及源代码 1个目标文件 摘要:Java源码,初学实例,波浪文字 Java波浪文字,一个利用...
Java编写的显示器显示模式检测程序 2个目标文件 内容索引:JAVA源码,系统相关,系统信息检测 用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作...
`bin`目录通常包含可执行文件和其他运行时需要的库,而`codec`目录可能包含了各种音频编解码器,用于支持播放不同格式的音乐文件。 总的来说,Java音乐播放器+网络收音机是一个充分利用Java平台优势的多媒体应用,...
用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作方法及源代码 1个目标文件 摘要:Java源码,初学实例,波浪文字 Java波浪文字,一个利用...
Java编写的显示器显示模式检测程序 2个目标文件 内容索引:JAVA源码,系统相关,系统信息检测 用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作...
Java编写的显示器显示模式检测程序 2个目标文件 内容索引:JAVA源码,系统相关,系统信息检测 用JAVA编写了一个小工具,用于检测当前显示器也就是显卡的显示模式,比如分辨率,色彩以及刷新频率等。 Java波浪文字制作...
首先,我们要了解Java在GUI(图形用户界面)设计中使用的核心库——Java Swing或JavaFX。在这个音乐播放器项目中,Swing很可能被用来创建界面元素,如按钮、播放/暂停控件、音量调节滑块等。Swing提供了丰富的组件库...
它以其“一次编写,到处运行”(Write Once, Run Anywhere, WORA)的特性而闻名,因为Java程序可以在支持Java虚拟机(JVM)的任何平台上运行。在本文中,我们将关注基于Java的音乐播放器——JMPlayer的开发。 1. **...
这个项目描述指出,开发的网页浏览器是用Java语言编写的,这意味着开发者可能使用了Java的标准库,例如JavaFX或Swing来构建用户界面,使用Socket或者HttpURLConnection来进行网络通信。开发者提到运行程序前需要将...
【Java实现雷霆战机简易程序】是一个使用Java编程语言开发的游戏项目,主要涵盖了游戏设计、图形渲染、音频处理等多方面的技术。在这个项目中,开发者利用Java的灵活性和强大的库支持,构建了一个简单的飞行射击游戏...
这些任务通常使用Java Swing库来实现,需要掌握JFrame、JButton、JTextArea等组件的使用,以及事件监听机制。 3. 文件操作:文件浏览器的实现要求考生熟悉Java的文件I/O操作,包括File类、BufferedReader和...
第2章“Android系统开发综述”,介绍Android系统开发的综述性内容,包括工具使用、获得代码、编译系统、仿真器运行、SDK使用等。 第3章“Android的Linux内核与驱动程序”,介绍Android内核的特点、Android中使用...
**JavaProject_SIC_Simulator** 是一个基于Java编程语言开发的电子学习工具,主要用于教育目的,特别是为了帮助安娜大学第五学期的学生理解系统软件课程中的核心概念——**简化指令计算机(SIC)**的工作原理。...
这些元素可以包括音频和视频解码器、编码器、转换器、网络传输模块等。GStreamer通过连接这些元素,实现了音频和视频数据的流式处理,支持多种格式的媒体文件。 1. **元素(Elements)**: GStreamer中的元素负责...
这个项目的名称直观地表明了它的功能——提供一个简单易用的媒体播放解决方案。在Java编程语言的环境下,开发者通常会利用JavaFX或者Swing等库来构建这样的用户界面应用。下面我们将深入探讨这个项目可能包含的关键...
4. **文件兼容性**:如何扩展支持其他音频格式,例如 AU 和 AIFF,可能涉及到使用第三方库或者自定义解码器。 5. **无限循环机制**:实现音乐无限循环播放的编程逻辑,可能涉及到线程控制和事件监听。 6. **跨平台...
- **解码器**:Java的JMF(Java Media Framework)或FFmpeg库提供了音频和视频的解码能力,LiveNetworkStreamPlayer可能利用这些库将接收到的网络流数据转换成可播放的格式。 - **播放器组件**:解码后的音视频...