WAVE 文件意外行为

WAVE file unexpected behaviour

我目前正在尝试制作一个可以用莫尔斯播放 sos 的 .wav 文件。

我的方法是:我有一个字节数组,其中包含一波哔哔声。然后我重复,直到我有想要的长度。 之后,我将这些字节插入到一个新数组中,并放入包含 00(十六进制)的字节来分隔蜂鸣声。

如果我向 WAVE 文件添加 1 声蜂鸣声,它会正确创建文件(即我得到所需长度的蜂鸣声)。 这是放大后的海浪图片(我在 Audacity 中打开文件): 这是整个波浪部分的图片:

现在的问题是,当我添加第二个蜂鸣声时,第二个蜂鸣声变得完全失真: 所以这就是整个文件现在的样子:

如果我再添加一个嘟嘟声,它将再次成为正确的嘟嘟声,如果我再添加一个嘟嘟声,它将再次被扭曲,等等。 所以基本上,每隔一个波都是扭曲的。

有人知道为什么会这样吗?

这是我生成的 .txt 文件的 link,其中包含我创建的 wave 文件的音频数据:byteTest19.txt

这是我使用文件 format.info 生成的 .txt 文件的 lint,它是我生成的 .wav 文件中字节的十六进制表示形式,包含 5 声蜂鸣声(其中两声,甚至哔哔声都被扭曲了):test3.txt

您可以判断新的蜂鸣声何时开始,因为它之前有很多 00。

据我所知,第二声的字节与第一声没有区别,这就是我问这个问题的原因。

如果有人知道为什么会这样,请帮助我。如果您需要更多信息,请随时询问。我希望我解释清楚了我在做什么,如果没有,那是我的错。

编辑 这是我的代码:

    // First I calculate the byte array for a single beep
   
    // This file is just a single wave of the audio (up and down)
    //  (see below for the fileToAudioByteArray method) (In  my 
    // actual code I only take in half of the wave and then I 
    // invert it, but I didn't want to make this too complicated, 
    // I'll put the full code below
    final byte[] wave = fileToAudioByteArray(new File("path to my wav file");
    // This is how long that audio fragment is in seconds
    final double secondsPerWave = 0.0022195; 
    
    // This is the amount of seconds a beep takes up (e.g. the seconds picture)
    double secondsPerBeep = 0.25; 
    
    final int amountWaveInBeep = (int) Math.ceil((secondsPerBeep/secondsPerWave));
    // this is the byte array containing the audio data of 
    // 1 beep (see below for the repeatArray method)
    final byte[] beep = repeatArray(wave, amountWaveInBeep);
    
    // Now for the silence between the beeps
    final byte silenceByte = 0x00,
    // The amount of seconds a silence byte takes up
    final double secondsPerSilenceByte = 0.00002;
    // The amount of silence bytes I need to make one second
    final int amountOfSilenceBytesForOneSecond = (int) (Math.ceil((1/secondsPerSilenceByte))); 

    // The space between 2 beeps will be 0.25 * secondsPerBeep
    double amountOfBeepsEquivalent = 0.25;
    // This is the amount of bytes of silence I need 
    // between my beeps
    final int amntSilenceBytesPerSpaceBetween = (int) Math.ceil(secondsPerBeep * amountOfBeepsEquivalent * amountOfSilenceBytesForOneSecond);
    final byte[] spaceBetweenBeeps = new byte[amntSilenceBytesPerSpaceBetween];
    for (int i = 0; i < amntSilenceBytesPerSpaceBetween; i++) {
        spaceBetweenBeeps[i] = silenceByte;
    }

    WaveFileBuilder wavBuilder = new WaveFileBuilder(WaveFileBuilder.AUDIOFORMAT_PCM, 1, 44100, 16);


    // Adding all the beeps and silence to the WAVE file (test3.wav)
    wavBuilder.addBytes(beep);
    wavBuilder.addBytes(spaceBetweenDigits);
    wavBuilder.addBytes(beep);
    wavBuilder.addBytes(spaceBetweenDigits);
    wavBuilder.addBytes(beep);
    wavBuilder.addBytes(spaceBetweenDigits);
    wavBuilder.addBytes(beep);
    wavBuilder.addBytes(spaceBetweenDigits);
    wavBuilder.addBytes(beep);
    wavBuilder.addBytes(nextChar);

    File outputFile = new File("path/test3.wav");
    wavBuilder.saveFile(outputFile);

这是我一开始使用的两种方法:

    /**
     * Converts a wav file to a byte array containing its audio data
     * @param file the wav file you want to convert
     * @return the data part of a wav file in byte form
     */
    public static byte[] fileToAudioByteArrray(File file) throws UnsupportedAudioFileException, IOException {
        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);
        AudioFormat audioFormat = audioInputStream.getFormat();

        int bytesPerSample = audioFormat.getFrameSize();
        if (bytesPerSample == AudioSystem.NOT_SPECIFIED) {
            bytesPerSample = -1;
        }

        long numSamples = audioInputStream.getFrameLength();

        int numBytes = (int) (numSamples * bytesPerSample);

        byte[] audioBytes = new byte[numBytes];

        int numBytesRead;
        while((numBytesRead = audioInputStream.read(audioBytes)) != -1);

        return audioBytes;
    }
    /**
     * Repeats an array into a new array x times
     * @param array the array you want to copy x times
     * @param repeat the amount of times you want to copy the array into the new array
     * @return an array containing the content of {@code array} {@code repeat} times.
     */
    public static byte[] repeatArray(byte[] array, int repeat) {
        byte[] result = new byte[array.length * repeat];
        for (int i = 0; i < result.length; i++) {
            result[i] = array[i % array.length];
        }
        return result;
    }

现在我的 WaveFileBuilder class:

    /**
     * <p> Constructs a WavFileBuilder which can be used to create wav files.</p>
     *
     * <p>The builder takes care of the subchunks based on the parameters that are given in the constructor.</p>
     *
     * <h3>Adding audio to the wav file</h3>
     * There are 2 methods that can be used to add audio data to the WavFile.
     * One is {@link #addBytes(byte[]) addBytes} which lets you directly inject bytes
     * into the data section of the wav file.
     * The other is {@link #addAudioFile(File) addAudioFile} which lets you add the audio
     * data of another wav file to the wav file's audio data.
     *
     * @param audioFormat The be.jonaseveraert.util.audio format of the wav file {@link #AUDIOFORMAT_PCM PCM} = 1
     * @param numChannels The number of channels the wav file will have {@link #NUM_CHANNELS_MONO MONO} = 1,
     *                    {@link #NUM_CHANNELS_STEREO STEREO} = 2
     * @param sampleRate The sample rate of the wav file in Hz (e.g. 22050, 44100, ...)
     * @param bitsPerSample The amount of bits per sample. If 16 bits, the audio sample will contain 2 bytes per
     *                      channel. (e.g. 8, 16, ...). This is important to take into account when using the
     *                      {@link #addBytes(byte[]) addBytes} method to insert data into the wav file.
     */
    public WaveFileBuilder(int audioFormat, int numChannels, int sampleRate, int bitsPerSample) {
        this.audioFormat = audioFormat;
        this.numChannels = numChannels;
        this.sampleRate = sampleRate;
        this.bitsPerSample = bitsPerSample;

        // Subchunk 1 calculations
        this.byteRate = this.sampleRate * this.numChannels * (this.bitsPerSample / 8);
        this.blockAlign = this.numChannels * (this.bitsPerSample / 8);
    }


    /**
     * Contains the audio data for the wav file that is being constructed
     */
    byte[] audioBytes = null;

    // For debug purposes
    int counter = 0;

