java sound初探

网上关于java sound的正规资源讲解的非常好,本文不再给出示例,主要提供一些好的资源,并说说我的一些理解,用于形成对java sound的整体认识.

一.几个词汇

  • TTS:text-to-speech,文本到语音转换
  • OCR:optical-character-recignition光学字符识别
  • MIDI:Musical Instrument Digital Interface,乐器数字化接口

    MIDI是20世纪80年代初由Dave Smith提出的,目的是解决电声乐器之间的通信.现代音乐都是通过MIDI+音色库合成的.MIDI传输的不是声音信号而是一系列音符控制参数等指令,它告诉MIDI设备要做什么.MIDI传输的信号被统一成MIDIMessage,通过异步串行通信来传递.

  • Tritonus:java sound是一种标准,有两套实现.一套是Sun公司的,一套是Tritonus.在Java 1.3中,Sun公司的被纳入Java标准库.从那时起,Tritonus就很尴尬了.要想使用Tritonus就需要禁用掉Sun的,而禁用Sun的是一件多此一举的事情.Tritonus目前只支持Linux系统,但Tritonus的一些单独下载的插件也可以运行在其他系统上.
  • SPI:Service Provider Interface服务提供接口,这是API中常见的一种模式.把代码写成接口的形式,一些服务可以遵照这些接口来实现,从而实现可插拔式的编程.SPI还有另外一个意思:在电路中,SPI指串行外设接口,Serial Peripheral Interface,它是一种高速全双工,同步的通信总线.

  • Acousitic声学,Reverb回响,Gain增益,Pan声象.DAC(digital analog converter)数模转换器.

二.学习资源

jsresource

js指java sound,这个网站专门讲解java sound,包罗万象,堪称java sound的百科全书,有这一个网站就足够了,现在需要做的就是把这个网站从头看到尾.

oracle官网上的java sound介绍

官网一向都是最重要的文档提供者,在oracle官网上,有一个详尽强大的样例,它展示了java sound的各个方面.从这个页面上可以下载样例.这个样例特别好,竟然可以用来弹奏钢琴,充分展示了java sound的强大功能.

http://www.oracle.com/technetwork/java/javase/downloads/index.html

这个页面也是Oracle官网页面,是jdk下载页面.在这个页面中,有jdk的demo,doc的下载链接,这些都是学习java的上好资源,下载下来,仔细阅读javax.sound模块.

http://www.javazoom.net/index.shtml

java sound直接支持的音频格式非常少,只包括.wav(多见于windows)和.AIFF(多见于macintosh)和.au(多见于unix)三种格式的音频文件.但通过SPI,我们不须修改java代码,只需要提供相应格式的SPI就能够实现播放多种文件.javazoom网站提供了一套mp3解码库,名字叫做JLayer.
Java Zoom网站上有多个关于java音频的项目,这里主要介绍JLayer和MP3SPI和jlGUI.JLayer于1999年2月启动,目标是为Java提供实时的MP3解码器.它还包括JLayerME子项目,是JLayer在JavaME上的版本.MP3SPI是一个基于JLayer和Tritonus的Java插件,Tritonus是java sound标准的另一种实现,要想使用MP3SPI,需要三个jar包:mp3spi.jar和tritonus.jar和jlayer.jar,将这三个jar包放到类路径下,java sound便具备了播放MP3的能力.jlGUI是一个图形界面的音乐播放器,它纯粹用Java写成,依赖于MP3SPI,这个音乐播放器简洁简陋,用着还行.

http://www.sauronsoftware.it/projects/jave/manual.php

jave(java audio video encoder)是一个纯java版的音视频转码器.

三.概述

