欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

KeyboardPianoV1.7.2 Debug(音频优化)

程序员文章站 2022-05-24 21:03:11
...


详细步骤

例行说明

  • V1.7.1 主要在 MyButton 中处理贴图优化,这个版本开始整顿 MusicPlayer

具体步骤

  • Task Manager => 内存猛增 => JConsole => 堆内存泄露 => Memory Analyzer => 分析原因
  • 优化的过程:Clip 播放 + sleep 释放资源 => 同步释放 => 更改播放方式

    详细见优化过程的代码分析

内存泄露排查过程 请见另一篇博客 java 堆内存泄露排查(例子)
这里主要说明 代码优化的过程


优化过程

  • 泄露源码
    1. 内存随着按键的增加线性增长
    2. 多重按键后,内存飙倒 1000+ M,且只增不减
    3. 该代码块来源于 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();
  • 部分优化
    1. 使用 synchronized + waitclip 进行同步处理,音频播放完成即立即 close,省去统一 sleep 的麻烦
    2. 多重按键后内存稳定在 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();
}
  • 进一步优化

    1. 使用 SourceDataLine 替换 Clip 作为播放器
    2. 多重按键后,内存稳定在 230+ M
  • 代码分析

    1. 通过音频路径 musicPath 音频数据流 AudioInputStream
    2. 根据音频中获取信息 dataLine.Info,使用播放器 SourceDataLine 加载这些信息 getInfo(), 打开数据流 open() 并开启播放器 start() (类似线程启动)
    3. while 循环内将音频数据读进缓冲区,再写到 audioData[]
      这里 read 可以理解为通过音频输入流 AudioInputStream 把音频数据加载进来
      然后再通过播放器 SourceDataLine 把数据写出去 write(播放出来)
    4. 最后把建立的资源通道全部关闭,关闭顺序说明见 相关链接
/*
 * ~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 导致内存线性增加。然而事实证明并没有
    1. paintComponent() 只有在组件改变的时候才会重画,也就是说按钮按下抬起,只做两次重画处理,也就是说按钮按紧不放,也只产生一段音频而已
    2. 所以音频播放放置在 paintComponent() 是个正确的选择

相关链接

  • Java Sound API (译文)

    1. 播放音频文件 原理介绍
    2. SourceDataLine 详细介绍
  • IO Stream 手动释放资源

    1. 先开后关原则(栈原则, FILO)
    2. 由外到内原则(洋葱原则,%>_<%)
  • flush() 作用

    1. 缓冲区就像水泵,数据流就好比是水管,直接 close() 可能会导致水管中残留水分
    2. 使用 flush 就是强制把水管中数据输出,这样 close 就可以安全关掉水闸

后记

  • 博主在查阅资料的过程发现容易被大量的资料冲昏头脑,导致偏离了航向,后期总结得出以下结论
    1. 先把目前的工作完成,再做拓展学习
    2. 查询资料过程遇到一些新的/拓展补充的东西,先记录链接,后期再细察/系统学习,这样才不会顾此失彼