Java - 读取,操作和编写WAV文件

时间:2010-07-21 09:14:41

标签: java audio wav

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

9 个答案:

答案 0 :(得分:9)

我通过AudioInputStream阅读了WAV文件。 Java Sound Tutorials的以下代码段效果很好。

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) 又可用于:

AudioInputStream
  

提醒一下,可以使用TargetDataLine作为来源创建audioBytes

关于直接操作数据,我在上面的代码片段volume的最里面的循环中对缓冲区中的数据进行了很好的成功。

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

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

我找到"控制线"由Java提供的卷倾向于卷的跳跃会导致点击,我相信这是因为增量只能在单个缓冲区读取的粒度上实现(通常在每1024个样本一个更改的范围内)而不是将更改分成更小的部分,并将每个样本添加一个。但是我并不知道音量控制是如何实现的,所以请把这个猜想搞得一团糟。

总而言之,Java.Sound一直是一个令人头痛的问题。我错误的教程没有包括直接从字节写文件的明确示例。我错过了教程,以便在"如何转换......"中掩埋播放文件编码的最佳示例。部分。但是,该教程中有很多有价值的免费信息。


编辑:12/13/17

我使用以下代码在我自己的项目中从PCM文件中写入音频。可以扩展InputStream而不是实现AudioInputStream.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); } } 方法的参数。

skip

此处要导出的源数据采用立体声浮点数的形式,范围从-1到1.结果流的格式为16位,立体声,小端。

我为我的特定应用省略了markSupported和{{1}}方法。但如果需要的话,添加它们并不困难。

答案 1 :(得分:8)

有关您希望实现的内容的更多细节将会有所帮助。如果原始WAV数据适合您,只需使用FileInputStream和可能的扫描仪将其转换为数字。但是,让我试着给你一些有意义的示例代码,以帮助你入门:

为此目的,有一个名为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。

答案 2 :(得分:6)

如果您需要访问实际样本值,则javax.sound.sample包不适合处理WAV文件。该软件包允许您更改音量,采样率等,但如果您想要其他效果(例如,添加回声),则您可以自己动手。 (Java教程暗示应该可以直接处理样本值,但技术作者过度表达。)

此网站有一个处理WAV文件的简单类:http://www.labbookpages.co.uk/audio/javaWavFiles.html

答案 3 :(得分:5)

答案 4 :(得分:4)

这是直接写入wav文件的源代码。 你只需要知道数学和声音工程就可以产生你想要的声音。 在此示例中,等式计算双耳节拍。

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

public class Example 
{
    public static void main(String[] args) throws  IOException {

    double sampleRate = 44100.0;
    double frequency = 440;
    double frequency2 = 90;
    double amplitude = 1.0;
    double seconds = 2.0;
    double twoPiF = 2 * Math.PI * frequency;
    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((double)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;
    i++;
    byteBuffer[i] = (byte) (x >>> 8);
    }
    File out = new File("out10.wav");
    boolean bigEndian = false;
    boolean signed = true;
    int bits = 16;
    int channels = 1;
    AudioFormat format;
    format = new AudioFormat((float)sampleRate, bits, channels, signed, bigEndian);
    ByteArrayInputStream bais = new ByteArrayInputStream(byteBuffer);
    AudioInputStream audioInputStream;
    audioInputStream = new AudioInputStream(bais, format,buffer.length);
    AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, out);
    audioInputStream.close();
    }

}

如果你可以修改它来创建一个很酷的嘻哈次低音,因为这是我正在尝试修改这个程序的目的。

答案 5 :(得分:2)

javax.sound.sample package

支持wave文件

由于不是一个简单的API,你应该阅读一篇介绍API的文章/教程,如

Java Sound, An Introduction

答案 6 :(得分:2)

首先,您可能需要知道WAVE结构的标题和数据位置,您可以找到规范here。 请注意,数据是小端的。

有一个API可以帮助您实现目标。

答案 7 :(得分:0)

如果仍然有人需要它,那么我正在开发一个音频框架,旨在解决该问题和类似问题。虽然在科特林上。您可以在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 Audio。

答案 8 :(得分:-1)

我使用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