Join Two Wav Files from Java

Join two WAV files from Java?

Here is the barebones code:

import java.io.File;
import java.io.IOException;
import java.io.SequenceInputStream;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

public class WavAppender {
public static void main(String[] args) {
String wavFile1 = "D:\\wav1.wav";
String wavFile2 = "D:\\wav2.wav";

try {
AudioInputStream clip1 = AudioSystem.getAudioInputStream(new File(wavFile1));
AudioInputStream clip2 = AudioSystem.getAudioInputStream(new File(wavFile2));

AudioInputStream appendedFiles =
new AudioInputStream(
new SequenceInputStream(clip1, clip2),
clip1.getFormat(),
clip1.getFrameLength() + clip2.getFrameLength());

AudioSystem.write(appendedFiles,
AudioFileFormat.Type.WAVE,
new File("D:\\wavAppended.wav"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

How do I combine/merge two wav files into one wav file?

If you work with the bytes of a wav file directly you can use the same strategy in any programming language. For this example I'll assume the two source files have the same bitrate/numchannels and are the same length/size.
(if not you can probably edit them before starting the merge).

First look over the wav specificaiton, I found a good one at a stanford course website:

Common header lengths are 44 or 46 bytes.

If you want to concatenate two files (ie play one wav then the other in a single file):

  1. find out what format your wav files are
  2. chop off the first 44/46 bytes which are the headers, the remainder of the file is the data
  3. create a new file and stick one of the headers in that.

    new wav file = {header} = {44/46} bytes long

  4. add the two data parts from the original files

    new wav file = {header + data1 + data2 } = {44/46 + size(data1) + size(data2)} bytes long

  5. modify your header in two places to reflect the new file's length.

    a. modify bytes 4+4 (ie. 4 bytes starting at offset 4).
    The new value should be a hexadecimal number representing the size of the new wav file in bytes {44/46 + size(data1) + size(data2)} - 8bytes.

    b. modify bytes 40+4 or 42+4 (the 4 bytes starting at offset 40 or 42, depending on if you have a 44byte header or 46 byte header).
    The new value should be a hexadecimal number representing the total size of the new wav file. ie {44/46 + size(data1) + size(data2)}

If you want to instead merge or mix the two files (so that they both play at the same time then):

  1. you won't have to edit the header if both files are the same length.
  2. starting at byte 44/46 you will have to edit each sample to be the value in data1 + the value in data2.
    so for example if your SampleRate was 8 bits you would modify 1 byte, if your sample rate was 16bits you would modify 2 bytes.
    the rest of the file is just Samples of 1/2bytes storing an int value representing the waveform of the sound at that time.

    a. For each of the remaining samples in the file grab the 1/2 byte hex string and get the int value from both files data1 and data2.

    b. add the 1/2 byte integers together
    convert the result back to hexadecimal and use that value in your output file.

    c. You normally have to divide that number by 2 to get an average value that fits back in the original 1/2byte sample block. I was getting distortion when i tried it in objc(probably related to signed or unsigned ints) and just skipped the division part since it will only likely be a problem if you are merging very loud sounds together.
    ie when data1 + data2 is larger than 1/2 bytes the sound will clip. There was a discussion about the clipping issue here and you may want to try one of those clipping techniques.

Concatenate two wav files with different AudioFormat

One of the two formats will have to first be converted to the other. Oracle has provided the following tutorial dedicated to this topic, with examples: Using Files and Format Converters.

Java: Mixing two WAV files without introducing noise

These files contain 16-bit samples - each sample is two bytes long. However, your first method tries to mix each byte independently. The result is like adding a number by adding each digit (without carry): 165 + 248 -> 103.

Your second method is using bitwise OR instead of addition. That's like adding two numbers by taking the greatest value of each digit: 165 + 248 -> 268.

Try using your second method, but replacing | with +.

Java merge Audio files with delay and overlapping

Thanks to the help of https://stackoverflow.com/users/740553/mike-pomax-kamermans I now have working code.

whistle.wav: https://voca.ro/1iqDr3yVZ6uG

out.wav: https://voca.ro/1jxlHkNUuH9r

The mayor problem was to create an empty wav file. To achieve this I needed to write a proper header at the beginning. You can read detailed information about the .wav header here: http://www.topherlee.com/software/pcm-tut-wavformat.html

When I implemented this I struggled with reading and writing bytes in Little/Big-Endian. Basically these specify the direction in which numbers are stored. Big Endian stores them like we and Java now (left to right) and Little Endian stores them reverse (right to left). A wav file expects all its numbers to be in Little Endian. So when we load numbers from a wav file we need to convert them to Big Endian (our Java numbers) and when we write the file we need to reconvert them to Little Endian. To do so we can use the Integer.reverseBytes() and Short.reverseBytes() methods.

OneHundredTwo:

Big Endian: 102

Little Endian: 201

Another struggle I had was when merging the audio byte arrays.
I added every bit of the arrays together and calculated the mean. However my SampleSize was 16 bit so I need to calculate the mean for every two bytes and not for every.

When getting this to work first there was always a strange noise right before my inserted audio played. I carelessly filled the byte arrays with the file contents. When merging my program also merged header data and interpreted them as sound data which created this noise. After chopping off the header my audio sounded fine.

However when my streams overlapped they produced a lot of foreground noise. When calculating the mean I didnt cast my divisor to a float so it chopped of some of the audio data. 3/2 became 1 and not 1.5 rounded to two

Something I actually did correct was ensuring that my audio can only be inserted with an offset that is dividable by two. Else it would merge the first byte of the previous amplitude with the last byte of the next amplitude.

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;

public class Main {

public static void main(String[] args) throws IOException, UnsupportedAudioFileException, LineUnavailableException {

AudioMerger merger = new AudioMerger();
MergeSound sound = new MergeSound(new File("whistle.wav"));

merger.addSound(2, sound);
merger.addSound(5, sound);
merger.addSound(5.5, sound);
merger.merge(10);
merger.saveToFile(new File("out.wav"));

}

}
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

public class MergeSound {

private short audioFormat;
private int sampleRate;
private short sampleSize;
private short channels;

private ByteBuffer buffer;

public MergeSound(File file) throws IOException {

DataInputStream in = new DataInputStream(new FileInputStream(file));
byte[] sound = new byte[in.available() - 44];

// read header data
in.skipNBytes(20);
audioFormat = Short.reverseBytes(in.readShort());
channels = Short.reverseBytes(in.readShort());
sampleRate = Integer.reverseBytes(in.readInt());
in.skipNBytes(6);
sampleSize = Short.reverseBytes(in.readShort());
in.skipNBytes(8);// make sure to cut the full header of else there will be strange noise

in.read(sound);
buffer = ByteBuffer.wrap(sound);
}

public ByteBuffer getBuffer() {
return buffer;
}

public short getAudioFormat() {
return audioFormat;
}

public void setAudioFormat(short audioFormat) {
this.audioFormat = audioFormat;
}

public int getSampleRate() {
return sampleRate;
}

public void setSampleRate(int sampleRate) {
this.sampleRate = sampleRate;
}

public short getSampleSize() {
return sampleSize;
}

public void setSampleSize(short sampleSize) {
this.sampleSize = sampleSize;
}

public short getChannels() {
return channels;
}

public void setChannels(short channels) {
this.channels = channels;
}

}

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

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

// exit if audio plays after end
if (position >= audioBuffer.capacity())
return;

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

}

Java - Trouble combining more than 2 .wav files

It might be because the input streams cannot be read more than once. Once you read an input stream, it will be at its end and attempt to read further will read no more bytes from that stream.

This should work with a slight modification, keep creating new audio input streams in your loop:

File sample1 = new File("f1.wav");
File sample2 = new File("f2.wav");

File fileOut = new File("combined.wav");

AudioInputStream audio1 = AudioSystem.getAudioInputStream(sample1);
AudioInputStream audio2 = AudioSystem.getAudioInputStream(sample2);

AudioInputStream audioBuild = new AudioInputStream(new SequenceInputStream(audio1, audio2), audio1.getFormat(), audio1.getFrameLength() + audio2.getFrameLength());

for(int i = 0; i < 5; i++)
{
audioBuild = new AudioInputStream(new SequenceInputStream(audioBuild, /* keep creating new input streams */ AudioSystem.getAudioInputStream(sample2)), audioBuild.getFormat(), audioBuild.getFrameLength() + audio2.getFrameLength());
}

AudioSystem.write(audioBuild, AudioFileFormat.Type.WAVE, fileOut);

Also, ensure your audio formats for the files are exactly the same. That is, same sample rate, same channel count, same bits per sample. Otherwise you'll need additional code to do sample conversion.



Related Topics



Leave a reply



Submit