文档

Java™教程
隐藏目录
使用文件和格式转换器
路径:音频

使用文件和格式转换器

大多数处理声音的应用程序都需要读取声音文件或音频流。无论该程序随后对读取的数据进行什么操作(如播放、混合或处理),这是常见的功能。同样,许多程序需要写入声音文件(或流)。在某些情况下,已读取的数据(或将要写入的数据)需要转换为不同的格式。

正如在访问音频系统资源中简要提到的,Java Sound API为应用程序开发人员提供了各种文件输入/输出和格式转换的功能。应用程序可以在各种声音文件格式和音频数据格式之间读取、写入和转换。

示例包概述介绍了与声音文件和音频数据格式相关的主要类。作为复习:


注意: 

Java Sound API的实现不一定提供完整的读取、写入和转换不同数据和文件格式的音频功能。它可能只支持最常见的数据和文件格式。但是,服务提供者可以开发和分发扩展这个集合的转换服务,如后面在提供采样音频服务中所述。AudioSystem类提供的方法允许应用程序了解可用的转换方式,后面在转换文件和数据格式中描述。


读取声音文件

AudioSystem类提供了两种类型的文件读取服务:

第一种是通过getAudioFileFormat方法的三个变体提供的:

    static AudioFileFormat getAudioFileFormat (java.io.File file)
    static AudioFileFormat getAudioFileFormat(java.io.InputStream stream)
    static AudioFileFormat getAudioFileFormat (java.net.URL url)

如上所述,返回的AudioFileFormat对象告诉您文件类型、文件中数据的长度、编码、字节顺序、通道数、采样率和每个样本的位数。

第二种文件读取功能由这些AudioSystem方法提供

    static AudioInputStream getAudioInputStream (java.io.File file)
    static AudioInputStream getAudioInputStream (java.net.URL url)
    static AudioInputStream getAudioInputStream (java.io.InputStream stream)

这些方法给您一个对象(AudioInputStream),让您使用AudioInputStream的其中一种读取方法读取文件的音频数据。稍后我们将看到一个示例。

假设您正在编写一个音频编辑应用程序,允许用户从文件加载音频数据,显示相应的波形图或频谱图,编辑音频,播放编辑后的数据,并将结果保存在新文件中。或者,您的程序将读取存储在文件中的数据,应用某种信号处理(如将音频放慢但保持音调不变的算法),然后播放处理后的音频。无论哪种情况,您都需要访问音频文件中包含的数据。假设您的程序提供了某种方式让用户选择或指定输入音频文件,读取该文件的音频数据包括三个步骤:

  1. 从文件中获取一个AudioInputStream对象。
  2. 创建一个字节数组,用来存储从文件中连续读取的数据块。
  3. 重复从音频输入流中读取字节到数组中。在每次迭代中,对数组中的字节进行一些有用的操作(例如,可以播放它们、过滤它们、分析它们、显示它们或将它们写入另一个文件)。

下面的代码片段概述了这些步骤:

int totalFramesRead = 0;
File fileIn = new File(somePathName);
// somePathName 是一个预先存在的字符串,其值基于用户的选择。
try {
  AudioInputStream audioInputStream = 
    AudioSystem.getAudioInputStream(fileIn);
  int bytesPerFrame = 
    audioInputStream.getFormat().getFrameSize();
    if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
    // 一些音频格式可能没有指定帧大小
    // 在这种情况下,我们可以读取任意数量的字节
    bytesPerFrame = 1;
  } 
  // 设置一个任意的缓冲区大小为1024帧。
  int numBytes = 1024 * bytesPerFrame; 
  byte[] audioBytes = new byte[numBytes];
  try {
    int numBytesRead = 0;
    int numFramesRead = 0;
    // 尝试从文件中读取numBytes个字节。
    while ((numBytesRead = 
      audioInputStream.read(audioBytes)) != -1) {
      // 计算实际读取的帧数。
      numFramesRead = numBytesRead / bytesPerFrame;
      totalFramesRead += numFramesRead;
      // 在这里,对现在在audioBytes数组中的音频数据做一些有用的操作...
    }
  } catch (Exception ex) { 
    // 处理错误...
  }
} catch (Exception e) {
  // 处理错误...
}

