简体   繁体   中英

WAVE file unexpected behaviour

I am currently trying to make a.wav file that will play sos in morse.

The way I went about this is: I have a byte array that contains one wave of a beep. I then repeated that until I had the desired length. After that I inserted those bytes into a new array and put bytes containing 00 (in hexadecimal) to separate the beeps.

If I add 1 beep to a WAVE file, it creates the file correctly (ie I get a beep of the desired length). Here is a picture of the waves zoomed in (I opened the file in Audacity): 放大的海浪 And here is a picture of the entire wave part: 全波

The problem now is that when I add a second beep, the second one becomes completely distorted: 扭曲的波浪放大 So this is what the entire file looks like now: 整个音频文件

If I add another beep, it will be the correct beep again, If I add yet another beep it's going to be distorted again, etc. So basically, every other wave is distorted.

Does anyone know why this happens?

Here is a link to a.txt file I generated containing the the audio data of the wave file I created: byteTest19.txt

And here is a lint to a.txt file that I generated using file format.info that is a hexadecimal representation of the bytes in the.wav file I generated containing 5 beeps (with two of them, the even beeps being distorted): test3.txt

You can tell when a new beep starts because it is preceded by a lot of 00's.

As far as I can see, the bytes of the second beep does not differ from the first one, which is why I am asking this question.

If anyone knows why this happens, please help me. If you need more information, don't hesitate to ask. I hope I explained well what I'm doing, if not, that's my bad.

EDIT Here is my code:

    // 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);

These are the 2 methods I used in the beginning:

    /**
     * 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;
    }

Now for my 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();
    }

And lastly: if you want the full code, replace final byte[] wave = fileToAudioByteArray(new File("path to my wav file"); in the beginning of my code with:

    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;
    }

PS Here is the morse_audio_fragment.wav: morse_audio_fragment.wav

Thanks in advance, Jonas

The problem

Your.wav file is Signed 16 bit Little Endian, Rate 44100 Hz, Mono - which means that each sample in the file is 2 bytes long, and describes a signed amplitude. So you can copy-and-paste chunks of samples without any problems, as long as their lengths are divisible by 2 (your block size ). Your silences are likely of odd length, so that the 1st sample after a silence is interpreted as

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

and all subsequent pairs bytes are interpreted wrong (taking the 2nd byte from each sample with the 1st byte from the next sample) due to this initial mis-alignment, until you find the next odd-length silence, when suddenly everything gets re-aligned correctly again; instead of the expected

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

How to fix it

Do not allow calls to addBytes that would add a number of bytes that does not evenly divide the block-size.

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);
        } 
        // ...
   }
}    

First, you could have avoided some confusion using a different name for the attribute and the parameter. Then, you are constantly growing an array over and over; this is wasteful, making code that could run in O(n) run in O(n^2) , because you are calling it like this:

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);

Instead, I propose the following:

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);
        } 
   }
}  

This version uses no concatenation at all, and should be much faster and easier to test. It also requires less memory, because it is not copying all those arrays around to concatenate them to each other.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM