Java游戏编程不完全详解-4

前言

代码演示环境:

  • 软件环境:Windows 10
  • 开发工具:Visual Studio Code
  • JDK版本:OpenJDK 15

声效和音乐

声效基础知识


当我们玩游戏时,我们可能会听到声效,但是不会真正注意它们。因为希望听到他们,所以声效在游戏中是非常重要的。另外,在游戏中的音乐会动态被修改来配合游戏的剧情的发展。那么什么是声效(声音)呢?声效是通过媒体振动产生的效果。该媒体是空气和计算机中的扬声器产生的振动—从而发出了声音—传送到我们耳朵里;然后我们的耳膜会捕获这些信号,接着传送给我们的大脑,从而人类听到了声音。

共振(vibration)是通过空气的压缩振动(fluctuations)产生的,快速的振动产生高频声效,让我们听到高音。每个振动的压缩数量是使用振幅(amplitude)来表示的。高振幅会让我们听到声音大;简而言之,声波(sound waves)就是在持久时间不停修改振幅而已。如下图所示:

Java游戏编程不完全详解-4_第1张图片

数码声效、CD和计算机的音效格式都是一系列的声波,每秒中的音波振幅叫做音频采样。当然高采样的音波可以更加精确的表现声音,这些采样是使用16位来表示65535种可能的振幅。许多声音允许多个声道,比如CD有两个声道—一个给左扬声器,一个给右扬声器。

Java声效API


Java可以播放8位和16位的采样,它的范围从8000hz到48000hz,当然它也可以播放单声道和立体声声效。那么使用什么声音,这需要根据游戏的剧情,比如16位单声道,44100Hz声音。Java支持三种声频格式文件:AIFF, AU和WAV文件。我们装载音频文件时使用AudioSystem类,该类有几个静态方法,一般我们使用getAudioInputStream()方法来打开一个音频文件,可以从本地系统,或者从互联网打开,然后返回AudioInputStream对象。然后使用该对象读取音频采样,或者查询音频样式。

File file = new File(“sound.wav”);
AudioInputStream stream = AudioSystem.getAudioInputStream(file);
AudioFormat format = stream.getFormat();

其中AudioFormat类提供了获取声效采样的功能,比如采样率和声道数量。另外它还提供了frame尺寸--一些字节数量。比如16位立体声,它的frame大小是4,或者2个字节表示采样值,这样我们可以很方便的计算出立体声可以占多少内存。比如16位三分之二长度的立体音频格式采样所占内存值:44100x 3x 4字节 = 517KB,如果是单声道,那么采样容量是立体声的一半。

当我们知识声频采样的大小与格式之后,接下来就是从这些声频文件中读取内容了。接口Line是用来发送和接收系统的音频的API。我们可以使用Line发送声音采样到OS的声音系统去播放,或者接收OS的声音系统的声音,比如microphone声音等。Line有几个子接口,最主要的子接口是SourceDataLine,该接口可以让我们向OS中的声音系统写入声音数据。Line的实例是通过AudioSystem的getLine()方法获取,我们可以传送参数Line.Info对象来指定返回的Line类型。因为Line.Info有一个DataLine.Info子类,它知道Line类型除了SourceDataLine接口之外,还有另外一个Line叫做Clip()接口。该接口可以为我们做许多事情,比如把采样从AudioInputStream流装载到内存中去,并且自动向音频系统输送这些数据去播放。下面是使用Clipe来实现声音的播放代码:

//指定哪种line需要被创建
DataLine.Info info = new DataLine.Info(Clip.class,format);
//创建该line
Clip clip = (Clip)AudioSystem.getLine(info);
//从流对象装载采样
clip.open(stream);
//开始播放音频内容
clip.start();

Clip接口非常好用,它非常类似于JDK 1.0版本中AudioClip对象,但是它有一些缺点,比如Java声效有限制Line的数量,这种限制是在相同的时间打开Line时出现,一般最多有32个Line对象同时存在。也就是说,我们只能打开有限个line对象使用。另外,如果我们想同时播放多个Clip对象,那么Clip只能在同一时间播放一个声音,比如我们想同时播放两到三个爆炸声,但是一个声音只能应用一个爆炸声。因为这种缺陷,所以我们会创建一种解决方案来克服这种问题。

播放声音


下面我们创建一个简单的声音播放器,主要使用AudioInputStream类把音频文件读到字节数组中,然后使用Line对象来自动播放。因为ByteArrayInputStream类封装了字节数组,所以,我们可以同时播放多个相同音频的复本。getSamples(AudioInputStream)方法从AudioInputStream流中读采样数据,然后保存到字节数组中,最后使用play()方法从InputStream流对象中读取数据到缓存,然后写到SourceDataLine对象中让它播放。

