KeyboardPianoV1.7.2 Debug(音频优化)
程序员文章站
2022-05-24 21:03:11
...
详细步骤
例行说明
-
V1.7.1
主要在MyButton
中处理贴图优化,这个版本开始整顿MusicPlayer
具体步骤
- Task Manager => 内存猛增 => JConsole => 堆内存泄露 => Memory Analyzer => 分析原因
- 优化的过程:Clip 播放 + sleep 释放资源 => 同步释放 => 更改播放方式
详细见优化过程的代码分析
内存泄露排查过程 请见另一篇博客 java 堆内存泄露排查(例子)
这里主要说明 代码优化的过程
优化过程
-
泄露源码
- 内存随着按键的增加线性增长
- 多重按键后,内存飙倒 1000+ M,且只增不减
- 该代码块来源于
V1.6
, 详见 代码分析,这里不再赘述
buffer = new BufferedInputStream(new FileInputStream(musicPath));
audioInputStream = AudioSystem.getAudioInputStream(buffer);
clip = AudioSystem.getClip();
clip.open(audioInputStream);
FloatControl gainControl =
(FloatControl)clip.getControl(FloatControl.Type.MASTER_GAIN);
if(musicType == KeyboardPiano.L) {
gainControl.setValue(LEFT_VALUE); //set +- dB
} else if(musicType == KeyboardPiano.R) {
gainControl.setValue(RIGHT_VALUE);
}
clip.start();
//sleep(7000); then clip.close();
-
部分优化
- 使用
synchronized + wait
对clip
进行同步处理,音频播放完成即立即close
,省去统一sleep
的麻烦 - 多重按键后内存稳定在 500+ M,后期缓慢上升
- 使用
/*
* ~500M there are no sounds sometimes when multiple-type, **Busy Thread**
* but synchronized(clip) and wait is a pretty smart way
* rather than sleep for a while then close
*/
AudioInputStream inputStream = AudioSystem.getAudioInputStream(new File(musicPath));
DataLine.Info info = new DataLine.Info( Clip.class, inputStream.getFormat());
final Clip clip = (Clip) AudioSystem.getLine(info);
clip.addLineListener(new LineListener() {
public void update(LineEvent e) {
if (e.getType() == LineEvent.Type.STOP) {
synchronized(clip) {
clip.notify();
}
}
}
});
clip.open(inputStream);
clip.setFramePosition(0);
clip.start();
synchronized (clip) {
clip.wait();
}
if(clip.isOpen()) {
clip.drain();
clip.close();
}
-
进一步优化
- 使用
SourceDataLine
替换Clip
作为播放器 - 多重按键后,内存稳定在 230+ M
- 使用
-
代码分析
- 通过音频路径
musicPath
音频数据流AudioInputStream
- 根据音频中获取信息
dataLine.Info
,使用播放器SourceDataLine
加载这些信息getInfo()
, 打开数据流open()
并开启播放器start()
(类似线程启动) -
while
循环内将音频数据读进缓冲区,再写到audioData[]
中
这里read
可以理解为通过音频输入流AudioInputStream
把音频数据加载进来
然后再通过播放器SourceDataLine
把数据写出去write
(播放出来) - 最后把建立的资源通道全部关闭,关闭顺序说明见 相关链接
- 通过音频路径
/*
* ~200M this one is so close to perfect
*/
@Override
public void run() {
AudioInputStream ais = null;
SourceDataLine line = null;
try{
ais = AudioSystem.getAudioInputStream(new File(musicPath));
/*
* PCM_SIGNED 44100.0 Hz, 16 bit, mono, 2 bytes/frame, little-endian
*/
AudioFormat audioFormat = ais.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
line = (SourceDataLine)AudioSystem.getLine(info);
line.open(audioFormat);
line.start();
int BUFFER_SIZE = 1024;
int intBytes=0;
byte[] audioData=new byte[BUFFER_SIZE];
while(intBytes != -1)
{
intBytes = ais.read(audioData, 0, BUFFER_SIZE);
if(intBytes >= 0)
line.write(audioData, 0, intBytes);
}
if(line != null) {
line.flush();
line.close();
line = null;
}
if(ais != null) {
ais.close();
ais = null;
}
}
}
以上是对音频播放的内存优化过程,虽然最终能把内存压下来并稳住,不过增加的量还是不容小瞧,若有思路,博主将做进一步的优化
- 主要参考链接: java播放音频文件 (CSDN) 里面介绍了 6 种播放方式(包括 MIDI)
-
flush()
的作用以及数据通道关闭的顺序请见 相关链接
Option
- 之前测试的时候,博主猜想是不是因为 按钮按紧,组件可能会一直重画,不断 new
MusicPlayer
导致内存线性增加。然而事实证明并没有-
paintComponent()
只有在组件改变的时候才会重画,也就是说按钮按下抬起,只做两次重画处理,也就是说按钮按紧不放,也只产生一段音频而已 - 所以音频播放放置在
paintComponent()
是个正确的选择
-
相关链接
-
Java Sound API (译文)
- 播放音频文件 原理介绍
- SourceDataLine 详细介绍
-
- 先开后关原则(栈原则, FILO)
- 由外到内原则(洋葱原则,%>_<%)
-
flush() 作用
- 缓冲区就像水泵,数据流就好比是水管,直接 close() 可能会导致水管中残留水分
- 使用
flush
就是强制把水管中数据输出,这样close
就可以安全关掉水闸
后记
- 博主在查阅资料的过程发现容易被大量的资料冲昏头脑,导致偏离了航向,后期总结得出以下结论
- 先把目前的工作完成,再做拓展学习
- 查询资料过程遇到一些新的/拓展补充的东西,先记录链接,后期再细察/系统学习,这样才不会顾此失彼