/**
     * Adds audio data to the wav file from bytes
     * <p>See the "see also" for the structure of the "Data" part of a wav file</p>
     * @param audioBytes audio data
     * @see <a href="https://web.archive.org/web/20081210162727/https://ccrma.stanford.edu/CCRMA/Courses/422/projects/WaveFormat/">Wave PCM Soundfile Format</a>
     */
    public void addBytes(byte[] audioBytes) throws IOException {
        // This is all debug code that I used to maker byteText19.txt
        // which I have linked in my question
        String test1;
        try {
            test1 = (temp.bytesToHex(this.audioBytes, true));
        } catch (NullPointerException e) {
            test1 = "null";
        }
        File file = new File("/Users/jonaseveraert/Desktop/Morse Sound Test/debug/byteTest" + counter + ".txt");
        file.createNewFile();
        counter++;
        BufferedWriter writer = new BufferedWriter(new FileWriter(file));
        writer.write(test1);
        writer.close();

        // This is where the actual code starts //
        if (this.audioBytes != null)
            this.audioBytes = ArrayUtils.addAll(this.audioBytes, audioBytes);
        else
            this.audioBytes = audioBytes;
        // End of code //
        
        // This is for debug again
        String test2 = (temp.bytesToHex(this.audioBytes, true));
        File file2 = new File("/Users/jonaseveraert/Desktop/Morse Sound Test/debug/byteTest" + counter + ".txt");
        file2.createNewFile();
        counter++;
        BufferedWriter writer2 = new BufferedWriter(new FileWriter(file2));
        writer2.write(test2);
        writer2.close();
    }

    /**
     * Saves the file to the location of the {@code outputFile}.
     * @param outputFile The file that will be outputted (not created yet), contains the path
     * @return true if the file was created and written to successfully. Else false.
     * @throws IOException If an I/O error occurred
     */
    public boolean saveFile(File outputFile) throws IOException {
        // subchunk2 calculations

        //int numBytesInData = data.length()/2;
        int numBytesInData = audioBytes.length;
        int numSamples = numBytesInData / (2 * numChannels);

        subchunk2Size = numSamples * numChannels * (bitsPerSample / 8);

        // chunk calculation
        chunkSize = 4 + (8 + subchunk1Size) + (8 + subchunk2Size);

        // convert everything to hex string //
        // Chunk descriptor
        String f_chunkID = asciiStringToHexString(chunkID);
        String f_chunkSize = intToLittleEndianHexString(chunkSize, 4);
        String f_format = asciiStringToHexString(format);

        // fmt subchunck
        String f_subchunk1ID = asciiStringToHexString(subchunk1ID);
        String f_subchunk1Size = intToLittleEndianHexString(subchunk1Size, 4);
        String f_audioformat = intToLittleEndianHexString(audioFormat, 2);
        String f_numChannels = intToLittleEndianHexString(numChannels, 2);
        String f_sampleRate = intToLittleEndianHexString(sampleRate, 4);
        String f_byteRate = intToLittleEndianHexString(byteRate, 4);
        String f_blockAlign = intToLittleEndianHexString(blockAlign, 2);
        String f_bitsPerSample = intToLittleEndianHexString(bitsPerSample, 2);

        // data subchunk
        String f_subchunk2ID = asciiStringToHexString(subchunk2ID);
        String f_subchunk2Size = intToLittleEndianHexString(subchunk2Size, 4);
        // data is stored in audioData

        // Combine all hex data into one String (except for the 
        // audio data, which is passed in as a byte array)
        final String AUDIO_BYTE_STREAM_STRING = f_chunkID  + f_chunkSize + f_format
                + f_subchunk1ID + f_subchunk1Size + f_audioformat + f_numChannels + f_sampleRate + f_byteRate  + f_blockAlign + f_bitsPerSample
                + f_subchunk2ID + f_subchunk2Size;

        // Convert the hex data to a byte array
        final byte[] BYTES = hexStringToByteArray(AUDIO_BYTE_STREAM_STRING);

        // Create & write file
        if (outputFile.createNewFile()) {
            // Combine byte arrays
            // This array now contains the full WAVE file
            byte[] audioFileBytes = ArrayUtils.addAll(BYTES, audioBytes);

            try (FileOutputStream fos = new FileOutputStream(outputFile)) {
                fos.write(audioFileBytes); // Write the bytes into a file
            } 
            catch (IOException e) {
                logger.log(Level.SEVERE, "IOException occurred");
                logger.log(Level.SEVERE, null, e);

                return false;
            }
            

            logger.log(Level.INFO, "File created: " + outputFile.getName());
            }
            return true;
        } else {
            //System.out.println("File already exists.");
            logger.log(Level.WARNING, "File already exists.");
            }
            return false;
        }
    }

    // Aiding methods
    /**
     * Converts a string containing hexadecimal to bytes
     * @param s e.g. 00014F
     * @return an array of bytes e.g. {00, 01, 4F}
     */
    private byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] bytes = new byte[len / 2];
        for (int i = 0; i < len; i+= 2) {
            bytes[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
        }
        return bytes;
    }

    /**
     * Converts an int to a hexadecimal string in the little-endian format
     * @param input an integer number
     * @param numberOfBytes The number of bytes the the integer is stored in
     * @return The integer as a hexadecimal string in the little-endian byte ordering
     */
    private String intToLittleEndianHexString(int input, int numberOfBytes) {
        String hexBigEndian = Integer.toHexString(input);
        StringBuilder hexLittleEndian = new StringBuilder();
        int amountOfNumberProcessed = 0;
        for (int i = 0; i < hexBigEndian.length()/2f; i++) {
            int endIndex = hexBigEndian.length() - (i * 2);
            try {
                hexLittleEndian.append(hexBigEndian.substring(endIndex-2, endIndex));
            } catch (StringIndexOutOfBoundsException e ) {
                hexLittleEndian.append(0).append(hexBigEndian.charAt(0));
            }
            amountOfNumberProcessed++;
        }
        while (amountOfNumberProcessed != numberOfBytes) {
            hexLittleEndian.append("00");
            amountOfNumberProcessed++;
        }
        return hexLittleEndian.toString();
    }

    /**
     * Converts a string containing ascii to its hexadecimal notation
     * @param input The string that has to be converted
     * @return The string as a hexadecimal notation in the big-endian byte ordering
     */
    private String asciiStringToHexString(String input) {
        byte[] bytes = input.getBytes(StandardCharsets.US_ASCII);
        StringBuilder hex = new StringBuilder();
        for (byte b : bytes) {
            String hexChar = String.format("%02X", b);
            hex.append(hexChar);
        }
        return hex.toString().trim();
    }

