使用文件和格式转换器

原文: https://docs.oracle.com/javase/tutorial/sound/converters.html

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

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

Sampled Package 概述介绍了与声音文件和音频数据格式相关的主要类。作为评论:

  • 可以从文件读取或写入文件的音频数据流由AudioInputStream对象表示。 (AudioInputStream继承自java.io.InputStream。)
  • The format of this audio data is represented by an AudioFormat object.

    此格式指定音频样例本身的排列方式,但不指定可能存储的文件结构。换句话说,AudioFormat描述“原始”音频数据,例如系统从麦克风输入捕获程序或从声音文件解析后,可能会将程序交给你。 AudioFormat包括诸如编码,字节顺序,信道数,采样率和每个样例的比特数之类的信息。

  • 声音文件有几种众所周知的标准格式,例如 WAV,AIFF 或 AU。不同类型的声音文件具有不同的结构,用于存储音频数据以及存储关于音频数据的描述性信息。声音文件格式由AudioFileFormat对象在 Java Sound API 中表示。 AudioFileFormat包括AudioFormat对象,用于描述存储在文件中的音频数据的格式,还包括有关文件类型和文件中数据长度的信息。

  • AudioSystem类提供了以下方法:(1)将来自AudioInputStream的音频数据流存储到特定类型的音频文件中(换句话说,写入文件),(2)提取来自音频文件的音频字节流(AudioInputStream)(换句话说,读取文件),以及(3)将音频数据从一种数据格式转换为另一种数据格式。该页面分为三个部分,解释了这三种活动。

Note:

Java Sound API 的实现不一定提供用于以不同数据和文件格式读取,写入和转换音频的全面工具。它可能仅支持最常见的数据和文件格式。但是,服务提供商可以开发和分发扩展此集合的转换服务,您将在后面的提供的采样音频服务中看到。 AudioSystem类提供的方法允许应用程序了解可用的转换,如后面转换文件和数据格式中所述。


读取声音文件

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

  • 有关存储在声音文件中的音频数据格式的信息
  • 可以从声音文件中读取的格式化音频数据流

第一个由getAudioFileFormat方法的三个变体给出:

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

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

第二种类型的文件读取功能由这些AudioSystem方法给出

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

这些方法为您提供了一个对象(AudioInputStream),可以使用AudioInputStream的一种读取方法读取文件的音频数据。我们会暂时看到一个例子。

假设您正在编写一个声音编辑应用程序,允许用户从文件加载声音数据,显示相应的波形或频谱图,编辑声音,播放编辑的数据,并将结果保存到一个新文件。或者,您的程序可能会读取存储在文件中的数据,应用某种信号处理(例如在不改变音调的情况下降低声音的算法),然后播放已处理的音频。在任何一种情况下,您都需要访问音频文件中包含的数据。假设您的程序为用户提供了一些选择或指定输入声音文件的方法,那么读取该文件的音频数据包括三个步骤:

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

以下代码段概述了以下步骤:

  1. int totalFramesRead = 0;
  2. File fileIn = new File(somePathName);
  3. // somePathName is a pre-existing string whose value was
  4. // based on a user selection.
  5. try {
  6. AudioInputStream audioInputStream =
  7. AudioSystem.getAudioInputStream(fileIn);
  8. int bytesPerFrame =
  9. audioInputStream.getFormat().getFrameSize();
  10. if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
  11. // some audio formats may have unspecified frame size
  12. // in that case we may read any amount of bytes
  13. bytesPerFrame = 1;
  14. }
  15. // Set an arbitrary buffer size of 1024 frames.
  16. int numBytes = 1024 * bytesPerFrame;
  17. byte[] audioBytes = new byte[numBytes];
  18. try {
  19. int numBytesRead = 0;
  20. int numFramesRead = 0;
  21. // Try to read numBytes bytes from the file.
  22. while ((numBytesRead =
  23. audioInputStream.read(audioBytes)) != -1) {
  24. // Calculate the number of frames actually read.
  25. numFramesRead = numBytesRead / bytesPerFrame;
  26. totalFramesRead += numFramesRead;
  27. // Here, do something useful with the audio data that's
  28. // now in the audioBytes array...
  29. }
  30. } catch (Exception ex) {
  31. // Handle the error...
  32. }
  33. } catch (Exception e) {
  34. // Handle the error...
  35. }

