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

2022-09-01 14:11:12

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


答案 1

我通过 .Java Sound Tutorials 中的以下片段运行良好。AudioInputStream

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,我发现这很棘手。从表面上看,这似乎是一个循环问题,写入的命令依赖于 a 作为参数。AudioInputStream

但是如何将字节写入?难道不应该有一个?AudioInputStreamAudioOutputStream

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

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

由于此方法可能会被多次调用,因此还应该有一个实例变量来指示一个人在数据中取得了多大进展,并将其作为上述方法的一部分进行更新。read

当你实现了这个方法,那么你的对象就可以用在中创建一个新的,而这个对象又可以与:AudioInputStream

AudioSystem.write(yourAudioInputStream, AudioFileFormat.WAV, yourFileDestination)

作为提醒,可以使用 以 创建一个作为源。AudioInputStreamTargetDataLine

至于直接操作数据,我已经成功地对上面片段示例最里面的循环中的缓冲区中的数据进行了操作。audioBytes

当您处于该内部循环中时,您可以将字节转换为整数或浮点数,并将值相乘(范围从到),然后将其转换回小字节序字节。volume0.01.0

我相信,由于您可以访问该缓冲区中的一系列样本,因此您还可以在该阶段使用各种形式的DSP滤波算法。根据我的经验,我发现最好直接对该缓冲区中的数据进行体积更改,因为这样您就可以进行尽可能小的增量:每个样本一个增量,从而最大限度地减少由于体积引起的不连续性而导致的点击机会。

我发现Java提供的音量“控制线”倾向于音量跳跃会导致点击的情况,我相信这是因为deltas仅在单个缓冲区读取的粒度上实现(通常在每1024个样本一次变化的范围内),而不是将更改分成更小的部分并为每个样本添加一个。但是我不知道音量控制是如何实现的,所以请把这个猜想当作一粒盐。

总而言之,Java.Sound一直是一个真正令人头疼的问题。我责怪教程没有包含直接从字节写入文件的显式示例。我责怪教程将播放文件编码的最佳示例埋没在“如何转换...”中。部分。但是,该教程中有很多有价值的免费信息。


编辑: 12/13/17

此后,我使用以下代码在我自己的项目中从PCM文件编写音频。而不是实现,可以扩展并将其用作方法的参数。TargetDataLineInputStreamAudioSystem.write

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


答案 2

这是直接写入 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();
    }
}

推荐