最后:如果你想要完整的代码,替换 final byte[] wave = fileToAudioByteArray(new File("path to my wav file"); 在我的代码开头:

    File morse_half_wave_file = new File("/Users/jonaseveraert/Desktop/Morse Sound Test/morse_audio_fragment.wav");
    final byte[] half_wave = temp.fileToAudioByteArrray(morse_half_wave_file);
    final byte[] half_wave_inverse = temp.invertByteArray(half_wave);

    // Then the wave byte array becomes:
    final byte[] wave = ArrayUtils.addAll(half_wave, half_wave_inverse); // This ArrayUtils.addAll comes from the Apache Commons lang3 library

    // And this is the invertByteArray method
    /**
     * Inverts bytes e.g. 000101 becomes 111010
     */
    public static byte[] invertByteArray(byte[] bytes) {
        if (bytes == null) {
            return null;
            // TODO: throw empty byte array expcetion
        }

        byte[] outputArray = new byte[bytes.length];
        for(int i = 0; i < bytes.length; i++) {
            outputArray[i] = (byte) ~bytes[i];
        }
        return outputArray;
    }

P.S. 这里是 morse_audio_fragment.wav: morse_audio_fragment.wav

提前致谢, 乔纳斯

问题

您的 .wav 文件是 Signed 16 bit Little Endian, Rate 44100 Hz, Mono - 这意味着文件中的每个样本都是 2 个字节长,并描述了一个带符号的振幅。因此,您可以毫无问题地复制和粘贴样本块,只要它们的长度可以被 2 整除(您的 块大小 )。你的沉默可能是奇数长度,所以沉默后的第一个样本被解释为

0x00 0x65 // last byte of silence, 1st byte of actual beep: weird

并且所有后续的字节对都被解释错误(从每个样本中获取第二个字节,从下一个样本中获取第一个字节)由于这种初始未对齐,直到你找到下一个奇数长度的沉默,突然一切都重新正确对齐;而不是预期的

0x65 0x05 // 1st and 2nd byte of beep: actual expected sample

如何修复

不允许调用 addBytes 来添加不均匀划分块大小的字节数。

public class WaveFileBuilder() {
   byte[] audioBytes = null;

   // ... other attributes, methods, constructor 

   public void addBytes(byte[] audioBytes) throws IOException {
        // ... debug code above, handle empty
           
        // THIS SHOULD CHECK audioBytes IS MULTIPLE OF blockSize
        this.audioBytes = ArrayUtils.addAll(this.audioBytes, audioBytes);
        // ... debug code below
   }

   public boolean saveFile(File outputFile) throws IOException {
        // ... prepare headers
        
        // concatenate header (BYTES) and contents
        byte[] audioFileBytes = ArrayUtils.addAll(BYTES, audioBytes);

        // ... write out bytes
        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            fos.write(audioFileBytes);
        } 
        // ...
   }
}    

首先,您可以使用不同的属性和参数名称来避免一些混淆。然后,你不断地一遍又一遍地增长一个数组;这很浪费,使代码可以在 O(n) 运行 中 O(n^2),因为你这样调用它:

wavBuilder.addBytes(beep);
wavBuilder.addBytes(spaceBetweenDigits);
wavBuilder.addBytes(beep);
wavBuilder.addBytes(spaceBetweenDigits);
wavBuilder.addBytes(beep);
wavBuilder.addBytes(spaceBetweenDigits);
wavBuilder.addBytes(beep);
wavBuilder.addBytes(spaceBetweenDigits);
wavBuilder.addBytes(beep);
wavBuilder.addBytes(nextChar);

相反,我提出以下建议:

public class WaveFileBuilder() {
   List<byte[]> chunks = new ArrayList<>();

   // ... other attributes, methods, constructor 

   public void addBytes(byte[] audioBytes) throws IOException {
        if ((audioBytes.length % blockAlign) != 0) {
           throw new IllegalArgumentException("Trying to add a chunk that does not fit evenly; this would cause un-aligned blocks")
        }
        chunks.add(audioBytes);
   }

   public boolean saveFile(File outputFile) throws IOException {
        // ... prepare headers

        // ... write out bytes
        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            for (byte[] chunk : chunks) fos.write(chunk);
        } 
   }
}  

这个版本根本不使用连接,而且应该更快更容易测试。它还需要更少的内存,因为它不会复制所有这些数组来将它们连接起来。