JavaFX 音频可视化:从基础到实践
1. JavaFX 媒体播放基础
JavaFX 提供了一定的媒体播放支持。可以通过创建
Media
对象并传入指向媒体文件的 URI,再用该
Media
对象创建
MediaPlayer
对象来实现媒体播放。
MediaPlayer
类提供了诸如播放、暂停、重置等功能。示例代码如下:
Media media = new Media(mediaFileURI);
MediaPlayer mediaPlayer = new MediaPlayer(media);
mediaPlayer.play();
若媒体是视频,还需创建
MediaView
对象来在场景中显示视频。
MediaView
作为一个节点,可以进行平移、动画处理或应用效果。不过,JavaFX 并未提供用于启动和停止媒体的小部件,开发者需自行创建供用户点击的启动和停止节点。
此外,
javafx.scene.media
包中还有一些在这个简单示例中未使用的类,这些类能让开发者获取特定媒体的更多详细信息,特别是关于轨道的细节。
需要注意的是,由于 JavaFX 的一个 bug,无法像读取图像那样从 JAR 文件中读取电影文件。解决办法是将电影文件复制到本地硬盘的某个位置,并相应地更改 URI。虽然 JavaFX 有不错的媒体支持且 API 易于使用,但它无法以编程方式访问媒体内容,因此需要借助 Java Sound API 来从音频文件中获取所需数据。
2. Java 中的音频播放 API 选择
JavaFX 平台运行在 Java 平台之上,这意味着 JVM 提供的所有功能以及数千个用 Java 编写的库都可用于 JavaFX 应用程序。在开发桌面 JavaFX 应用程序时,至少有四种 API 可用于音频播放:
- JavaFX 媒体类
- Java 媒体框架(JMF)API
- AudioClip API
- Java Sound
这些 API 似乎支持不同格式的音乐文件,Java 的编解码器支持较为复杂。在示例中使用的是 MP3 文件,不过并非所有 MP3 文件都能与 Java Sound 正常配合使用。
各 API 之间存在差异。JMF 是一个强大且复杂的工具,可处理任何类型的媒体,但 Java Sound 的 API 更现代、更简单,更适合作为示例代码。而 AudioClip 类属于 Applet API,仅提供最基本的功能,不适合本需求。
3. 使用 Java Sound API 的准备工作
要使用 Java Sound API,需在代码中完成以下操作:
- 准备音频文件以供播放
- 对歌曲进行缓冲
- 创建一个读取和写入音频数据的线程
- 编写代码在播放音频时分析音频数据
涉及到三个线程:
-
音频线程
:从音频源读取数据,并使用 Java Sound 通过扬声器播放。
-
累积线程
:在音频播放时对声音数据进行采样,并简化数据,使其更适用于应用程序。通过 Observable/Observer 模式通知 JavaFX 线程数据有变化。
-
JavaFX 渲染线程
:负责绘制场景,在创建任何 JavaFX 应用程序时隐式定义。
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(音频线程):::process --> B(扬声器):::process
C(累积线程):::process --> D(简化音频数据):::process
D --> E(JavaFX 线程):::process
E --> F(更新场景):::process
4. 音频文件的准备
示例代码中提供了一个 WAV 文件,同时使用的 MP3 文件位于
org/lj/jfxe/chapter9/media
文件夹中。由于 Java Sound 不能直接从 JAR 文件中播放声音文件,需要将文件从 JAR 复制到本地文件系统。
以下是
SoundHelper
类的部分代码:
public class SoundHelper extends Observable implements SignalProcessorListener {
private URL url = null;
private SourceDataLine line = null;
private AudioFormat decodedFormat = null;
private AudioDataConsumer audioConsumer = null;
private ByteArrayInputStream decodedAudio;
private int chunkCount;
private int currentChunk;
private boolean isPlaying = false;
private Thread thread = null;
private int bytesPerChunk = 4096;
private float volume = 1.0f;
public SoundHelper(String urlStr) {
try {
if (urlStr.startsWith("jar:")) {
this.url = createLocalFile(urlStr);
} else {
this.url = new URL(urlStr);
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
init();
}
private File getMusicDir() {
File userHomeDir = new File(System.getProperties().getProperty("user.home"));
File synethcDir = new File(userHomeDir, ".chapter9_music_cache");
File musicDir = new File(synethcDir, "music");
if (!musicDir.exists()) {
musicDir.mkdirs();
}
return musicDir;
}
private URL createLocalFile(String urlStr) throws Exception {
File musicDir = getMusicDir();
String fileName = urlStr.substring(urlStr.lastIndexOf('/')).replace("%20", " ");
File musicFile = new File(musicDir, fileName);
if (!musicFile.exists()) {
InputStream is = new URL(urlStr).openStream();
FileOutputStream fos = new FileOutputStream(musicFile);
byte[] buffer = new byte[512];
int nBytesRead = 0;
while ((nBytesRead = is.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, nBytesRead);
}
fos.close();
}
return musicFile.toURL();
}
private void init() {
fft = new FFT(saFFTSampleSize);
old_FFT = new float[saFFTSampleSize];
saMultiplier = (saFFTSampleSize / 2) / saBands;
AudioInputStream in = null;
try {
in = AudioSystem.getAudioInputStream(url.openStream());
AudioFormat baseFormat = in.getFormat();
decodedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
baseFormat.getSampleRate(), 16, baseFormat.getChannels(),
baseFormat.getChannels() * 2,
baseFormat.getSampleRate(), false);
AudioInputStream decodedInputStream = AudioSystem.getAudioInputStream(decodedFormat, in);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
chunkCount = 0;
byte[] data = new byte[bytesPerChunk];
int bytesRead = 0;
while ((bytesRead = decodedInputStream.read(data, 0, data.length)) != -1) {
chunkCount++;
baos.write(data, 0, bytesRead);
}
decodedInputStream.close();
decodedAudio = new ByteArrayInputStream(baos.toByteArray());
DataLine.Info info = new DataLine.Info(SourceDataLine.class, decodedFormat);
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(decodedFormat);
line.start();
audioConsumer = new AudioDataConsumer(bytesPerChunk, 10);
audioConsumer.start(line);
audioConsumer.add(this);
isPlaying = false;
thread = new Thread(new SoundRunnable());
thread.start();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
若提供的 URL 以
jar
开头,会调用
createLocalFile
方法将声音文件从 JAR 复制到本地文件系统。
init
方法使用
AudioSystem
类的
getAudioInputStream
方法对音频文件进行解码,将音频数据存储在
decodedAudio
中,最终目的是将整个歌曲解码并存储在内存中,以便随时播放、停止或从任意点开始播放。
5. 音频数据的处理
init
方法的最后使用
AudioSubsystem
类创建了一个
DataLine
对象,用于通过扬声器发出声音。
SoundRunnable
类在单独的线程中完成此操作:
private class SoundRunnable implements Runnable {
public void run() {
try {
byte[] data = new byte[bytesPerChunk];
byte[] dataToAudio = new byte[bytesPerChunk];
int nBytesRead;
while (true) {
if (isPlaying) {
while (isPlaying && (nBytesRead = decodedAudio.read(data, 0, data.length)) != -1) {
for (int i = 0; i < nBytesRead; i++) {
dataToAudio[i] = (byte) (data[i] * volume);
}
line.write(dataToAudio, 0, nBytesRead);
audioConsumer.writeAudioData(data);
currentChunk++;
}
}
Thread.sleep(10);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
SoundRunnable
类的
run
方法中有两个
while
循环。外层循环用于切换是否播放声音,内层循环从
decodedAudio
中读取数据块,并将其写入
line
和
audioConsumer
。
line
是实际发出声音的 Java Sound 对象,其
write
方法会阻塞,直到准备好接收更多数据,从而使循环与听到的声音同步。
audioConsumer
负责进行数字信号处理。
数字信号处理类会将音频数据分解为 20 个值,每个值代表音频中特定频率的声音贡献程度,这些值存储在
SoundHelper
类的
levels
变量中,该变量是一个包含 20 个双精度浮点数的数组,值在 0.0 到 1.0 之间。
6. JavaFX 与 Java 的交互
SoundHelper
类使我们能够播放音频文件并获取音频播放时各频率的高低信息。接下来,需要将此功能暴露给 JavaFX 应用程序。在创建桥接 JavaFX 和 Java 两个环境的应用程序时,建议使用 Observer/Observable 模式。
Java 提供的
java.lang.Observable
类实现了多个方法,我们关注的是
addObserver
、
setChanged
和
notifyObservers
。当 Observable 的数据发生变化时,
addObserver
方法用于添加应被通知的 Observer。Observable 通过调用
setChanged
和
notifyObservers
方法通知 Observer,这会触发 Observer 接口的
update
方法。
SoundPlayer
类实现了
Observer
接口,用于包装
SoundHelper
并为需要其功能的应用程序提供 JavaFX 风格的接口:
public class SoundPlayer extends Observer{
public var volume:Number = 1.0 on replace {
soundHelper.setVolume(volume);
}
public var currentTime:Duration;
public var songDuration:Duration;
public var url:String;
public var file:File;
var soundHelper:SoundHelper;
override function update(observable: Observable, arg: Object) {
FX.deferAction(
function(): Void {
for (i in [0..(soundHelper.levels.length-1)]){
levels[i] = soundHelper.getLevel(i);
}
currentTime =
(soundHelper.getCurrentChunk()*1.0/soundHelper.getChunkCount()*1.0)*soundHelper.getSongLengt
hInSeconds()*1s;
}
);
}
//20 channels
public var levels: Number[] = for (i in [1..20]) 0.0;
public var hiChannels:Number = bind levels[19] + levels[18] + levels[17] + levels[16] +
levels[15] + levels[14] + levels[13];
public var midChannels:Number = bind levels[7] + levels[8] + levels[9] + levels[10] +
levels[11] + levels[12];
public var lowChannels:Number = bind levels[0] + levels[1] + levels[2] + levels[3] +
levels[4] + levels[5] + levels[6];
init{
soundHelper = new SoundHelper(url);
soundHelper.addObserver(this);
songDuration = soundHelper.getSongLengthInSeconds() * 1s;
soundHelper.setVolume(volume);
reset();
}
public function reset():Void{
soundHelper.pause();
soundHelper.setTimeInMills(0);
}
public function stop():Void{
soundHelper.pause();
}
public function pause():Void{
soundHelper.pause();
}
public function play():Void{
soundHelper.play();
}
public function setTime(time:Duration){
soundHelper.setTimeInMills(time.toMillis());
}
public function isPlaying():Boolean{
return soundHelper.isPlaying();
}
}
在
init
函数中,
SoundPlayer
创建一个新的
SoundHelper
并将自己注册为观察者。当
SoundHelper
中的
levels
发生变化时,
SoundPlayer
的
update
函数会被调用。
update
函数将
SoundHelper
中的
levels
复制到
SoundPlayer
的
levels
序列中,且复制操作在传递给
FX.deferAction
的函数中执行,这确保了其他 JavaFX 对象能可靠地绑定到
levels
序列。
此外,
SoundPlayer
还有一些变量(如
hiChannels
、
midChannels
和
lowChannels
)绑定到
levels
,这些变量是
levels
值的聚合结果,后续可用于音频可视化,使其仅绑定到歌曲的高音、中音或低音部分。
7. 音频可视化示例
现在有了一个不错的 JavaFX 接口用于声音处理代码,可以在示例应用程序中使用
SoundPlayer
来展示在 JavaFX 中创建引人注目的音频可视化是多么容易。示例应用程序的场景包含用于启动和暂停音乐的控件、可更改歌曲播放位置的控制条,以及三个复选框,用于控制三种示例效果的显示。
以下是
Main.fx
的代码:
var soundPlayer = SoundPlayer{
url: "{__DIR__}media/01 One Sound.mp3";
}
var bars = Bars{
translateX: 50
translateY: 400
soundPlayer:soundPlayer
visible: bind barsButton.selected
}
var barsButton = CheckBox{
graphic: Label{text: "Show Bars", textFill: Color.WHITESMOKE}
}
var disco = DiscoStar{
translateX: 320
translateY: 240
soundPlayer:soundPlayer
visible: bind discoButton.selected
}
var discoButton = CheckBox{
graphic: Label{text: "Show Disco", textFill: Color.WHITESMOKE}
}
var wave = Wave{
translateX: 620
translateY: 380
soundPlayer:soundPlayer
visible: bind waveButton.selected
}
var waveButton = CheckBox{
graphic: Label{text: "Show Wave", textFill: Color.WHITESMOKE}
}
var scene = Scene {
fill: Color.BLACK
content: [
SoundControl{
translateX: 30
translateY: 30
soundPlayer:soundPlayer
}, wave, disco, bars
]
}
function run():Void{
var vbox = VBox{
translateX: 500
translateY: 50
content: [barsButton, discoButton, waveButton]
}
insert vbox into scene.content;
Stage {
title: "Chapter 9"
width: 640
height: 480
scene: scene
} barsButton.selected = true;
}
通过上述代码,我们可以看到如何利用
SoundPlayer
以及各种控件和效果类来创建一个完整的音频可视化应用程序。用户可以通过复选框控制不同效果的显示,为音频播放增添丰富的视觉体验。
综上所述,通过 JavaFX 和 Java Sound API 的结合,我们能够实现音频的播放、处理以及可视化,为用户带来更加生动的音频体验。在实际开发中,开发者可以根据具体需求对代码进行扩展和优化,创造出更多有趣的音频可视化效果。
JavaFX 音频可视化:从基础到实践
8. 音频可视化效果的实现原理
在前面的示例中,我们看到了
Bars
、
DiscoStar
和
Wave
等效果类,这些类是实现音频可视化的关键部分。下面我们来分析一下这些效果是如何与音频数据进行关联并实现动态可视化的。
以
Bars
效果为例,它的高度或长度通常会根据音频的不同频率级别进行动态变化。在
SoundPlayer
类中,我们已经将音频的频率级别信息存储在
levels
序列中,
Bars
效果可以通过绑定到
levels
序列中的特定值来实现动态变化。
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(SoundPlayer):::process --> B(levels 序列):::process
B --> C(Bars 效果):::process
C --> D(动态变化):::process
同样,
DiscoStar
效果可能会根据音频的节奏闪烁或改变颜色,
Wave
效果可能会根据音频的波动呈现出波浪形状的变化。这些效果的实现都依赖于
SoundPlayer
提供的音频数据。
9. 扩展音频可视化效果
开发者可以根据自己的创意和需求扩展音频可视化效果。以下是一些扩展思路:
- 自定义形状 :创建自定义的形状类,如圆形、三角形等,根据音频数据改变其大小、颜色或透明度。
- 动画效果 :为可视化元素添加动画效果,如旋转、缩放、平移等,增强视觉效果。
- 多通道可视化 :除了高音、中音和低音通道,还可以对更多的频率通道进行可视化,展示更详细的音频信息。
例如,我们可以创建一个自定义的
CircleVisualizer
类,根据音频的某个频率级别改变圆形的半径:
public class CircleVisualizer extends Node {
private Circle circle;
private SoundPlayer soundPlayer;
public CircleVisualizer(SoundPlayer soundPlayer) {
this.soundPlayer = soundPlayer;
circle = new Circle(50);
circle.setFill(Color.BLUE);
getChildren().add(circle);
// 绑定圆形半径到音频级别
circle.radiusProperty().bind(soundPlayer.levels[5].multiply(100));
}
}
然后在
Main.fx
中添加这个自定义的可视化效果:
var circleVisualizer = CircleVisualizer{
translateX: 400
translateY: 300
soundPlayer:soundPlayer
}
var scene = Scene {
fill: Color.BLACK
content: [
SoundControl{
translateX: 30
translateY: 30
soundPlayer:soundPlayer
}, wave, disco, bars, circleVisualizer
]
}
10. 性能优化
在实现音频可视化时,性能是一个需要考虑的重要因素。以下是一些性能优化的建议:
| 优化策略 | 说明 |
|---|---|
| 减少不必要的计算 | 在处理音频数据时,避免进行不必要的计算,只处理必要的频率通道。 |
| 批量更新 | 尽量批量更新可视化元素的属性,减少频繁的重绘操作。 |
| 线程管理 | 合理管理线程,避免创建过多的线程,防止资源耗尽。 |
例如,在
SoundRunnable
类中,可以通过减少不必要的循环和计算来提高性能:
private class SoundRunnable implements Runnable {
public void run() {
try {
byte[] data = new byte[bytesPerChunk];
byte[] dataToAudio = new byte[bytesPerChunk];
int nBytesRead;
while (true) {
if (isPlaying) {
while (isPlaying && (nBytesRead = decodedAudio.read(data, 0, data.length)) != -1) {
// 减少不必要的计算
for (int i = 0; i < nBytesRead; i++) {
dataToAudio[i] = (byte) (data[i] * volume);
}
line.write(dataToAudio, 0, nBytesRead);
audioConsumer.writeAudioData(data);
currentChunk++;
}
}
Thread.sleep(10);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
11. 兼容性问题
在使用 JavaFX 和 Java Sound API 时,可能会遇到一些兼容性问题。例如,不同版本的 JavaFX 和 Java 可能会对音频文件的支持有所不同,某些编解码器可能无法正常工作。
为了避免兼容性问题,建议:
- 使用稳定版本的 JavaFX 和 Java。
- 测试不同格式的音频文件,确保应用程序在各种情况下都能正常工作。
- 关注官方文档和社区论坛,及时了解和解决兼容性问题。
12. 总结
通过结合 JavaFX 和 Java Sound API,我们可以实现强大而有趣的音频可视化效果。整个过程包括准备音频文件、处理音频数据、在 JavaFX 和 Java 之间进行交互以及创建可视化效果。
在实际开发中,开发者可以根据自己的需求选择合适的 API,扩展可视化效果,并进行性能优化和兼容性测试。希望本文能为你在音频可视化领域的开发提供一些帮助和灵感,让你能够创造出更加精彩的音频可视化应用程序。
超级会员免费看
65

被折叠的 条评论
为什么被折叠?