让我们来看一下上面的代码样例中发生了什么。首先,外部的try块通过调用AudioSystem.getAudioInputStream(File)方法来实例化一个AudioInputStream对象。这个方法会透明地执行所有必要的测试,以确定指定的文件是否实际上是一种Java Sound API支持的声音文件类型。如果正在检查的文件(在此示例中为fileIn)不是声音文件,或者是某种不受支持的声音文件类型的声音文件,则会抛出一个UnsupportedAudioFileException异常。这种行为很方便,因为应用程序员不需要烦扰于测试文件属性,也不需要遵循任何文件命名约定。相反,getAudioInputStream方法会处理所有需要验证输入文件的低级解析和验证。

tryaudioBytesAudioInputStreamgetFrameLengthClip

内部的try子句包含一个while循环,在该循环中我们从AudioInputStream中将音频数据读入字节数组中。您应该在该循环中添加代码,以适合程序需求处理该数组中的音频数据。如果您对数据应用了某种信号处理,可能需要进一步查询AudioInputStreamAudioFormat,以了解每个样本的位数等信息。

请注意,AudioInputStream.read(byte[])方法返回的是读取的字节数,而不是样本数或帧数。当没有更多数据可读时,该方法将返回-1。在检测到这种情况时,我们会从while循环中退出。

写入音频文件

前一节介绍了使用AudioSystemAudioInputStream类的特定方法读取音频文件的基础知识。本节将介绍如何将音频数据写入到新文件中。

下面的AudioSystem方法将创建一个指定文件类型的磁盘文件。该文件将包含指定AudioInputStream中的音频数据:

static int write(AudioInputStream in, 
  AudioFileFormat.Type fileType, File out)

请注意,第二个参数必须是系统支持的文件类型之一(例如AU、AIFF或WAV),否则write方法将抛出IllegalArgumentException异常。为了避免这种情况,您可以通过调用以下AudioSystem方法来测试特定AudioInputStream是否可以写入特定类型的文件:

static boolean isFileTypeSupported
  (AudioFileFormat.Type fileType, AudioInputStream stream)

只有在特定组合受支持时,该方法才会返回true

更一般地,您可以通过调用以下任一AudioSystem方法来了解系统可以写入的文件类型:

static AudioFileFormat.Type[] getAudioFileTypes() 
static AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) 

第一个方法返回系统可以写入的所有文件类型,第二个方法仅返回系统可以从给定音频输入流写入的文件类型。

下面的摘录演示了使用上述提到的write方法从AudioInputStream创建输出文件的一种技术。

File fileOut = new File(someNewPathName);
AudioFileFormat.Type fileType = fileFormat.getType();
if (AudioSystem.isFileTypeSupported(fileType, 
    audioInputStream)) {
  AudioSystem.write(audioInputStream, fileType, fileOut);
}

上面的第一个语句创建了一个名为fileOut的新的File对象,其路径名由用户或程序指定。第二个语句从一个预先存在的名为fileFormat的AudioFileFormat对象中获取文件类型,该对象可能是从其他声音文件中获取的,比如在上面的“读取声音文件”中读取的文件。(你也可以提供任何支持的文件类型,而不是从其他地方获取文件类型。例如,你可以删除第二个语句,并将上面代码中的另外两个fileType的出现替换为AudioFileFormat.Type.WAVE。)

第三个语句测试是否可以从所需的AudioInputStream中写入指定类型的文件。与文件格式一样,该流可能是从先前读取的声音文件中派生的。(如果是这样的话,可能你已经以某种方式处理或修改了其数据,否则只需简单地复制文件即可。)或者,该流可能包含刚从麦克风输入捕获的字节。

最后,将流、文件类型和输出文件传递给AudioSystem.write方法,以实现写入文件的目标。

转换文件和数据格式

回想一下,在“什么是格式化音频数据?”中,Java Sound API区分音频文件格式和音频数据格式。两者在某种程度上是独立的。粗略地说,数据格式是指计算机表示每个原始数据点(样本)的方式,而文件格式是指存储在磁盘上的声音文件的组织方式。每种声音文件格式都有一种特定的结构,用于定义文件头中存储的信息。在某些情况下,文件格式还包括包含某种形式的元数据的结构,除了实际的“原始”音频样本。本页面的其余部分介绍了Java Sound API的一些方法,可以实现各种文件格式和数据格式的转换。

从一种文件格式转换到另一种

