Java - 读取、操作和写入 WAV 文件 [英] Java - reading, manipulating and writing WAV files

查看:38
本文介绍了Java - 读取、操作和写入 WAV 文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

解决方案

我通过 AudioInputStream 读取 WAV 文件.Java Sound Tutorials 中的以下片段运行良好.

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

要编写 WAV,我发现这很棘手.从表面上看,这似乎是一个循环问题,写入的命令依赖于 AudioInputStream 作为参数.

但是如何将字节写入AudioInputStream?不应该有 AudioOutputStream 吗?

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

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

由于此方法可能会被多次调用,因此还应该有一个实例变量来指示数据的进展程度,并将其更新为上述 read 方法的一部分.

当你实现了这个方法后,你的对象就可以用来创建一个新的AudioInputStream,它又可以用于:

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

<块引用>

提醒一下,可以使用 TargetDataLine 作为源创建 AudioInputStream.

至于直接操作数据,我在上面代码片段示例的最内层循环中对缓冲区中的数据进行了操作,取得了很好的成功,audioBytes.

当您处于内部循环中时,您可以将字节转换为整数或浮点数并乘以 volume 值(范围从 0.01.0),然后将它们转换回小端字节.

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

我找到了控制线"对于 Java 提供的音量,往往出现音量跳跃会导致点击的情况,我相信这是因为增量仅在单个缓冲区读取的粒度上实现(通常在每 1024 个样本一次更改的范围内)而不是将变化分成更小的部分,并为每个样本添加一个.但我不知道音量控制是如何实现的,所以请持保留态度.

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


12/13/17

我已经使用以下代码从我自己的项目中的 PCM 文件中写入音频.可以扩展 InputStream 并将其用作 AudioSystem.write 方法的参数,而不是实现 TargetDataLine.

 公共类 StereoPcmInputStream 扩展 InputStream{私有浮点[] 数据帧;私有 int 帧计数器;私有整数游标;private int[] pcmOut = new int[2];private int[] frameBytes = new int[4];私有 int idx;私有 int 帧读取;public void setDataFrames(float[] dataFrames){this.dataFrames = 数据帧;frameToRead = dataFrames.length/2;}@覆盖public int read() 抛出 IOException{而(可用()> 0){idx &= 3;if (idx == 0)//设置下一帧的数据值{帧计数器++;//计算经过的帧数//缩放到 16 位pcmOut[0] = (int)(dataFrames[cursor++] * Short.MAX_VALUE);pcmOut[1] = (int)(dataFrames[cursor++] * Short.MAX_VALUE);//输出为无符号字节,范围为 [0..255]frameBytes[0] = (char)pcmOut[0];frameBytes[1] = (char)(pcmOut[0] > > 8);frameBytes[2] = (char)pcmOut[1];frameBytes[3] = (char)(pcmOut[1] > > 8);}返回 frameBytes[idx++];}返回-1;}@覆盖公共整数可用(){//注意:不是并发安全的.//总和的第一半:每帧有 4 次读取可用//sum 的第二部分:当前帧中剩余要读取的字节数返回 4 * ((framesToRead - 1) - framesCounter)+ (4 - (idx % 4));}@覆盖公共无效重置(){光标 = 0;帧计数器 = 0;idx = 0;}@覆盖公共无效关闭(){System.out.println(StereoPcmInputStream 在读取帧后停止:"+ 帧计数器);}}

这里要导出的源数据是立体声浮点数的形式,范围从 -1 到 1.结果流的格式是 16 位、立体声、小端.

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

In a Java program, what is the best way to read an audio file (WAV file) to an array of numbers (float[], short[], ...), and to write a WAV file from an array of numbers?

解决方案

I read WAV files via an AudioInputStream. The following snippet from the Java Sound Tutorials works well.

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...
}

To write a WAV, I found that quite tricky. On the surface it seems like a circular problem, the command that writes relies on an AudioInputStream as a parameter.

But how do you write bytes to an AudioInputStream? Shouldn't there be an AudioOutputStream?

What I found was that one can define an object that has access to the raw audio byte data to implement TargetDataLine.

This requires a lot of methods be implemented, but most can stay in dummy form as they are not required for writing data to a file. The key method to implement is read(byte[] buffer, int bufferoffset, int numberofbytestoread).

As this method will probably be called multiple times, there should also be an instance variable that indicates how far through the data one has progressed, and update that as part of the above read method.

When you have implemented this method, then your object can be used in to create a new AudioInputStream which in turn can be used with:

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

As a reminder, an AudioInputStream can be created with a TargetDataLine as a source.

As to the direct manipulating the data, I have had good success acting on the data in the buffer in the innermost loop of the snippet example above, audioBytes.

While you are in that inner loop, you can convert the bytes to integers or floats and multiply a volume value (ranging from 0.0 to 1.0) and then convert them back to little endian bytes.

I believe since you have access to a series of samples in that buffer you can also engage various forms of DSP filtering algorithms at that stage. In my experience I have found that it is better to do volume changes directly on data in this buffer because then you can make the smallest possible increment: one delta per sample, minimizing the chance of clicks due to volume-induced discontinuities.

I find the "control lines" for volume provided by Java tend to situations where the jumps in volume will cause clicks, and I believe this is because the deltas are only implemented at the granularity of a single buffer read (often in the range of one change per 1024 samples) rather than dividing the change into smaller pieces and adding them one per sample. But I'm not privy to how the Volume Controls were implemented, so please take that conjecture with a grain of salt.

All and all, Java.Sound has been a real headache to figure out. I fault the Tutorial for not including an explicit example of writing a file directly from bytes. I fault the Tutorial for burying the best example of Play a File coding in the "How to Convert..." section. However, there's a LOT of valuable FREE info in that tutorial.


EDIT: 12/13/17

I've since used the following code to write audio from a PCM file in my own projects. Instead of implementing TargetDataLine one can extend InputStream and use that as a parameter to the AudioSystem.write method.

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);
    }
}

The source data to be exported here is in the form of stereo floats ranging from -1 to 1. The format of the resulting stream is 16-bit, stereo, little-endian.

I omitted skip and markSupported methods for my particular application. But it shouldn't be difficult to add them if they are needed.

这篇关于Java - 读取、操作和写入 WAV 文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