AudioSystem是javax.sound包的重要入口类,一切都是以它为中心展开的,AudioSystem的默认输入设备是麦克风,默认输出设备是扬声器
SourceDataLine和TargetDataLine都可以通过AudioSystem获得.SourceDataLine意思是"源数据流",是指AudioSystem的输入流,把音频文件写入到AudioSystem中,AudioSystem就会播放音频文件.TargetDataLine意思是"目标数据流",是指AudioSystem的输出流,是AudioSystem的target.所以,当播放文件时,把文件内容写入AudioSystem的SourceDataLine;当录音时,把AudioSystem的TargetDataLine中的内容读入内存.
Clip是"剪辑","片段",表示内存中的一段完整的音频数据,可以一遍一遍的播放,非常适合播放游戏的背景音乐.Clip和SourceDataLine都是AudioSystem的输入端口.
在java中处理声音的包括四个包:

  • javax.sound.sample处理数字音频
  • javax.sound.midi处理midi形式的音频
  • javax.sound.sample.spi相当于sample类型的服务提供接口
  • javax.sound.midi.spi相当于midi类型的服务提供接口

四.最简单的播放器

AudioInputStream cin = AudioSystem.getAudioInputStream(new File("haha.wav"));
AudioFormat format = cin.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
line.open(format);//或者line.open();format参数可有可无
line.start();
int nBytesRead = 0;
byte[] buffer = new byte[512];
while (true) {
    nBytesRead = cin.read(buffer, 0, buffer.length);
    if (nBytesRead <= 0)
        break;
    line.write(buffer, 0, nBytesRead);
}
line.drain();
line.close();

这个程序只能播放wav,pcm文件,不能播放mp3文件.
第一步,从文件对象构建AudioInputStream cin,这个cin对象包含文件的格式数据和音频数据.
第二步,根据cin的AudioFormat创建DataLine.Info对象.
第三步,根据AudioFormat来获取SourceDataLine,扬声器有了SourceDataLine就有了数据流,扬声器就可以发声了.

SourceDataLine像一个管道一样,数据流从计算机内部的音频文件中流到AudioSystem音频系统中,line.open()打开管道入口端,line.start()打开管道的出口端.line.drain()将管道的出口端导入另一个地方将管道中剩余的数据流放空.line.close()关住了管道的出口端.

五.最简单的录音机

File outputFile = new File("recoder.wav");
AudioFormat audioFormat = new AudioFormat(
        AudioFormat.Encoding.PCM_SIGNED, 44100.0F, 16, 2, 4, 44100.0F,
        false);
DataLine.Info info = new DataLine.Info(TargetDataLine.class,
        audioFormat);
TargetDataLine targetDataLine = (TargetDataLine) AudioSystem
        .getLine(info);
targetDataLine.open(audioFormat);
targetDataLine.start();
new Thread() {
    public void run() {
        AudioInputStream cin = new AudioInputStream(targetDataLine);
        try {
            AudioSystem.write(cin, AudioFileFormat.Type.WAVE,
                    outputFile);
            System.out.println("over");
        } catch (IOException e) {
            e.printStackTrace();
        }
    };
}.start();
System.in.read();
targetDataLine.close();

跟SourceDataLine的获取一样,要想从AudioSystem中获取TargetDataLine,需要规定好AudioFormat.
AudioInputStream有两个构造函数:

  • AudioInputStream(TargetDataLine line)
  • AudioInputStream(InputStream stream, AudioFormat format, long length)

重点说说第二个函数,InputStream stream指的是音频的数据流,AudioFormat format指的是音频的形式,length表示stream中包含多少个frame,计算方法是stream.length/frameSize,其中frameSize表示一个frame占用的字节数.frameSize=channelCount*sampleSize,一个frame记录的是当前时刻各个声道的音频值.采样率指的是一秒钟在某个声道的取样数,也就是一秒钟内的frame数.sampleRate=frameRate.

AudioSystem.write()有两个重载函数:

  • write(AudioInputStream stream,AudioFileFormat.Type fileType, File out):写入到文件
  • write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out):写入到OutputStream

AudioSystem.write()函数是线程阻塞的,只要AudioInputStream没有结束,就会一直等待输入.所以必须另开一个线程来录音,否则就无法关闭录音了.