本节介绍了Java Sound API中转换音频文件类型的基础知识。再次,我们提出了一个假设的程序,其目的是从任意输入文件中读取音频数据,并将其写入类型为AIFF的文件中。当然,输入文件必须是系统能够读取的类型,输出文件必须是系统能够写入的类型。(在这个例子中,我们假设系统能够写入AIFF文件。)该示例程序不进行任何数据格式转换。如果输入文件的数据格式无法表示为AIFF文件,程序将简单地通知用户出现了该问题。另一方面,如果输入音频文件已经是AIFF文件,程序将通知用户无需转换。

以下函数实现了刚才描述的逻辑:

public void ConvertFileToAIFF(String inputPath, 
  String outputPath) {
  AudioFileFormat inFileFormat;
  File inFile;
  File outFile;
  try {
    inFile = new File(inputPath);
    outFile = new File(outputPath);     
  } catch (NullPointerException ex) {
    System.out.println("错误:ConvertFileToAIFF的一个参数为空!");
    return;
  }
  try {
    // 查询文件类型
    inFileFormat = AudioSystem.getAudioFileFormat(inFile);
    if (inFileFormat.getType() != AudioFileFormat.Type.AIFF) 
    {
      // inFile不是AIFF格式,尝试转换它。
      AudioInputStream inFileAIS = 
        AudioSystem.getAudioInputStream(inFile);
      inFileAIS.reset(); // 重新定位到起始位置
      if (AudioSystem.isFileTypeSupported(
             AudioFileFormat.Type.AIFF, inFileAIS)) {
         // inFileAIS可以转换为AIFF格式。
         // 将AudioInputStream写入输出文件。
         AudioSystem.write(inFileAIS,
           AudioFileFormat.Type.AIFF, outFile);
         System.out.println("成功创建AIFF文件,"
           + outFile.getPath() + ",来源于"
           + inFileFormat.getType() + "文件," +
           inFile.getPath() + "。");
         inFileAIS.close();
         return; // 完成
       } else
         System.out.println("警告:当前音频系统不支持将" 
           + inFile.getPath()
           + "转换为AIFF格式。");
    } else
      System.out.println("输入文件" + inFile.getPath() +
          "是AIFF格式。无需转换。");
  } catch (UnsupportedAudioFileException e) {
    System.out.println("错误:" + inFile.getPath()
        + "不是支持的音频文件类型!");
    return;
  } catch (IOException e) {
    System.out.println("错误:读取" 
      + inFile.getPath() + "时发生错误!");
    return;
  }
}

如前所述,这个示例函数ConvertFileToAIFF的目的是查询输入文件以确定它是否是AIFF音频文件,如果不是,则尝试将其转换为AIFF格式,并生成一个指定路径的新副本。(作为练习,你可以尝试使此函数更通用,使其可以根据新的函数参数将文件转换为指定的文件类型。)注意,副本的音频数据格式与原始输入文件的音频数据格式相同。

这个函数的大部分内容都是不言自明的,与Java Sound API无关。然而,这个函数使用了一些对于声音文件类型转换至关重要的Java Sound API方法。这些方法调用都在上面的第二个try语句中,包括以下内容:

第二个方法isFileTypeSupported可以在写入之前帮助确定是否可以将特定的输入声音文件转换为特定的输出声音文件类型。在下一节中,我们将看到如何通过对ConvertFileToAIFF示例程序进行一些修改来转换音频数据格式以及声音文件类型。

在不同数据格式之间进行音频转换

前一节展示了如何使用Java Sound API将文件从一种文件格式(即一种声音文件类型)转换为另一种。本节将探讨一些方法,这些方法可以实现音频数据格式的转换。

在前一节中,我们从任意类型的文件中读取数据,并将其保存在一个AIFF文件中。请注意,尽管我们更改了用于存储数据的文件类型,但我们并没有更改音频数据本身的格式。(包括AIFF在内的大多数常见音频文件类型都可以包含不同格式的音频数据。)因此,如果原始文件包含CD音质的音频数据(16位样本大小,44.1kHz采样率和两个声道),那么我们的输出AIFF文件也将包含相同的音频数据。

现在假设我们希望指定输出文件的数据格式以及文件类型。例如,我们可能要保存许多长时间的文件供互联网使用,并且关心文件所需的磁盘空间和下载时间。我们可以选择创建包含较低分辨率数据的较小的AIFF文件,例如具有8位样本大小、8kHz采样率和单声道的数据。

为了不像之前那样详细进行编码,让我们探索一些用于数据格式转换的方法,并考虑我们需要对ConvertFileToAIFF函数进行哪些修改来实现新的目标。

