将PCM录制的数据写入.wav文件(Java Android)

37

我正在使用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个字节)的十六进制值:例如52 49 46 46 E1 C3 ... - Zelimir
5个回答

17
我也曾经面对这个问题好几个小时,我的问题主要是在使用16位录音时必须非常小心地写入输出。WAV文件期望以Little Endian格式接收数据,但使用writeShort将其作为Big Endian写入输出。我在使用其他功能时也得到了有趣的结果,所以我返回按正确顺序编写字节并且那样可以运行。
在调试过程中,我广泛使用了十六进制编辑器。我建议你也这样做。另外,在上面的答案中使用的标头是有效的,我用它来检查我的代码与之匹配,该标头相当可靠。

11
根据标题,我已经按照以下代码进行了操作(如果有帮助的话)。
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);

2
你能提供更多关于变量(例如totalDataLenbyteRate)的细节信息(例如可能的值),以及它们之间的关系吗?此外,header[32]的值不依赖于任何变量吗? - Kaarel
3
如果不知道如何填写这些变量,那么这段代码将无法正常工作。你可以在这里找到相关信息。此外,如果通道数值>255,则该值并不正确。尽管看起来不太可能,但我正在处理使用任意输入通道并存储为WAV文件的项目,因此您的写入程序将无法使用。字段应为: header[22] = (byte) (channels & 0xFF); header[23] = (byte) ((channels >> 8) & 0xFF); 另一个改进是: header[32] = (byte) ((channels * RECORDER_BPP) / 8); // block align - EntangledLoops
谢谢!帮助强调了需要在“fmt”后加上一个空格。 - Joe Maher

4
你确定字节顺序吗?“RIFF”、“WAV”、“fmt”和“data”看起来都没问题,但头部中的数字可能需要不同的顺序(小端 vs. 大端)。你也不需要使用你的intToByteArray方法手动转换成字节。你可以使用DataOutputStream的writeInt和writeShort方法。对于第一个,它应该是这样的:
outFile.writeInt(Integer.reverseBytes((int)myChunkSize));
对于shorts,它应该像这样:
outFile.writeShort(Short.reverseBytes((short)myFormat))
这样你也不需要提供偏移量和长度(0, 4)数字。很好。

3

正如Ronald Kunenborg所说,问题在于小端/大端转换。

最简单的方法是编写一个类似以下的帮助程序:

public static void writeShortLE(DataOutputStream out, short value) {
  out.writeByte(value & 0xFF);
  out.writeByte((value >> 8) & 0xFF);
}

如果你在Android上记录音频到wave文件,并且还需要短数组,那么这将非常有帮助。

(来源:https://dev59.com/8nM_5IYBdhLWcg3wZSPX#1394839


1
以下是创建一个带有正确头文件的小端格式.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"));
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接