Java - 读取、操作和写入WAV文件

29

在Java程序中,读取音频文件(WAV文件)并将其转换为数字数组(float[], short[]等),以及从数字数组写入WAV文件,最佳的方法是什么?

9个回答

15

我通过AudioInputStream读取WAV文件。以下代码片段来自Java声音教程,效果很好。

int totalFramesRead = 0;
File fileIn = new File(somePathName);
// somePathName is a pre-existing string whose value was
// based on a user selection.
try {
  AudioInputStream audioInputStream = 
    AudioSystem.getAudioInputStream(fileIn);
  int bytesPerFrame = 
    audioInputStream.getFormat().getFrameSize();
    if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
    // some audio formats may have unspecified frame size
    // in that case we may read any amount of bytes
    bytesPerFrame = 1;
  } 
  // Set an arbitrary buffer size of 1024 frames.
  int numBytes = 1024 * bytesPerFrame; 
  byte[] audioBytes = new byte[numBytes];
  try {
    int numBytesRead = 0;
    int numFramesRead = 0;
    // Try to read numBytes bytes from the file.
    while ((numBytesRead = 
      audioInputStream.read(audioBytes)) != -1) {
      // Calculate the number of frames actually read.
      numFramesRead = numBytesRead / bytesPerFrame;
      totalFramesRead += numFramesRead;
      // Here, do something useful with the audio data that's 
      // now in the audioBytes array...
    }
  } catch (Exception ex) { 
    // Handle the error...
  }
} catch (Exception e) {
  // Handle the error...
}

写一个WAV文件,我发现这相当棘手。表面上看起来像是一个循环问题,写入命令依赖于作为参数的AudioInputStream

但是如何向AudioInputStream写入字节呢?难道不应该有一个AudioOutputStream吗?

我发现可以定义一个对象,它可以访问原始音频字节数据来实现TargetDataLine

这需要实现很多方法,但大多数方法都可以保持虚拟形式,因为它们对于将数据写入文件并不是必需的。要实现的关键方法是read(byte[] buffer, int bufferoffset, int numberofbytestoread)

由于这个方法可能会被调用多次,应该还有一个实例变量,指示进度到达了哪个数据,并在上述read方法中更新它。

当你实现了这个方法后,那么你的对象就可以用来创建一个新的AudioInputStream,进而可以与以下内容一起使用:

AudioSystem.write(yourAudioInputStream, AudioFileFormat.WAV, yourFileDestination)
作为提醒,可以使用TargetDataLine作为源来创建AudioInputStream。
至于直接操纵数据,在上面代码片段的最内层循环中,即audioBytes中已经有了优秀的操作方案。
当您处于该内部循环时,可以将字节转换为整数或浮点数,并乘以从0.0到1.0范围的volume值,然后将它们转换回小端字节。
我认为,由于您在该缓冲区中可以访问一系列样本,因此可以在该阶段应用各种形式的DSP滤波算法。根据我的经验,我发现直接在该缓冲区中对数据进行音量更改更好,因此您可以做出最小可能的增量:每个样本一个增量,从而将由于其产生体积感应性不连续而引起的单击几率降到最低。
我发现Java提供的音量控制线路往往会导致点击情况,我认为这是因为增量只实现在单个缓冲读取(通常在1024个样本中的每个变化)的粒度上,而不是将变化分成更小的部分并逐个添加它们。但我不知道Volume Control是如何实现的,所以请带着怀疑的态度看待这个推测。
总的来说,Java.Sound真的很难理解。我认为教程没有包括直接从字节写入文件的明确示例。 我认为教程将最佳播放文件编码的示例藏在“How to Convert…”部分中。 但是,该教程中有很多有价值的免费信息。
编辑:12/13/17
我后来使用以下代码在自己的项目中从PCM文件写入音频。 可以扩展InputStream并使用它作为参数来AudioSystem.write方法,而不是实现TargetDataLine。
public class StereoPcmInputStream extends InputStream
{
    private float[] dataFrames;
    private int framesCounter;
    private int cursor;
    private int[] pcmOut = new int[2];
    private int[] frameBytes = new int[4];
    private int idx;
    
    private int framesToRead;

    public void setDataFrames(float[] dataFrames)
    {
        this.dataFrames = dataFrames;
        framesToRead = dataFrames.length / 2;
    }
    
    @Override
    public int read() throws IOException
    {
        while(available() > 0)
        {
            idx &= 3; 
            if (idx == 0) // set up next frame's worth of data
            {
                framesCounter++; // count elapsing frames

                // scale to 16 bits
                pcmOut[0] = (int)(dataFrames[cursor++] * Short.MAX_VALUE);
                pcmOut[1] = (int)(dataFrames[cursor++] * Short.MAX_VALUE);
            
                // output as unsigned bytes, in range [0..255]
                frameBytes[0] = (char)pcmOut[0];
                frameBytes[1] = (char)(pcmOut[0] >> 8);
                frameBytes[2] = (char)pcmOut[1];
                frameBytes[3] = (char)(pcmOut[1] >> 8);
            
            }
            return frameBytes[idx++]; 
        }
        return -1;
    }

