在Java程序中,读取音频文件(WAV文件)并将其转换为数字数组(float[]
, short[]
等),以及从数字数组写入WAV文件,最佳的方法是什么?
我通过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。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位、立体声、小端字节序。
对于我的特定应用程序,我省略了skip
和markSupported
方法。但如果需要,添加它们应该不难。
这是将音频直接写入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();
}
}
如果您只需要原始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);
writer.write(new VoodooAudioInputStream(numbers), AudioFileFormat.Type.WAVE, outStream);
正如@stacker所提到的,你当然应该熟悉API。
如果你需要访问实际的样本值,javax.sound.sample包不适用于处理WAV文件。该包可让你更改音量、采样率等,但如果需要其他效果(如添加回声),则需自行实现。(Java教程暗示可以直接处理样本值,但技术作家过度承诺了。)
这个网站提供了一个简单的类来处理WAV文件:http://www.labbookpages.co.uk/audio/javaWavFiles.html
WAV文件规范 https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
有一个API可以满足您的需求 http://code.google.com/p/musicg/
首先,您可能需要了解WAVE结构的头文件和数据位置,可以在此处找到规范:https://ccrma.stanford.edu/courses/422/projects/WaveFormat/。 请注意,数据是小端序的。
有一个API可以帮助您实现目标。
如果有人仍然需要,我正在开发一个音频框架,旨在解决这个和类似的问题。虽然它是基于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音频。
我使用一些神奇的方法,结合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
中!