将 PCM 录制的数据写入 .wav 文件(java android)
Posted
技术标签:
【中文标题】将 PCM 录制的数据写入 .wav 文件(java android)【英文标题】:Writing PCM recorded data into a .wav file (java android) 【发布时间】:2012-02-29 01:47:21 【问题描述】:我正在使用 AudioRecord 在 android 中录制 16 位 PCM 数据。记录数据并将其保存到文件后,我将其读回以将其保存为 .wav 文件。
问题是媒体播放器可以识别 WAV 文件,但播放的只是纯噪音。目前我最好的猜测是我的 wav 文件头不正确,但我一直无法看到究竟是什么问题。 (我认为这是因为我可以播放我在 Audacity 中录制的原始 PCM 数据)
这是我读取原始 PCM 文件并将其保存为 .wav 的代码:
private void properWAV(File fileToConvert, float newRecordingID)
try
long mySubChunk1Size = 16;
int myBitsPerSample= 16;
int myFormat = 1;
long myChannels = 1;
long mySampleRate = 22100;
long myByteRate = mySampleRate * myChannels * myBitsPerSample/8;
int myBlockAlign = (int) (myChannels * myBitsPerSample/8);
byte[] clipData = getBytesFromFile(fileToConvert);
long myDataSize = clipData.length;
long myChunk2Size = myDataSize * myChannels * myBitsPerSample/8;
long myChunkSize = 36 + myChunk2Size;
OutputStream os;
os = new FileOutputStream(new File("/sdcard/onefile/assessor/OneFile_Audio_"+ newRecordingID+".wav"));
BufferedOutputStream bos = new BufferedOutputStream(os);
DataOutputStream outFile = new DataOutputStream(bos);
outFile.writeBytes("RIFF"); // 00 - RIFF
outFile.write(intToByteArray((int)myChunkSize), 0, 4); // 04 - how big is the rest of this file?
outFile.writeBytes("WAVE"); // 08 - WAVE
outFile.writeBytes("fmt "); // 12 - fmt
outFile.write(intToByteArray((int)mySubChunk1Size), 0, 4); // 16 - size of this chunk
outFile.write(shortToByteArray((short)myFormat), 0, 2); // 20 - what is the audio format? 1 for PCM = Pulse Code Modulation
outFile.write(shortToByteArray((short)myChannels), 0, 2); // 22 - mono or stereo? 1 or 2? (or 5 or ???)
outFile.write(intToByteArray((int)mySampleRate), 0, 4); // 24 - samples per second (numbers per second)
outFile.write(intToByteArray((int)myByteRate), 0, 4); // 28 - bytes per second
outFile.write(shortToByteArray((short)myBlockAlign), 0, 2); // 32 - # of bytes in one sample, for all channels
outFile.write(shortToByteArray((short)myBitsPerSample), 0, 2); // 34 - how many bits in a sample(number)? usually 16 or 24
outFile.writeBytes("data"); // 36 - data
outFile.write(intToByteArray((int)myDataSize), 0, 4); // 40 - how big is this data chunk
outFile.write(clipData); // 44 - the actual data itself - just a long string of numbers
outFile.flush();
outFile.close();
catch (IOException e)
e.printStackTrace();
private static byte[] intToByteArray(int i)
byte[] b = new byte[4];
b[0] = (byte) (i & 0x00FF);
b[1] = (byte) ((i >> 8) & 0x000000FF);
b[2] = (byte) ((i >> 16) & 0x000000FF);
b[3] = (byte) ((i >> 24) & 0x000000FF);
return b;
// convert a short to a byte array
public static byte[] shortToByteArray(short data)
/*
* NB have also tried:
* return new byte[](byte)(data & 0xff),(byte)((data >> 8) & 0xff);
*
*/
return new byte[](byte)(data & 0xff),(byte)((data >>> 8) & 0xff);
我没有包含 getBytesFromFile(),因为它占用了太多空间,而且它是一种久经考验的方法。 无论如何,这是进行实际录制的代码:
public void run()
Log.i("ONEFILE", "Starting main audio capture loop...");
int frequency = 22100;
int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
final int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
AudioRecord audioRecord = new AudioRecord(MediaRecorder.Audiosource.MIC, frequency, channelConfiguration, audioEncoding, bufferSize);
audioRecord.startRecording();
ByteArrayOutputStream recData = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(recData);
short[] buffer = new short[bufferSize];
audioRecord.startRecording();
while (!stopped)
int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
for(int i = 0; i < bufferReadResult;i++)
try
dos.writeShort(buffer[i]);
catch (IOException e)
e.printStackTrace();
audioRecord.stop();
try
dos.flush();
dos.close();
catch (IOException e1)
e1.printStackTrace();
audioRecord.stop();
byte[] clipData = recData.toByteArray();
File file = new File(audioOutputPath);
if(file.exists())
file.delete();
file = new File(audioOutputPath);
OutputStream os;
try
os = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(os);
DataOutputStream outFile = new DataOutputStream(bos);
outFile.write(clipData);
outFile.flush();
outFile.close();
catch (FileNotFoundException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
请提出可能出了什么问题。
【问题讨论】:
能否请您显示您的标题是什么样的。例如,前 48 个字节(3 行,每行 16 个字节)显示为 HEX 值:例如52 49 46 46 E1 C3 ... 【参考方案1】:我已经为这个完全相同的问题苦苦挣扎了好几个小时,而我的问题主要是在以 16 位录制时,您必须非常小心写入输出的内容。 WAV 文件需要 Little Endian 格式的数据,但使用 writeShort 会将其作为 Big Endian 写入输出。在使用其他函数时,我也得到了有趣的结果,因此我返回以正确的顺序写入字节并且有效。
我在调试时广泛使用了十六进制编辑器。我可以建议你也这样做。另外,上面答案中的标题有效,我用它来检查我自己的代码,这个标题相当万无一失。
【讨论】:
【参考方案2】:根据标题的问题,我已遵循此代码(如果它对您有所帮助)。
byte[] header = new byte[44];
header[0] = 'R'; // RIFF/WAVE header
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1; // format = 1
header[21] = 0;
header[22] = (byte) channels;
header[23] = 0;
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) (2 * 16 / 8); // block align
header[33] = 0;
header[34] = RECORDER_BPP; // bits per sample
header[35] = 0;
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
【讨论】:
您能否提供有关变量(例如totalDataLen
、byteRate
)的更多详细信息(例如可能的值)以及它们之间的关系?还有header[32]
的值不依赖于任何变量?
如果不知道如何填充这些变量,这将无法完全按原样工作。您可以找到该信息here。此外,如果值 > 255,则通道数不正确。尽管看起来不太可能,但我正在处理使用任意输入通道并存储为 WAV 的项目,因此您的作者将无法工作。字段应该是:header[22] = (byte) (channels & 0xFF); header[23] = (byte) ((channels >> 8) & 0xFF);
另外,另一个改进是:header[32] = (byte) ((channels * RECORDER_BPP) / 8); // block align
谢谢!帮助突出显示“fmt”需要一个空格【参考方案3】:
您确定字节顺序吗? “RIFF”、“WAV”、“fmt”和“data”看起来不错,但标题中的数字可能需要不同的顺序(小端与大端)。您也不需要使用 intToByteArray
方法手动转换为字节。您可以使用DataOutputStream
的writeInt
和writeShort
方法。对于第一个,这看起来像:
outFile.writeInt(Integer.reverseBytes((int)myChunkSize));
短裤是这样的:
outFile.writeShort(Short.reverseBytes((short)myFormat))
这样您也不需要提供偏移量和长度(0, 4)
数字。挺好的。
【讨论】:
【参考方案4】:正如 Ronald Kunenborg 正确指出的那样,问题在于 Litte Endian / Big Endian 转换。
最简单的方法是像这样编写一个简短的助手:
public static void writeShortLE(DataOutputStream out, short value)
out.writeByte(value & 0xFF);
out.writeByte((value >> 8) & 0xFF);
如果您使用 Android 将音频录制到波形文件并且您也需要短数组,这将非常有用。
(学分:https://***.com/a/1394839/1686216)
【讨论】:
【参考方案5】:以下是一个创建带有适当标题的 Little Endian 格式的 .wav 文件的示例。然后它会在指定的持续时间内附加空音频数据(以秒为单位)。如果您想附加录制的音频。
public static void createEmptyWaveFile(int sampleRate, short sampleSize, short channels, int duration, File file)
throws IOException
// calculate some
short blockAlign = (short) (sampleSize * channels / 8);
int byteRate = sampleRate * sampleSize * channels / 8;
int audioSize = byteRate * duration;
int fileSize = audioSize + 44;
// create an Array that contains empty audio data for the given duration
byte[] audioData = new byte[audioSize];
for (int i = 0; i < audioData.length; i++)
audioData[i] = (byte) 0;
// The stream that writes the audio file to the disk
DataOutputStream out = new DataOutputStream(new FileOutputStream(file));
// Write Header
out.writeBytes("RIFF");// 0-4 ChunkId always RIFF
out.writeInt(Integer.reverseBytes(fileSize));// 5-8 ChunkSize always audio-length +header-length(44)
out.writeBytes("WAVE");// 9-12 Format always WAVE
out.writeBytes("fmt ");// 13-16 Subchunk1 ID always "fmt " with trailing whitespace
out.writeInt(Integer.reverseBytes(16)); // 17-20 Subchunk1 Size always 16
out.writeShort(Short.reverseBytes((short) 1));// 21-22 Audio-Format 1 for PCM PulseAudio
out.writeShort(Short.reverseBytes(channels));// 23-24 Num-Channels 1 for mono, 2 for stereo
out.writeInt(Integer.reverseBytes(sampleRate));// 25-28 Sample-Rate
out.writeInt(Integer.reverseBytes(byteRate));// 29-32 Byte Rate
out.writeShort(Short.reverseBytes(blockAlign));// 33-34 Block Align
out.writeShort(Short.reverseBytes(sampleSize));// 35-36 Bits-Per-Sample
out.writeBytes("data");// 37-40 Subchunk2 ID always data
out.writeInt(Integer.reverseBytes(audioSize));// 41-44 Subchunk 2 Size audio-length
// Append the silent audio data or what you recorded from the mic
out.write(audioData);
out.close();// close the stream properly
public static void main(String[] args) throws IOException
createEmptyWaveFile(44100, (short) 16, (short) 2, 10, new File("out.wav"));
【讨论】:
以上是关于将 PCM 录制的数据写入 .wav 文件(java android)的主要内容,如果未能解决你的问题,请参考以下文章
写入 PCM 数据时的文件 (.wav) 持续时间 @16KBps
android上的qt5不使用QAudioRecorder录制PCM wav文件