音频数据转换的主要方法再次位于AudioSystem类中。该方法是getAudioInputStream的一个变体:

AudioInputStream getAudioInputStream(AudioFormat
    format, AudioInputStream stream)

该函数返回使用指定的AudioFormat formatAudioInputStream stream进行转换后的AudioInputStream。如果转换不受AudioSystem支持,则该函数抛出IllegalArgumentException异常。

为了避免这种情况,我们可以通过调用这个AudioSystem方法来首先检查系统是否可以执行所需的转换:

boolean isConversionSupported(AudioFormat targetFormat,
    AudioFormat sourceFormat)

在这种情况下,我们将把 stream.getFormat() 作为第二个参数传递。

要创建特定的 AudioFormat 对象,我们使用下面显示的两个 AudioFormat 构造函数之一:

AudioFormat(float sampleRate, int sampleSizeInBits,
    int channels, boolean signed, boolean bigEndian)

它使用给定的参数构造一个带有线性PCM编码的 AudioFormat,或者:

AudioFormat(AudioFormat.Encoding encoding, 
    float sampleRate, int sampleSizeInBits, int channels,
    int frameSize, float frameRate, boolean bigEndian) 

它也构造了一个 AudioFormat,但除了其他参数之外,还可以指定编码、帧大小和帧速率。

现在,有了上述方法,让我们看看如何扩展我们的 ConvertFileToAIFF 函数以执行所需的“低分辨率”音频数据格式转换。首先,我们将构造一个描述所需输出音频数据格式的 AudioFormat 对象。下面的语句足够,并且可以插入到函数的顶部附近:

AudioFormat outDataFormat = new AudioFormat((float) 8000.0,
(int) 8, (int) 1, true, false);

由于上述 AudioFormat 构造函数描述的是具有8位样本的格式,构造函数的最后一个参数,用于指定样本是大端还是小端,是无关紧要的。(大端与小端只有在样本大小大于一个字节时才是一个问题。)

下面的示例显示了我们将如何使用这个新的 AudioFormat 来转换我们从输入文件创建的 AudioInputStream,即 inFileAIS

AudioInputStream lowResAIS;         
  if (AudioSystem.isConversionSupported(outDataFormat,   
    inFileAIS.getFormat())) {
    lowResAIS = AudioSystem.getAudioInputStream
      (outDataFormat, inFileAIS);
  }

我们插入这段代码的位置并不重要,只要它在构造 inFileAIS 之后即可。如果特定的转换不受支持,那么没有 isConversionSupported 测试,调用将失败并抛出 IllegalArgumentException。(在这种情况下,控制流会转移到函数中适当的 catch 子句。)

到目前为止,我们已经产生了一个新的 AudioInputStream,它是通过将原始输入文件(以其 AudioInputStream 形式)转换为由 outDataFormat 定义的所需低分辨率音频数据格式而得到的。

产生所需的低分辨率 AIFF 声音文件的最后一步是将调用 AudioSystem.write 中的 AudioInputStream 参数(即第一个参数)替换为我们的转换流 lowResAIS,如下所示:

AudioSystem.write(lowResAIS, AudioFileFormat.Type.AIFF, 
  outFile);

对我们之前的函数进行少量修改,可以实现将指定输入文件的音频数据和文件格式进行转换,当然前提是系统支持该转换。

了解可用的转换

几个AudioSystem方法测试其参数以确定系统是否支持特定的数据格式转换或文件写入操作(通常,每个方法都与执行数据转换或写入文件的另一个方法配对)。其中一个查询方法AudioSystem.isFileTypeSupported在我们的示例函数ConvertFileToAIFF中被使用,以确定系统是否能够将音频数据写入AIFF文件。相关的AudioSystem方法getAudioFileTypes(AudioInputStream)返回给定流支持的所有文件类型的完整列表,以AudioFileFormat.Type实例数组的形式返回。该方法签名为:
boolean isConversionSupported(AudioFormat.Encoding encoding, AudioFormat format)

boolean isConversionSupported(AudioFormat newFormat,
                              AudioFormat oldFormat) 

告诉我们是否可以通过转换具有指定音频格式newFormatAudioInputStream来获得具有音频格式oldFormatAudioInputStream。(该方法在前一节的代码摘录中被调用,用于创建低分辨率的音频输入流lowResAIS。)

这些与格式相关的查询有助于在尝试使用Java Sound API执行格式转换时避免错误。


上一页: 使用控件处理音频
下一页: MIDI 包概述