代码之家  ›  专栏  ›  技术社区  ›  Gonzalo Acosta

Java录制/混合两个音频流

  •  4
  • Gonzalo Acosta  · 技术社区  · 12 年前

    我有一个java应用程序,它记录混音器中的音频,并将其存储在字节数组中,或者将其保存到文件中。 我需要的是同时从两个混音器中获取音频,并将其保存到音频文件中(我正在尝试使用.wav)。 问题是,我可以得到两个字节的数组,但不知道如何合并它们(我所说的“合并”不是指连接)。 具体地说,这是一个通过USB调制解调器处理对话的应用程序,我需要记录它们(流是每个说话的人的声音,已经被映像为单独记录它们)。

    有关于怎么做的线索吗?

    这是我的代码:

    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.nio.file.Path;
    
    public class FileMixer {
    
        Path path1 = Paths.get("/file1.wav");
        Path path2 = Paths.get("/file2.wav");
        byte[] byte1 = Files.readAllBytes(path1);
        byte[] byte2 = Files.readAllBytes(path2);
        byte[] out = new byte[byte1.length];
    
        public FileMixer() {
    
            byte[] byte1 = Files.readAllBytes(path1);
            byte[] byte2 = Files.readAllBytes(path2);
    
            for (int i=0; i<byte1.Length; i++)
                out[i] = (byte1[i] + byte2[i]) >> 1;
    
        }
    }
    

    提前谢谢

    2 回复  |  直到 9 年前
        1
  •  6
  •   leif.gruenwoldt    9 年前

    要以数字方式混合声波,需要将两个文件中的每个对应数据点添加到一起。

    for (int i=0; i<source1.length; i++)
        result[i] = (source1[i] + source2[i]) >> 1;
    

    换句话说,您从字节数组1中获取项0,从字节数组2中获取项0。将它们相加,并将结果数字放在结果数组的项0中。对其余值重复此操作。为了防止过载,您可能需要将每个结果值除以二。

        2
  •  1
  •   Huhngut    4 年前

    确保合并幅度数据,而不仅仅是字节数据。如果采样率为8:一个字节等于一个振幅数据。但如果它是16,您需要将两个字节添加到一个短字节中,并将它们合并。

    当前您正在像这样加载文件

    byte[] byte1 = Files.readAllBytes(path1);
    

    这也会将.wav文件头加载到字节数组中,但您只想合并实际的音频数据。加载方式如下:

    public static ByteBuffer loadFile(File file) throws IOException {
            DataInputStream in = new DataInputStream(new FileInputStream(file));
            byte[] sound = new byte[in.available() - 44];
            in.skipNBytes(44); // skip the header
            in.read(sound);
            return ByteBuffer.wrap(sound);
        }
    

    然后,您可以根据样本大小合并这些缓冲区的每个字节或每两个字节。我将使用16作为更常见的。

    public static ByteBuffer mergeAudio(ByteBuffer smaller, ByteBuffer larger) {
            // When we merge we will get problems with LittleEndian/BigEndian
            // Actually the amplitude data is stored reverse in the .wav fille
            // When we extract the amplitude value we need to reverse it to get the actuall
            // value
            // We can then add up all the amplitude data and divide it by their amount to
            // get the mean
            // When we save the value we need to reverse it again
    
            // The result will have the size of the larger audio file. In my case its file2
            ByteBuffer result = ByteBuffer.allocate(larger.capacity());
    
            while (larger.hasRemaining()) {
    
                // getShort() for SampleSize 16bit get() for 8 bit.
                // Reverse the short because of LittleEndian/BigEndian
                short sum = Short.reverseBytes(larger.getShort());
                int matches = 1;
    
                // check if the smaller file still has content so it needs to merge
                if (smaller.hasRemaining()) {
                    // getShort() for SampleSize 16bit get() for 8 bit
    
                    // Reverse the short because of LittleEndian/BigEndian
                    sum += Short.reverseBytes(smaller.getShort());
                    matches++;
                }
    
                // append the mean of all merged values
                // reverse again
                result.putShort(Short.reverseBytes((short) (sum / (float) matches)));
    
            }
    
            return result;
        }
    

    我们现在需要创建自己的.wav文件头并附加合并后的数据。最后,我们可以将更改写入磁盘。

    public static void saveToFile(File file, byte[] audioData) throws IOException {
    
            int audioSize = audioData.length;
            int fileSize = audioSize + 44;
    
            // 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(audioFormat));// 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
    
            out.write(audioData);// append the merged data
            out.close();// close the stream properly
        }
    

    要合并的两个文件必须相同,这一点很重要
    频道、采样大小、采样率、音频格式
    这就是计算表头数据的方式:

    private static short audioFormat = 1;
    private static int sampleRate = 44100;
    private static short sampleSize = 16;
    private static short channels = 2;
    private static short blockAlign = (short) (sampleSize * channels / 8);
    private static int byteRate = sampleRate * sampleSize * channels / 8;
    

    以下是您的工作示例,我将所有内容放在一起:

    
    import static java.lang.Math.ceil;
    import static java.lang.Math.round;
    
    import java.io.DataOutputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.util.ArrayList;
    
    public class AudioMerger {
    
        private short audioFormat = 1;
        private int sampleRate = 44100;
        private short sampleSize = 16;
        private short channels = 2;
        private short blockAlign = (short) (sampleSize * channels / 8);
        private int byteRate = sampleRate * sampleSize * channels / 8;
        private ByteBuffer audioBuffer;
        private ArrayList<MergeSound> sounds = new ArrayList<MergeSound>();
        private ArrayList<Integer> offsets = new ArrayList<Integer>();
    
        public void addSound(double offsetInSeconds, MergeSound sound) {
    
            if (sound.getAudioFormat() != audioFormat)
                new RuntimeException("Incompatible AudioFormat");
            if (sound.getSampleRate() != sampleRate)
                new RuntimeException("Incompatible SampleRate");
            if (sound.getSampleSize() != sampleSize)
                new RuntimeException("Incompatible SampleSize");
            if (sound.getChannels() != channels)
                new RuntimeException("Incompatible amount of Channels");
    
            int offset = secondsToByte(offsetInSeconds);
            offset = offset % 2 == 0 ? offset : offset + 1;// ensure we start at short when merging
    
            sounds.add(sound);
            offsets.add(secondsToByte(offsetInSeconds));
        }
    
        public void merge(double durationInSeconds) {
            audioBuffer = ByteBuffer.allocate(secondsToByte(durationInSeconds));
    
            for (int i = 0; i < sounds.size(); i++) {
    
                ByteBuffer buffer = sounds.get(i).getBuffer();
                int offset1 = offsets.get(i);
    
                // iterate over all sound data to append it
                while (buffer.hasRemaining()) {
    
                    int position = offset1 + buffer.position();// the global position in audioBuffer
    
                    // add the audio data to the vars
                    short sum = Short.reverseBytes(buffer.getShort());
                    int matches = 1;
    
                    // make sure later entries dont override the previsously merged
                    // continue only if theres empty audio data
                    if (audioBuffer.getShort(position) == 0) {
    
                        // iterate over the other sounds and check if the need to be merged
                        for (int j = i + 1; j < sounds.size(); j++) {// set j to i+1 to avoid all previous
                            ByteBuffer mergeBuffer = sounds.get(j).getBuffer();
                            int mergeOffset = offsets.get(j);
    
                            // check if this soundfile contains data that has to be merged
                            if (position >= mergeOffset && position < mergeOffset + mergeBuffer.capacity()) {
                                sum += Short.reverseBytes(mergeBuffer.getShort(position - mergeOffset));
                                matches++;
                            }
                        }
                        // make sure to cast to float 3/1=1 BUT round(3/1f)=2 for example
                        audioBuffer.putShort(position, Short.reverseBytes((short) round(sum / (float) matches)));
                    }
                }
                buffer.rewind();// So the sound can be added again
            }
        }
    
        private int secondsToByte(double seconds) {
            return (int) ceil(seconds * byteRate);
        }
    
        public void saveToFile(File file) throws IOException {
    
            byte[] audioData = audioBuffer.array();
    
            int audioSize = audioData.length;
            int fileSize = audioSize + 44;
    
            // 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(audioFormat));// 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
    
            out.write(audioData);// append the merged data
            out.close();// close the stream properly
        }
    
    }