六.使用Clip循环播放的小段音频

    public static void main(String[] args) throws Exception {
        Clip clip = AudioSystem.getClip();
        clip.open(AudioSystem.getAudioInputStream(new File("haha.wav")));
        clip.start();
        clip.setLoopPoints(0, clip.getFrameLength() - 1);
        while (true) {

        }
    }

AudioSystem就像一个芯片,它有三种引脚:SourceDataLine,TargetDataLine,Clip.这三个东西都是继承自DataLine接口的接口.SourceDataLine用于播放音频,TargetDataLine用于录音,Clip用于循环播放一段音频,可以设置循环次数等.
使用Clip播放音频时,会开启一个线程去播放音频,所以Clip是不阻塞的,这一点跟SourceDataLine不同.所以这个程序末尾写了一个死循环.
Clip的获取方式除了直接从AudioSystem.getClip(),AudioSystem.getClip(Info)直接获取,还可以像SourceDataLine,TargetDataLine那样获取.

AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(clipFile);
AudioFormat    format = audioInputStream.getFormat();
DataLine.Info    info = new DataLine.Info(Clip.class, format);
Clip clip = (Clip) AudioSystem.getLine(info);

七.播放MP3音频

播放非pcm格式的音频时,必须有对应的解码器将相应格式转化为pcm格式才能够播放.pcm格式有三种:PCM_FLOAT,PCM_SIGNED,PCM_UNSIGNED.
JLayer是一款MP3解码器,MP3SPI是基于JLayer和Tritonus的一款MP3 service provider interface.将jlayer.jar和mp3spi.jar和tritonus.jar三个jar包放到classpath中,解码时会自动查找相应的解码器进行解码.本示例程序依赖上述三个jar包.

        AudioInputStream stream = AudioSystem
                .getAudioInputStream(new File("haha.mp3"));
        AudioFormat format = stream.getFormat();
        if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
            format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                    format.getSampleRate(), 16, format.getChannels(),
                    format.getChannels() * 2, format.getSampleRate(), false);
            stream = AudioSystem.getAudioInputStream(format, stream);
        }
        DataLine.Info info = new DataLine.Info(SourceDataLine.class,
                stream.getFormat());
        SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem
                .getLine(info);
        sourceDataLine.open(stream.getFormat(), sourceDataLine.getBufferSize());
        sourceDataLine.start();
        int numRead = 0;
        byte[] buf = new byte[sourceDataLine.getBufferSize()];
        while ((numRead = stream.read(buf, 0, buf.length)) >= 0) {
            int offset = 0;
            while (offset < numRead) {
                offset += sourceDataLine.write(buf, offset, numRead - offset);
            }
            System.out.println(sourceDataLine.getFramePosition() + " "
                    + sourceDataLine.getMicrosecondPosition());
        }
        sourceDataLine.drain();
        sourceDataLine.stop();
        sourceDataLine.close();
        stream.close();

先获取原文件的AudioFormat,然后根据这个旧的AudioFormat创建一个新的AudioFormat,注意新的AudioFormat除了Encoding发生改变,sampleSizeInBits也要改成16,相应的frameSize也要发生变化.frameSize表示一个frame占用的字节数,frameSize=channelCount*sampleSizeInBytes,也就是声道数channelCount*2.
构建完了新的AudioFormat,通过AudioSystem.getAudioInputStream(format,stream)这个函数就能完成解码工作,这个函数会调用解码器进行解码.一旦解码完成,就会获得一个新的AudioInputStream,就可以像播放普通的wav文件一样进行播放了.
在上面使用byte[]buf数组进行读取的过程中使用了两重循环,为什么呢?第一重是必需的,里面的第二重是怕sourceData里面写不完buf,如果只写一次有可能造成数据丢失.
在上面播放过程中,不停地输出播放的frame数和播放的毫秒数,这是为了展示SourceDataLine的这两个函数.
转码的另一种方式是

AudioInputStream stream = AudioSystem.getAudioInputStream(new File("haha.mp3"));
stream = AudioSystem.getAudioInputStream(AudioFormat.Encoding.PCM_SIGNED, stream);