我们来看看上面的代码示例中发生了什么。首先,外部 try 子句通过调用AudioSystem.getAudioInputStream(File)方法实例化AudioInputStream对象。此方法透明地执行确定指定文件是否实际上是 Java Sound API 支持的类型的声音文件所需的所有测试。如果正在检查的文件(本例中为fileIn)不是声音文件,或者是某种不支持类型的声音文件,则抛出UnsupportedAudioFileException异常。这种行为很方便,因为应用程序员不需要考虑测试文件属性,也不需要遵守任何文件命名约定。相反,getAudioInputStream方法负责验证输入文件所需的所有低级解析和验证。 外部try子句然后创建一个任意固定长度的字节数组audioBytes。我们确保其以字节为单位的长度等于帧的整数,这样我们最终不会只读取帧的一部分,更糟糕的是,只读取部分样例。此字节数组将用作缓冲区,以便在从流中读取时临时保存一大块音频数据。如果我们知道我们只读取非常短的声音文件,我们可以通过从AudioInputStream's getFrameLength方法返回的帧长度中导出长度(以字节为单位)来使该数组与文件中的数据长度相同。 (实际上,我们可能只是使用Clip对象。)但是为了避免在一般情况下耗尽内存,我们反而以块的形式读取文件,一次读取一个缓冲区。

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

注意方法AudioInputStream.read(byte[])返回字节读取的数量 - 而不是样例或帧的数量。当没有更多数据要读取时,此方法返回-1。在检测到这种情况后,我们从while循环中断开。

写声音文件

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

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

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

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

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

只有在支持特定组合时才会返回true

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

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

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

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

  1. File fileOut = new File(someNewPathName);
  2. AudioFileFormat.Type fileType = fileFormat.getType();
  3. if (AudioSystem.isFileTypeSupported(fileType,
  4. audioInputStream)) {
  5. AudioSystem.write(audioInputStream, fileType, fileOut);
  6. }

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

第三个语句测试是否可以从所需的AudioInputStream写入指定类型的文件。与文件格式一样,此流可能是从先前读取的声音文件派生的。 (如果是这样,大概是你以某种方式处理或更改了它的数据,因为否则有更简单的方法来简单地复制文件。)或者流可能包含从麦克风输入中新捕获的字节。

最后,流,文件类型和输出文件被传递给AudioSystemwrite方法,完成编写文件的目标。