由于Java声效API中有bug,所以让Java进程不会自己退出,通常情况下,JVM只运行精灵线程,但是当我们使用Java声效时,非精灵线程在台后进行中运行,所以我们必须呼叫System.exit(0)结束Java声效进程。

SimpleSoundPlayer类

package com.funfree.arklis.sounds;
import java.io.*;
import javax.sound.sampled.*;

/**
    功能:书写一个的类,用来封装声音从文件系统打开,然后进行播放
    */

public class SimpleSoundPlayer  {
    private AudioFormat format;
    private byte[] samples;//保存声音采样

    /**
        Opens a sound from a file.
    */
    public SimpleSoundPlayer(String filename) {
        try {
            //打开一个音频流
            AudioInputStream stream =
                AudioSystem.getAudioInputStream(
                new File(filename));

            format = stream.getFormat();

            //取得采样
            samples = getSamples(stream);
        }
        catch (UnsupportedAudioFileException ex) {
            ex.printStackTrace();
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
    }


    /**
        Gets the samples of this sound as a byte array.
    */
    public byte[] getSamples() {
        return samples;
    }


    /**
        从AudioInputStream获取采样,然后保存为字节数组。--这里的数组会被封装ByteArrayInputStream类中,
        以便Line可以同时播放多个音频文件。
    */
    private byte[] getSamples(AudioInputStream audioStream) {
        //获取读取字节数
        int length = (int)(audioStream.getFrameLength() *
            format.getFrameSize());

        //读取整个流
        byte[] samples = new byte[length];
        DataInputStream is = new DataInputStream(audioStream);
        try {
            is.readFully(samples);
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }

        // 返回样本
        return samples;
    }


    /**
        播放流
    */
    public void play(InputStream source) {
        //每100毫秒的音频采样
        int bufferSize = format.getFrameSize() *
            Math.round(format.getSampleRate() / 10);
        byte[] buffer = new byte[bufferSize];

        //创建line对象来执行声音的播放
        SourceDataLine line;
        try {
            DataLine.Info info =
                new DataLine.Info(SourceDataLine.class, format);
            line = (SourceDataLine)AudioSystem.getLine(info);
            line.open(format, bufferSize);
        }catch (LineUnavailableException ex) {
            ex.printStackTrace();
            return;
        }

        // 开始自动播放
        line.start();

        // 拷贝数据到line对象中
        try {
            int numBytesRead = 0;
            while (numBytesRead != -1) {
                numBytesRead =
                    source.read(buffer, 0, buffer.length);
                if (numBytesRead != -1) {
                   line.write(buffer, 0, numBytesRead);
                }
            }
        }catch (IOException ex) {
            ex.printStackTrace();
        }

        // 等待所有的数据播放完毕,然后关闭line对象。
        line.drain();
        line.close();

    }

}

如果需要循环播出,那么修改一下上面类就可以实现该功能。

LoopingByteInputStream类

package com.funfree.arklis.engine;
import static java.lang.System.*;
import java.io.*;

/**
    功能:封装ByteArrayInputStream类,用来循环播放音频文件。当停止循环播放时
          呼叫close()方法
    */
public class LoopingByteInputStream extends ByteArrayInputStream{
    private boolean closed;
    
    public LoopingByteInputStream(byte[] buffer){
        super(buffer);
        closed = false;
    }
    
    /**
        读取长度为length的数组。如果读完数组内容,那么把下标设置为开始处,
        如果关闭状态,那么返回-1.
        */
    public int read(byte[] buffer, int offset, int length){
        if(closed){
            return -1;
        }
        int totalBytesRead = 0;
        while(totalBytesRead < length){
            int numBytesRead = super.read(buffer,offset + totalBytesRead,
                length - totalBytesRead);
            if(numBytesRead > 0){
                totalBytesRead += numBytesRead;
            }else{
                reset();
            }
        }
        return totalBytesRead;
    }
    
    /**
        关闭流
        */
    public void close()throws IOException{
        super.close();
        closed = true;
    }
}

声效过滤器是简单的音频处理器,用来现有的声音样本,这种过滤器一般用来实时处理声音。所以谓声效过滤器就是常说的是数字信号处理器(digital signal processor)—用于后期声效的处理,比如吉他添加回响效果。
图片来源:http://www.cungun.com/ 游戏

你可能感兴趣的:(java游戏开发)