Reading and Writing Samples from Audio Files in Java
Managing audio files in Java is done using the "JavaSound" API, javax.sound.sampled, which is part of the Java distribution since version 1.3.
This API is extremely rich, but provides few services at the sample level. This article proposes a class which fullfill this need and allows to read simply and directly the samples of the various channels of an audio file, for example in order to perform a Fourier analysis. Sample values are sound intensities between -1 and 1, identical to the ones given by MATLAB fonctions wavread or auread.
Supported files
The supported sound files of the JavaSound API are:- WAV files (Microsoft/IBM)
- AU files (Unix/Linux)
- AIFF/AIFF-C files (Apple)
The compressed files MP3 and WMA are NOT usable with this base API/ However, the open architecture of JavaSound allows contibutors to provide plugins for other file formats. For MP3 open-source plugins, see related article Extend JavaSound to Play MP3, Ogg Vorbis, and More To my knowledge, there is no plugin for Microsoft WMA files, probably for legal reasons.
Reading samples from a file
An ordinary audio file has 2 channels, channel 0 being left channel and channel 1 being right channel, but there could be any number of channels. The samples of all channels are interleaved, a frame corresponding to the group of samples of all channels at a given time.
Samples are sound intensity values and are given as double values between -1 and +1; these values are encoded in the audio file, using one of the following encodings: PCM-UNSIGNED, PCM-SIGNED, A-LAW and U-LAW.
The following class allows to read and decode the samples of an audio file. The getSampleCount method returns the total number of samples per channel. The getInterleavedSamples method copies all, or a portion of all interleaved samples in an array of double. The getChannelSamples can extract samples of a particular channel from an array of interleaved sample values.
import java.io.*;
import javax.sound.sampled.*;
public class AudioSampleReader {
private AudioInputStream audioInputStream;
private AudioFormat format;
public AudioSampleReader(File file)
throws UnsupportedAudioFileException, IOException {
audioInputStream = AudioSystem.getAudioInputStream(file);
format = audioInputStream.getFormat();
}
// Return audio format, and through it, most properties of
// the audio file: sample size, sample rate, etc.
public AudioFormat getFormat() {
return format;
}
// Return the number of samples of all channels
public long getSampleCount() {
long total = (audioInputStream.getFrameLength() *
format.getFrameSize() * 8) / format.getSampleSizeInBits();
return total / format.getChannels();
}
// Get the intervealed decoded samples for all channels, from sample
// index begin (included) to sample index end (excluded) and copy
// them into samples. end must not exceed getSampleCount(), and the
// number of samples must not be so large that the associated byte
// array cannot be allocated
public void getInterleavedSamples(long begin, long end,
double[] samples) throws IOException,
IllegalArgumentException {
long nbSamples = end - begin;
// nbBytes = nbSamples * sampleSizeinByte * nbChannels
long nbBytes = nbSamples * (format.getSampleSizeInBits() / 8) *
format.getChannels();
if (nbBytes > Integer.MAX_VALUE)
throw new IllegalArgumentException("too many samples");
// allocate a byte buffer
byte[] inBuffer = new byte[(int)nbBytes];
// read bytes from audio file
audioInputStream.read(inBuffer, 0, inBuffer.length);
// decode bytes into samples. Supported encodings are:
// PCM-SIGNED, PCM-UNSIGNED, A-LAW, U-LAW
decodeBytes(inBuffer, samples);
}
// Extract samples of a particular channel from interleavedSamples and
// copy them into channelSamples
public void getChannelSamples(int channel,
double[] interleavedSamples, double[] channelSamples) {
int nbChannels = format.getChannels();
for (int i = 0; i < channelSamples.length; i++) {
channelSamples[i] = interleavedSamples[nbChannels*i + channel];
}
}
// Convenience method. Extract left and right channels for common stereo
// files. leftSamples and rightSamples must be of size getSampleCount()
public void getStereoSamples(double[] leftSamples, double[] rightSamples)
throws IOException {
long sampleCount = getSampleCount();
double[] interleavedSamples = new double[(int)sampleCount*2];
getInterleavedSamples(0, sampleCount, interleavedSamples);
for (int i = 0; i < leftSamples.length; i++) {
leftSamples[i] = interleavedSamples[2*i];
rightSamples[i] = interleavedSamples[2*i+1];
}
}
// Private. Decode bytes of audioBytes into audioSamples
private void decodeBytes(byte[] audioBytes, double[] audioSamples) {
int sampleSizeInBytes = format.getSampleSizeInBits() / 8;
int[] sampleBytes = new int[sampleSizeInBytes];
int k = 0; // index in audioBytes
for (int i = 0; i < audioSamples.length; i++) {
// collect sample byte in big-endian order
if (format.isBigEndian()) {
// bytes start with MSB
for (int j = 0; j < sampleSizeInBytes; j++) {
sampleBytes[j] = audioBytes[k++];
}
} else {
// bytes start with LSB
for (int j = sampleSizeInBytes - 1; j >= 0; j--) {
sampleBytes[j] = audioBytes[k++];
if (sampleBytes[j] != 0)
j = j + 0;
}
}
// get integer value from bytes
int ival = 0;
for (int j = 0; j < sampleSizeInBytes; j++) {
ival += sampleBytes[j];
if (j < sampleSizeInBytes - 1) ival <<= 8;
}
// decode value
double ratio = Math.pow(2., format.getSampleSizeInBits() - 1);
double val = ((double) ival) / ratio;
audioSamples[i] = val;
}
}
}
Writing samples into a file
import java.io.*;
import javax.sound.sampled.*;
public class AudioSampleWriter implements Runnable {
File file;
AudioFormat format;
AudioFileFormat.Type targetType;
PipedOutputStream pos;
PipedInputStream pis;
AudioInputStream ais;
byte[] bytes;
public AudioSampleWriter(File file, AudioFormat format,
AudioFileFormat.Type targetType) throws IOException {
this.format = format;
this.targetType = targetType;
this.file = file;
// Write to the output stream
pos = new PipedOutputStream();
// It will then go to the file via the input streams
pis = new PipedInputStream(pos);
ais = new AudioInputStream(pis, format, AudioSystem.NOT_SPECIFIED);
new Thread(this).start();
}
public void run() {
try {
AudioSystem.write(ais, targetType, file);
} catch(Exception e) {
e.printStackTrace();
}
}
public void write(double[] interleavedSamples) throws IOException {
writeInterleavedSamples(interleavedSamples, interleavedSamples.length);
}
public void writeInterleavedSamples(double[] interleavedSamples,
int sampleCount) throws IOException {
// Allocate a new bytes array if necessary. If bytes is too long,
// don't worry about it, just use as much as is needed.
int numBytes = sampleCount * (format.getSampleSizeInBits() / 8);
System.out.println("numBytesOut=" + numBytes);
if (bytes == null || numBytes > bytes.length)
bytes = new byte[numBytes];
// Convert doubles to bytes using format
encodeSamples(interleavedSamples, bytes, sampleCount);
// write it
pos.write(bytes, 0, numBytes);
}
public void close() throws IOException {
if (pos != null) {
ais.close();
pis.close();
pos.close();
}
}
private void encodeSamples(double[] audioData, byte[] audioBytes,
int length) {
int in;
if (format.getSampleSizeInBits() == 16) {
if (format.isBigEndian()) {
for (int i = 0; i < length; i++) {
in = (int)(audioData[i]*32767);
/* First byte is MSB (high order) */
audioBytes[2*i] = (byte)(in >> 8);
/* Second byte is LSB (low order) */
audioBytes[2*i+1] = (byte)(in & 255);
}
} else {
for (int i = 0; i < length; i++) {
in = (int)(audioData[i]*32767);
/* First byte is LSB (low order) */
audioBytes[2*i] = (byte)(in & 255);
/* Second byte is MSB (high order) */
audioBytes[2*i+1] = (byte)(in >> 8);
}
}
} else if (format.getSampleSizeInBits() == 8) {
if (format.getEncoding().toString().startsWith("PCM_SIGN")) {
for (int i = 0; i < length; i++) {
audioBytes[i] = (byte)(audioData[i]*127);
}
} else {
for (int i = 0; i < length; i++) {
audioBytes[i] = (byte)(audioData[i]*127 + 127);
}
}
}
}
}
The following test program gets all samples from a PCM-SIGNED encoded audio file (available here), separates the 2 stereo channels, lowers the sound and then writes it back in a different audio format.
public static void main(String[] args) {
AudioSampleReader sampleReader = null;
try {
sampleReader = new AudioSampleReader(new File(args[0]));
long nbSamples = sampleReader.getSampleCount();
System.out.println("nbChannel=" + format.getChannels());
System.out.println("frameRate=" + format.getFrameRate());
System.out.println("sampleSize=" + format.getSampleSizeInBits());
double[] samples = new double[(int)nbSamples];
sampleReader.getInterleavedSamples(0, nbSamples, samples);
// lowers sound level
for (int i = 0; i < isamples.length; i++) {
isamples[i] /= 4.;
}
File outFile = new File("out.wav");
AudioWriter audioWriter = new AudioWriter(outFile,
sampleReader.getFormat(), AudioFileFormat.Type.WAVE);
audioWriter.write(samples, samples.length);
audioWriter.close();
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}