这种方式通过两句话就能把mp3转码成pcm,但是却报错缺少这样的转码器.所以还是按照上面说的那种方法来吧.
AudioSystem.getAudioInputStream()函数有5种形式:从文件中读取,从InputStream中读取,从URL中读取,从AudioInputStream中读取并将结果转换成某种编码,从AudioInputStream中读取并将结果转换成某种AudioFormat.

八.使用SourceDataLine进行循环播放

前面说过Clip非常适合进行循环播放,实际上SourceDataLine也是可以很方便进行循环播放的.实现的关键在于AudioInputStream的mark(int readLimit)和reset()两个函数.mark+reset是一种机制,在java流体系中很常见,它是这样一种机制:比如当前位置为第3个字节处,一旦调用mark(100),就表示在3处做了一个标记,然后继续往前走,比如,假设走到66处,执行了reset(),于是一下子就回到了第3个字节处;如果在66处没有执行reset()而是继续往前走,走到110处(已经超过了100步限制),那么刚才在3处的mark标记就失效了.不是所有的InputStream子类都支持mark机制,可以通过调用InputStream#markSurported()函数来检测当前流是否支持mark机制.BufferedInputStream和ByteArrayInputStream是支持mark机制的.对于readLimit,并不是所有的流在走完readLimit步之后都会将标记置成无效,而是在走完readLimit步之后执行reset仍旧回到上一个mark处,简言之,就是实际的readLimit为max(显式readLimit,该流在内存中的最大空间).

byte[]    abData = new byte[EXTERNAL_BUFFER_SIZE];
int    nBytesRead = 0;
int nPlayCount = 0;
if (audioInputStream.getFrameLength() == AudioSystem.NOT_SPECIFIED ||
    audioFormat.getFrameSize() == AudioSystem.NOT_SPECIFIED)
{
    out("cannot calculate length of AudioInputStream!");
    System.exit(1);
}
long lStreamLengthInBytes = audioInputStream.getFrameLength()
    * audioFormat.getFrameSize();
if (lStreamLengthInBytes > Integer.MAX_VALUE)
{
    out("length of AudioInputStream exceeds 2^31, cannot properly reset stream!");
    System.exit(1);
}
int nStreamLengthInBytes = (int) lStreamLengthInBytes;

line.start();

while (nPlayCount < PLAY_COUNT)
{
    nPlayCount++;
    audioInputStream.mark(nStreamLengthInBytes);
    nBytesRead = 0;
    while (nBytesRead != -1)
    {
        try
        {
            nBytesRead = audioInputStream.read(abData, 0, abData.length);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        if (nBytesRead >= 0)
        {
            int    nBytesWritten = line.write(abData, 0, nBytesRead);
        }
    }
    audioInputStream.reset();
}
line.drain();
line.close();

九.关于混音器mixer

SourceDataLine和Clip是AudioSystem的输入端,TargetDataLine是AudioSystem的输出端.AudioSystem就像一个黑盒子,那么盒子里面装的是啥?实际上就是混音器.当同时播放两首乐曲时,需要混音器来将多个SourceDataLine和多个Clip的音频数据进行混合.
查看系统中的全部混音器

    public static void listMixers() {
        Mixer.Info[] a = AudioSystem.getMixerInfo();
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i].getName());
        }
    }

并不是所有的mixer都支持三种类型的DataLine,实际上,输入混音器和输出混音器是分开的.有的混音器支持SourceDataLine和Clip,有的混音器支持TargetDataLine,有的混音器什么也不支持.通过AudioSystem这个类来获取三种DataLine就避免了程序员手动查看哪些混音器可用于输入,哪些混音器可用于输出.

        Mixer.Info[] a = AudioSystem.getMixerInfo();
        for (int i = 0; i < a.length; i++) {
            Mixer mixer = AudioSystem.getMixer(a[i]);
            Line.Info[] b = {new Line.Info(SourceDataLine.class),
                    new Line.Info(TargetDataLine.class),
                    new Line.Info(Clip.class)};
            int ans = 0;
            for (int j = 0; j < b.length; j++) {
                if (mixer.isLineSupported(b[j])) {
                    ans |= (1 << j);
                }
            }
            System.out.println(a[i].getName() + " " + ans);
        }