    @Override 
    public int available()
    {
        // NOTE: not concurrency safe.
        // 1st half of sum: there are 4 reads available per frame to be read
        // 2nd half of sum: the # of bytes of the current frame that remain to be read
        return 4 * ((framesToRead - 1) - framesCounter) 
                + (4 - (idx % 4));
    }    

    @Override
    public void reset()
    {
        cursor = 0;
        framesCounter = 0;
        idx = 0;
    }

    @Override
    public void close()
    {
        System.out.println(
            "StereoPcmInputStream stopped after reading frames:" 
                + framesCounter);
    }
}

这里要导出的源数据是以从-1到1的立体声浮点数形式呈现的。生成流的格式为16位、立体声、小端字节序。

对于我的特定应用程序,我省略了skipmarkSupported方法。但如果需要,添加它们应该不难。


14

这是将音频直接写入wav文件的源代码。 你只需要了解数学和声学工程,就能产生想要的声音。 在这个例子中,方程计算了一个双耳节拍。

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;

public class Program {
    public static void main(String[] args) throws IOException {
        final double sampleRate = 44100.0;
        final double frequency = 440;
        final double frequency2 = 90;
        final double amplitude = 1.0;
        final double seconds = 2.0;
        final double twoPiF = 2 * Math.PI * frequency;
        final double piF = Math.PI * frequency2;

        float[] buffer = new float[(int)(seconds * sampleRate)];

        for (int sample = 0; sample < buffer.length; sample++) {
            double time = sample / sampleRate;
            buffer[sample] = (float)(amplitude * Math.cos(piF * time) * Math.sin(twoPiF * time));
        }

        final byte[] byteBuffer = new byte[buffer.length * 2];

        int bufferIndex = 0;
        for (int i = 0; i < byteBuffer.length; i++) {
            final int x = (int)(buffer[bufferIndex++] * 32767.0);

            byteBuffer[i++] = (byte)x;
            byteBuffer[i] = (byte)(x >>> 8);
        }

        File out = new File("out10.wav");

        final boolean bigEndian = false;
        final boolean signed = true;

        final int bits = 16;
        final int channels = 1;

        AudioFormat format = new AudioFormat((float)sampleRate, bits, channels, signed, bigEndian);
        ByteArrayInputStream bais = new ByteArrayInputStream(byteBuffer);
        AudioInputStream audioInputStream = new AudioInputStream(bais, format, buffer.length);
        AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, out);
        audioInputStream.close();
    }
}

9

如果您只需要原始WAV数据,则可以使用FileInputStream和Scanner将其转换为数字。但是,提供更多有关您想要实现的详细信息会更有帮助。让我尝试为您提供一些有意义的示例代码来帮助您入门:

为此目的,有一个名为com.sun.media.sound.WaveFileWriter的类。

InputStream in = ...;
OutputStream out = ...;

AudioInputStream in = AudioSystem.getAudioInputStream(in);

WaveFileWriter writer = new WaveFileWriter();
writer.write(in, AudioFileFormat.Type.WAVE, outStream);

你可以实现自己的AudioInputStream来将数字数组转换为音频数据。
writer.write(new VoodooAudioInputStream(numbers), AudioFileFormat.Type.WAVE, outStream);

正如@stacker所提到的,你当然应该熟悉API。


我的主要问题是voodoo本身。我想看看是否有现成的代码/类可以做到这一点。 我现在认为我成功了,使用AudioSystem和AudioInputStream。诀窍是在将其转换为short之前,反转每个声音样本中字节的顺序,因为WAV以little-Endian方式编码数值。 谢谢你,Yonatan。 - yonatan

8

如果你需要访问实际的样本值,javax.sound.sample包不适用于处理WAV文件。该包可让你更改音量、采样率等,但如果需要其他效果(如添加回声),则需自行实现。(Java教程暗示可以直接处理样本值,但技术作家过度承诺了。)

这个网站提供了一个简单的类来处理WAV文件:http://www.labbookpages.co.uk/audio/javaWavFiles.html



3

3

1

如果有人仍然需要,我正在开发一个音频框架,旨在解决这个和类似的问题。虽然它是基于Kotlin的。您可以在GitHub上找到它:https://github.com/WaveBeans/wavebeans

它会看起来像这样:

wave("file:///path/to/file.wav")
    .map { it.asInt() } // here it as Sample type, need to convert it to desired type
    .asSequence(44100.0f) // framework processes everything as sequence/stream
    .toList() // read fully
    .toTypedArray() // convert to array

而且它不依赖于Java音频。


0

我使用一些神奇的方法,结合FileInputStream

    byte[] byteInput = new byte[(int)file.length() - 44];
    short[] input = new short[(int)(byteInput.length / 2f)];


    try{

        FileInputStream fis = new FileInputStream(file);
        fis.read(byteInput, 44, byteInput.length - 45);
        ByteBuffer.wrap(byteInput).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(input);

    }catch(Exception e  ){
        e.printStackTrace();
    }

你的样本值在 short[] input 中!


什么意思:file.length() - 44 ...你是怎么得出这些数字的? - To Kra
这只是一段非常糟糕的代码。WAV是一个可以容纳几乎任何音频格式(甚至mp3)的容器。没有理由假设一个WAV文件包含16位PCM。还有,假设声音数据出现在文件中的固定位置是错误的。 - Radiodef

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接