转换文件和数据格式

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

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

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

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

  1. public void ConvertFileToAIFF(String inputPath,
  2. String outputPath) {
  3. AudioFileFormat inFileFormat;
  4. File inFile;
  5. File outFile;
  6. try {
  7. inFile = new File(inputPath);
  8. outFile = new File(outputPath);
  9. } catch (NullPointerException ex) {
  10. System.out.println("Error: one of the
  11. ConvertFileToAIFF" +" parameters is null!");
  12. return;
  13. }
  14. try {
  15. // query file type
  16. inFileFormat = AudioSystem.getAudioFileFormat(inFile);
  17. if (inFileFormat.getType() != AudioFileFormat.Type.AIFF)
  18. {
  19. // inFile is not AIFF, so let's try to convert it.
  20. AudioInputStream inFileAIS =
  21. AudioSystem.getAudioInputStream(inFile);
  22. inFileAIS.reset(); // rewind
  23. if (AudioSystem.isFileTypeSupported(
  24. AudioFileFormat.Type.AIFF, inFileAIS)) {
  25. // inFileAIS can be converted to AIFF.
  26. // so write the AudioInputStream to the
  27. // output file.
  28. AudioSystem.write(inFileAIS,
  29. AudioFileFormat.Type.AIFF, outFile);
  30. System.out.println("Successfully made AIFF file, "
  31. + outFile.getPath() + ", from "
  32. + inFileFormat.getType() + " file, " +
  33. inFile.getPath() + ".");
  34. inFileAIS.close();
  35. return; // All done now
  36. } else
  37. System.out.println("Warning: AIFF conversion of "
  38. + inFile.getPath()
  39. + " is not currently supported by AudioSystem.");
  40. } else
  41. System.out.println("Input file " + inFile.getPath() +
  42. " is AIFF." + " Conversion is unnecessary.");
  43. } catch (UnsupportedAudioFileException e) {
  44. System.out.println("Error: " + inFile.getPath()
  45. + " is not a supported audio file type!");
  46. return;
  47. } catch (IOException e) {
  48. System.out.println("Error: failure attempting to read "
  49. + inFile.getPath() + "!");
  50. return;
  51. }
  52. }

如上所述,此示例函数ConvertFileToAIFF的目的是查询输入文件以确定它是否是 AIFF 声音文件,如果不是,则尝试将其转换为 1,生成一个新副本,其路径名由第二个参数指定。 (作为练习,您可以尝试使此功能更通用,以便不是总是转换为 AIFF,而是将函数转换为由新函数参数指定的文件类型。)请注意副本的音频数据格式 - 即,新文件 - 模仿原始输入文件的音频数据格式。

此函数的大部分内容都是不言自明的,并不是特定于 Java Sound API 的。但是,例程使用的一些 Java Sound API 方法对声音文件类型转换至关重要。这些方法调用都在上面的第二个try子句中找到,包括以下内容:

  • AudioSystem.getAudioFileFormat:此处用于确定输入文件是否已经是 AIFF 类型。如果是这样,该功能快速返回;否则转换尝试继续进行。
  • AudioSystem.isFileTypeSupported:表示系统是否可以写入包含来自指定AudioInputStream.的音频数据的指定类型的文件。在我们的示例中,如果指定的音频输入文件可以是,则此方法返回true转换为 AIFF 音频文件格式。如果不支持AudioFileFormat.Type.AIFFConvertFileToAIFF会发出无法转换输入文件的警告,然后返回。
  • AudioSystem.write:此处用于将音频数据从 AudioInputStream inFileAIS写入输出文件outFile

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

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

上一节介绍了如何使用 Java Sound API 将文件从一种文件格式(即一种声音文件)转换为另一种格式。本节探讨了一些启用音频数据格式转换的方法。

在上一节中,我们从任意类型的文件中读取数据,并将其保存在 AIFF 文件中。请注意,虽然我们更改了用于存储数据的文件类型,但我们没有更改音频数据本身的格式。 (最常见的音频文件类型,包括 AIFF,可以包含各种格式的音频数据。)因此,如果原始文件包含 CD 质量的音频数据(16 位样例大小,44.1-kHz 采样率和两个通道),那么我们的输出 AIFF 文件。

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

在不考虑前面那么多的编码细节的情况下,让我们探讨一些用于数据格式转换的方法,并考虑我们需要对ConvertFileToAIFF函数进行修改以实现新目标。

音频数据转换的主要方法再次出现在AudioSystem类中。此方法是getAudioInputStream的变体:

  1. AudioInputStream getAudioInputStream(AudioFormat
  2. format, AudioInputStream stream)

该函数返回AudioInputStream,它是使用指示的AudioFormatformat转换AudioInputStreamstream的结果。如果AudioSystem不支持转换,则此函数会抛出IllegalArgumentException

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

  1. boolean isConversionSupported(AudioFormat targetFormat,
  2. AudioFormat sourceFormat)

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

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

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

它使用线性 PCM 编码和给定参数构造AudioFormat,或者:

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

它还构造AudioFormat,但允许您指定编码,帧大小和帧速率,以及其他参数。

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

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

由于上面的AudioFormat构造器正在描述具有 8 位样例的格式,因此构造器的最后一个参数(指定样例是大端还是小端)是无关紧要的。 (如果样例大小大于单个字节,则大端与小端只是一个问题。)

以下示例显示了如何使用这个新的AudioFormat转换我们从输入文件创建的AudioInputStreaminFileAIS

  1. AudioInputStream lowResAIS;
  2. if (AudioSystem.isConversionSupported(outDataFormat,
  3. inFileAIS.getFormat())) {
  4. lowResAIS = AudioSystem.getAudioInputStream
  5. (outDataFormat, inFileAIS);
  6. }

只要在inFileAIS构建之后,插入此代码的位置就没那么重要了。如果没有isConversionSupported测试,如果请求的特定转换不受支持,则调用将失败并抛出IllegalArgumentException。 (在这种情况下,控制将转移到我们函数中的相应catch子句。)

因此,在此过程中,我们将生成一个新的AudioInputStream,这是由原始输入文件(以其AudioInputStream形式)转换为所需的低分辨率音频数据格式而产生的由outDataFormat定义。

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

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

对我们之前的函数的这些少量修改产生了一些转换音频数据和任何指定输入文件的文件格式的东西,当然假设系统支持转换。

学习什么是可用的转换

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