要想获得mixer,就要使用AudioSystem#getMixer(Mixer.Info)函数.要想获得DataLine,就要通过Mixer#getLine(Line.Info)函数.这个过程需要检测mixer支持DataLine的情况,这是挺费事的.幸好通过AudioSystem#getLine(Line.Info)函数可以直接获得想要的DataLine,这个函数把底层封装了一下.

查看支持的文件类型

    public static void listSupportedTypes() {
        AudioFileFormat.Type[] aTypes = AudioSystem.getAudioFileTypes();
        for (int i = 0; i < aTypes.length; i++) {
            System.out.println(aTypes[i].getExtension());
        }
    }

十.查看文件元数据

查看文件元数据主要通过三个类:AudioFormat,AudioFileFormat,AudioInputStream.

File file = new File("haha.wav");
AudioFileFormat aff = AudioSystem.getAudioFileFormat(file);
AudioInputStream ais = AudioSystem.getAudioInputStream(file);
//AudioFormat既可以通过AudioInputStream获取,也可以通过AudioFileFormat获取.
AudioFormat af = ais.getFormat();// aff.getFormat()
out("---------AudioFileFormat---------");
out("Type " + aff.getType());
out("byteLength " + aff.getByteLength());
out("frame length " + aff.getFrameLength());
out("format " + aff.getFormat());
out("properties " + aff.properties());
out("--------AudioFormat----------");
out("encoding " + af.getEncoding());
out("channels " + af.getChannels());
out("sampleRate " + af.getSampleRate());
out("frameRate " + af.getFrameRate());
out("properties " + af.properties());
out("sampleSizeInBits " + af.getSampleSizeInBits());
out("frameSize " + af.getFrameSize());
out("--------------AudioInputStream-------");
out("frameLength " + ais.getFrameLength());
播放时长=frameLength/frameRate
frameRate=sampleRate
frameSize=channels*sampleSizeInBytes=channels*sampleSizeInBits/8
AudioFileFormat.getByteLength=文件头长度+数据长度=文件头长度+frameSize*frameLength

以上结论完全适用于wav文件,但不适用于mp3文件,因为mp3文件是经过压缩的,并不是原始音频数据.

十一.音频文件类型转换

AudioSystem.write(AudioInputStream,AudioFileFormat.Type,File)函数可以实现wav,aiff,au之间的转换.

十二.播放音频的其他方法

(1)使用Applet.getAudioClip(URL)来获取AudioClip

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.MalformedURLException;
import java.net.URL;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

public class TestAudioClip extends JPanel {

    AudioClip audioClip;

    TestAudioClip(String source) throws MalformedURLException {
        super(new GridLayout(1, 0, 10, 10));
        setBorder(new EmptyBorder(20, 20, 20, 20));
        JButton play = new JButton("Play");
        play.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                audioClip.play();
                audioClip.loop();
            }
        });
        add(play);

        JButton stop = new JButton("Stop");
        stop.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                audioClip.stop();
            }
        });
        add(stop);

        URL url = new URL(source);
        audioClip = Applet.newAudioClip(url);
        audioClip.play();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Thread() {
            public void run() {
                JFrame frame = new JFrame();
                TestAudioClip pc = null;
                try {
                    pc = new TestAudioClip(
                            "file:///C:/Users/weidiao/Documents/eclipseProject/实验室java/haha.mp3");
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
                frame.getContentPane().add(pc);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setVisible(true);
            }
        });
    }
}

(2)使用sun的AudioPlayer.player
sun这个包不是java标准包,在eclipse中编写如下代码会报错:
Access restriction: The type 'AudioPlayer' is not API (restriction on requir
解决方法是:右键项目>属性>buildpath,把jdk这个库的access rule(权限规则)放宽些.

import sun.audio.AudioPlayer;

public class TestAudioPlayer {
    public static void main(String[] args) throws FileNotFoundException {
        AudioPlayer.player.start(new FileInputStream("haha.mp3"));
    }
}

十三.播放MIDI文件

Sequencer sequencer = MidiSystem.getSequencer();
sequencer.open();
sequencer.setSequence(new FileInputStream("haha.mid"));
sequencer.start();

如果在classpath中添加了MP3SPI,播放很有可能没有声音.因为mp3spi.jar依赖的tritonus.jar会改变程序运行时行为.解决方法就是把mp3spi.jar和jlayer.jar和tritonus.jar移除掉.这个问题曾经令我困惑不已,为啥没有声音,我还以为我的扬声器坏了.结果把java文件在控制台下运行就能够发声了.
运行完了之后会发现这个程序无法终止,始终可以在任务管理器中看到.这是因为Sequencer没有close.给Sequencer添加一个事件监听器,当播放结束时,关闭Sequencer.这样程序就可以正常终止了.同样的问题在sample那一家子里面也同样存在,可以通过添加事件监听器来监听播放结束事件.

Sequencer sequencer = MidiSystem.getSequencer();
sequencer.open();
sequencer.setSequence(new FileInputStream("haha.mid"));
sequencer.start();
sequencer.addMetaEventListener(new MetaEventListener() {
    @Override
    public void meta(MetaMessage meta) {
    if (meta.getType() == 47) {
        sequencer.close();
    }
    }
});

十四.Midi系统概述

java sound明显分为两大门派:sample和midi.sample直接描述音频数据波形,相当于直接告诉你应该做成什么样.midi描述的是音频指令,告诉终端应该发的音符,响度,持续时间等.MIDI把命令直接发送给终端,终端有很大的自主权决定怎样发声.
sample系统和midi系统在Java API中设计的十分对称.熟悉了sample的那套API之后再看midi就有一种熟悉感.midi系统地入口类为MidiSystem,跟sample中的AudioSystem地位相当.通过MidiSystem可以管理MidiDivice,通过AudioSystem可以管理AudioDivice.通过MidiSystem可以获取Sequencer(序列器)和Synthesizer(合成器),序列器Sequencer用于播放一段声音,Synthesizer用于合成声音.
Sequencer和Synthesizer都是接口,继承自MidiDivice接口,而MidiDivice继承自java.io.Closable.
Transmitter和Receiver也都是接口,继承自java.io.Closable接口.Transmitter只有一个子接口MidiDiviceTransmitter,Receiver只有一个子接口MidiDiviceReceiver. Transmitter是MIDI输入端口,用于播放MIDI音频,Receiver是MIDI输出端口,用于录音.

MidiEvent=MidiMessage(这是一个实体类)+持续时间tick.
MidiMessage有三个子类:ShortMessage,MetaMessage,SysexMessage.
MidiMessage有两个成员变量:int length和byte[] data. length表示数据长度,data表示数据.其中,data的第一个字节表示status.

在java中,会看见很多byte使用int表示的,比如InputStream#read()读取一个字节,返回值为int.当读到末尾时,返回值为-1.之所以使用int来表示byte,是因为这里的byte是无符号byte,而java中有一个原则:一切数字皆有符号.于是有符号的byte无法表示128~255了,所以用int表示无符号byte.

instruments乐器,programs,pathes,timbres意义相近,都是指某种音色,某种乐器.midi说的soundbank跟java中的soundbank不太一样,midi中的一个soundbank可以包含128种乐器,而java中的soundbank包含16383*128种乐器.也就是一个java soundbank包含16383个midi bank. 为了定位一个乐器,java sound使用了Patch来定位Instrument,Patch只有两个成员方法:getBank(),getProgram().

十五.后记

Java sound越学越感觉像一个无底洞,任何一门技术都是一个无底洞.世界上有那么多的无底洞,千万不能掉到一个无底洞里出不来,不能因为探索一个无底洞而忽略了其它无底洞.
本文远写于2016.9.12,用markdown重写于2016.11.20

上一篇:闲话RPC调用


下一篇:2018-2019-20175334实验一《Java开发环境的熟悉》实验报告