PCM編碼的WAV文件,如何使用JAVA把兩個單聲道文件合併成一個雙聲道文件?

從大網可以拿到通話雙方的錄音文件,都是PCM編碼的WAV文件。有客戶的需求是隻要一個通話錄音文件,但是,要求文件包含雙聲道,每個聲道對應通話一方。
在原來WAV文件頭解析的基礎上,完成起來還是比較容易的。

public class WavFileUtil {
    private static Logger LOG = LoggerFactory.getLogger(WavFileUtil.class);

    private static final int HEAD_LENGTH = 12;

    private static final int FORMAT_LENGTH = 24;

    private static boolean isWav(byte[] head) {
        return ("RIFF".equals(new String(head, 0, 4, ISO_8859_1)) &&
                "WAVE".equals(new String(head, 8, 4, ISO_8859_1)));
    }

    private static void fileTooSmall(byte[] file) {
        if (file.length < HEAD_LENGTH + FORMAT_LENGTH) {
            LOG.warn("file is too small, size if {}.", file.length);
            throw new IvcFileException();
        }
    }

    private static int headSize() {
        return HEAD_LENGTH + FORMAT_LENGTH;
    }

    /**
     * resolve wav file head.
     * ChunkID,ChunkSize,Format, everyone 4 bytes.
     */
    public static int fileSize(byte[] file) {
        fileTooSmall(file);

        byte[] head = copyOfRange(file, 0, HEAD_LENGTH);

        if (isWav(head)) {
            return ByteBuffer.wrap(copyOfRange(head, 4, 8))
                    .order(LITTLE_ENDIAN)
                    .getInt() + 8;
        } else {
            LOG.warn("file format error: expected {}, actual {}.",
                    "[82, 73, 70, 70, *, *, *, *, 87, 65, 86, 69]",
                    head);
            throw new IvcFileException();
        }
    }

    public static AudioFormat fileFormat(byte[] file) {
        fileTooSmall(file);

        byte[] head = copyOfRange(file, 0, HEAD_LENGTH);

        if (isWav(head)) {
            byte[] format = copyOfRange(file, 12, HEAD_LENGTH + FORMAT_LENGTH);
            String chuckID = new String(format, 0, 4, ISO_8859_1);
            int chunkSize = ByteBuffer.wrap(copyOfRange(format, 4, 8))
                    .order(LITTLE_ENDIAN).getInt();
            int audioFmt = ByteBuffer.wrap(copyOfRange(format, 8, 10))
                    .order(LITTLE_ENDIAN).getShort();
            int channels = ByteBuffer.wrap(copyOfRange(format, 10, 12))
                    .order(LITTLE_ENDIAN).getShort();
            int sampleRate = ByteBuffer.wrap(copyOfRange(format, 12, 16))
                    .order(LITTLE_ENDIAN).getInt();
            int byteRate = ByteBuffer.wrap(copyOfRange(format, 16, 20))
                    .order(LITTLE_ENDIAN).getInt();
            int frameSize = ByteBuffer.wrap(copyOfRange(format, 20, 22))
                    .order(LITTLE_ENDIAN).getShort();
            int sampleSizeInBits = ByteBuffer.wrap(copyOfRange(format, 22, 24))
                    .order(LITTLE_ENDIAN).getShort();

            return new AudioFormat(PCM_SIGNED, sampleRate,
                    sampleSizeInBits, channels, frameSize, sampleRate, false);
        } else {
            LOG.warn("file is not a wav.");
            throw new IvcFileException();
        }
    }

    public static void merge(final byte[] left, final byte[] right, final String path) {
        int leftSize = fileSize(left);
        int rightSize = fileSize(right);
        int mergeSize = mergeSizeField(leftSize, rightSize);
        int mergeDataSize = mergeDataSize(leftSize, rightSize);

        try (RandomAccessFile file = new RandomAccessFile(path, "rw")) {
            file.write(mergeHead(left, mergeSize));
            file.write(dataChunkHead(mergeDataSize));
            int max = Math.max(leftSize, rightSize);
            for (int i = headSize() + 8; i < max + 8; i += 2) {
                file.write(read(left, i));
                file.write(read(right, i));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static byte[] read(final byte[] content, int offset) {
        if (content.length > offset) {
            return copyOfRange(content, offset, offset + 2);
        } else {
            return "\0\0".getBytes(ISO_8859_1);
        }
    }

    private static int mergeSizeField(int left, int right) {
        int max = Math.max(left - 8, right - 8);
        return max * 2;
    }

    private static int mergeDataSize(int left, int right) {
        int max = Math.max(left - headSize() - 8, right - headSize() - 8);
        return max * 2;
    }

    private static byte[] mergeHead(final byte[] left, final int mergeSize) {
        AudioFormat format = fileFormat(left);
        ByteBuffer size = ByteBuffer.allocate(4).order(LITTLE_ENDIAN).putInt(mergeSize);

        ByteBuffer channels = ByteBuffer.allocate(2).order(LITTLE_ENDIAN).putShort((short) 2);
        ByteBuffer sampleRate = ByteBuffer.allocate(4).order(LITTLE_ENDIAN)
                .putInt((int) format.getSampleRate());
        ByteBuffer byteRate = ByteBuffer.allocate(4).order(LITTLE_ENDIAN)
                .putInt((int) format.getSampleRate() * 2 * format.getSampleSizeInBits() / 8);
        ByteBuffer blockAlign = ByteBuffer.allocate(2).order(LITTLE_ENDIAN)
                .putShort((short) (2 * format.getSampleSizeInBits() / 8));
        ByteBuffer bitsPerSample = ByteBuffer.allocate(2).order(LITTLE_ENDIAN)
                .putShort((short) format.getSampleSizeInBits());

        ByteBuffer head = ByteBuffer.allocate(headSize());
        head.put(left, 0, 4);

        head.put(size.array());
        head.put(left, 8, 14);

        head.put(channels.array());
        head.put(sampleRate.array());
        head.put(byteRate.array());
        head.put(blockAlign.array());
        head.put(bitsPerSample.array());
        return head.array();
    }

    private static byte[] dataChunkHead(final int length) {
        ByteBuffer head = ByteBuffer.allocate(8);
        head.put("data".getBytes(ISO_8859_1));
        ByteBuffer size = ByteBuffer.allocate(4).order(LITTLE_ENDIAN).putInt(length);
        head.put(size.array());
        return head.array();
    }
}

文件合併的入口是merge方法,另外,mergeHead方法生成新的文件頭,dataChunkHead方法生成新的數據chunk頭。

使用實際的錄音文件做一下測試:

    @Test
    public void merge1() {
        try {
            byte[] left = Files.readAllBytes(Paths.get("recordFiles/", "in-1.wav"));
            byte[] right = Files.readAllBytes(Paths.get("recordFiles/", "out-1.wav"));

            merge(left, right, "recordFiles/11111.wav");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

生成的文件正確,通話的一方佔據一個聲道。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章