Created
August 9, 2011 16:24
-
-
Save rjeschke/1134491 to your computer and use it in GitHub Desktop.
Redesign of my simple RIFF WAVE writer class.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package allerlei; | |
import java.io.BufferedOutputStream; | |
import java.io.FileInputStream; | |
import java.io.FileOutputStream; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
/** | |
* <p>Simple RIFF WAVE writer class for quick audio dumps.</p> | |
* | |
* @author René Jeschke (rene_jeschke@yahoo.de) | |
*/ | |
public final class WavWriter | |
{ | |
/** The sample rate. */ | |
private final int sampleRate; | |
/** Bits per sample. */ | |
private final int bitsPerSample; | |
/** Number of channels. */ | |
private final int channels; | |
/** Temporary file for wave data. */ | |
private File tempFile = null; | |
/** Output stream for writing temporary wave data. */ | |
private OutputStream tempStream = null; | |
/** RIFF. */ | |
private final static byte[] RIFF = {'R', 'I', 'F', 'F'}; | |
/** WAVE. */ | |
private final static byte[] WAVE = {'W', 'A', 'V', 'E'}; | |
/** fmt . */ | |
private final static byte[] FMT_ = {'f', 'm', 't', ' '}; | |
/** data. */ | |
private final static byte[] DATA = {'d', 'a', 't', 'a'}; | |
/** | |
* Constructor. | |
* | |
* @param sampleRate A valid RIFF WAVE sample rate. | |
* @param bitsPerSample May be 8, 16 or 24. | |
* @param channels At least one channel. | |
*/ | |
public WavWriter(final int sampleRate, final int bitsPerSample, final int channels) | |
{ | |
this.sampleRate = sampleRate; | |
this.bitsPerSample = bitsPerSample; | |
this.channels = channels; | |
} | |
/** | |
* Opens temporary file stream. | |
* | |
* @throws IOException if an IO error occurred. | |
*/ | |
private void openTemp() throws IOException | |
{ | |
this.tempFile = File.createTempFile("wavwrite", "bin"); | |
this.tempFile.deleteOnExit(); | |
this.tempStream = new BufferedOutputStream(new FileOutputStream(this.tempFile)); | |
} | |
/** | |
* Saves this RIFF WAVE to a file. | |
* | |
* @param filename The filename. | |
* @throws IOException if an IO error occurred. | |
*/ | |
public void save(final String filename) throws IOException | |
{ | |
this.save(new File(filename)); | |
} | |
/** | |
* Saves this RIFF WAVE to a file. | |
* | |
* @param file The file to save to. | |
* @throws IOException if an IO error occurred. | |
*/ | |
public void save(final File file) throws IOException | |
{ | |
OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); | |
try | |
{ | |
this.save(out); | |
} | |
finally | |
{ | |
out.close(); | |
} | |
} | |
/** | |
* Saves this RIFF WAVE to an output stream. | |
* | |
* @param out the output stream to write to. | |
* @throws IOException if an IO error occurred. | |
*/ | |
public void save(final OutputStream out) throws IOException | |
{ | |
try | |
{ | |
if(this.tempFile == null) | |
throw new IOException("No data supplied."); | |
if(this.tempStream != null) | |
{ | |
// close also flushes ... | |
this.tempStream.close(); | |
this.tempStream = null; | |
} | |
if(this.channels < 1) | |
throw new IOException("Illegal channel count: " + this.channels); | |
if(this.sampleRate < 1) | |
throw new IOException("Illegal sample rate: " + this.sampleRate); | |
// dLen ... somehow reminds me of Babylon 5 ... hmmm | |
final int dLen = (int)this.tempFile.length(); | |
if(dLen <= 0 || (dLen + 36) <= 0) | |
throw new IOException("Too much wave data. RIFF WAVE is only 32 bits."); | |
final int samples = dLen / (this.bitsPerSample >> 3); | |
final int frames = samples / this.channels; | |
final int blockAlign = this.channels * (this.bitsPerSample >> 3); | |
if(dLen != frames * this.channels * (this.bitsPerSample >> 3)) | |
throw new IOException("Unfinished frame."); | |
out.write(RIFF); | |
write32(out, dLen + 36); | |
out.write(WAVE); | |
out.write(FMT_); | |
write32(out, 16); | |
write16(out, 1); // PCM | |
write16(out, this.channels); | |
write32(out, this.sampleRate); | |
write32(out, this.sampleRate * blockAlign); | |
write16(out, blockAlign); | |
write16(out, this.bitsPerSample); | |
out.write(DATA); | |
write32(out, dLen); | |
final FileInputStream in = new FileInputStream(this.tempFile); | |
try | |
{ | |
final byte[] b = new byte[8192]; | |
int todo = dLen; | |
while(todo > 0) | |
{ | |
final int read = in.read(b); | |
out.write(b, 0, read); | |
todo -= read; | |
} | |
} | |
finally | |
{ | |
in.close(); | |
} | |
} | |
finally | |
{ | |
this.dispose(); | |
} | |
} | |
/** | |
* Removes all temporary files and streams. | |
*/ | |
public void dispose() | |
{ | |
if(this.tempStream != null) | |
{ | |
try | |
{ | |
this.tempStream.close(); | |
} | |
catch (IOException e) | |
{ | |
// gnah ... dispose should not throw nothing ... so: | |
// let's eat it ... eat it ... eat it ... *singz* | |
} | |
finally | |
{ | |
this.tempStream = null; | |
} | |
} | |
if(this.tempFile != null) | |
{ | |
this.tempFile.delete(); | |
this.tempFile = null; | |
} | |
} | |
/** | |
* Writes a 16 bit, little endian value to the temporary output stream. | |
* | |
* @param value The value to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
private void write16(int value) throws IOException | |
{ | |
write16(this.tempStream, value); | |
} | |
/** | |
* Writes a 24 bit, little endian value to the temporary output stream. | |
* | |
* @param value The value to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
private void write24(int value) throws IOException | |
{ | |
this.tempStream.write(value); | |
this.tempStream.write(value >> 8); | |
this.tempStream.write(value >> 16); | |
} | |
/** | |
* Writes a 16 bit, little endian value into the given stream. | |
* | |
* @param out Output stream to write to. | |
* @param value The value to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
private static void write16(final OutputStream out, int value) throws IOException | |
{ | |
out.write(value); | |
out.write(value >> 8); | |
} | |
/** | |
* Writes a 32 bit, little endian value into the given stream. | |
* | |
* @param out Output stream to write to. | |
* @param value The value to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
private static void write32(final OutputStream out, int value) throws IOException | |
{ | |
out.write(value); | |
out.write(value >> 8); | |
out.write(value >> 16); | |
out.write(value >> 24); | |
} | |
/** | |
* <p>Writes the given (signed) sample(s) into this RIFF WAVE clamped to the given bit depth.</p> | |
* <p>Samples are expected to be in [-128,128[, [-32768,32768[ or [-8388608,8388608[ range depending on bit depth.</p> | |
* | |
* @param samples The samples to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
public void write(int ... samples) throws IOException | |
{ | |
if(this.tempStream == null) | |
this.openTemp(); | |
switch(this.bitsPerSample) | |
{ | |
case 8: | |
for(int i = 0; i < samples.length; i++) | |
this.tempStream.write(clamp(samples[i], -0x80, 0x7f) + 0x80); | |
break; | |
case 16: | |
for(int i = 0; i < samples.length; i++) | |
this.write16(clamp(samples[i], -0x8000, 0x7fff)); | |
break; | |
case 24: | |
for(int i = 0; i < samples.length; i++) | |
this.write24(clamp(samples[i], -0x800000, 0x7fffff)); | |
break; | |
default: | |
throw new IOException("Unsupported bit depth: " + this.bitsPerSample); | |
} | |
} | |
/** | |
* <p>Writes the given sample(s) into this RIFF WAVE clamped to the given bit depth.</p> | |
* <p>Samples are expected to be in [-1.0,1.0] range.</p> | |
* | |
* @param samples The samples to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
public void write(float ... samples) throws IOException | |
{ | |
if(this.tempStream == null) | |
this.openTemp(); | |
switch(this.bitsPerSample) | |
{ | |
case 8: | |
for(int i = 0; i < samples.length; i++) | |
this.tempStream.write(clamp((int)(samples[i] * 128.0f), -0x80, 0x7f) + 0x80); | |
break; | |
case 16: | |
for(int i = 0; i < samples.length; i++) | |
this.write16(clamp((int)(samples[i] * 32768.0f), -0x8000, 0x7fff)); | |
break; | |
case 24: | |
for(int i = 0; i < samples.length; i++) | |
this.write24(clamp((int)(samples[i] * 8388608.0f), -0x800000, 0x7fffff)); | |
break; | |
default: | |
throw new IOException("Unsupported bit depth: " + this.bitsPerSample); | |
} | |
} | |
/** | |
* <p>Writes the given sample(s) into this RIFF WAVE clamped to the given bit depth.</p> | |
* <p>Samples are expected to be in [-1.0,1.0] range.</p> | |
* | |
* @param samples The samples to write. | |
* @throws IOException if an IO error occurred. | |
*/ | |
public void write(double ... samples) throws IOException | |
{ | |
if(this.tempStream == null) | |
this.openTemp(); | |
switch(this.bitsPerSample) | |
{ | |
case 8: | |
for(int i = 0; i < samples.length; i++) | |
this.tempStream.write(clamp((int)(samples[i] * 128.0), -0x80, 0x7f) + 0x80); | |
break; | |
case 16: | |
for(int i = 0; i < samples.length; i++) | |
this.write16(clamp((int)(samples[i] * 32768.0), -0x8000, 0x7fff)); | |
break; | |
case 24: | |
for(int i = 0; i < samples.length; i++) | |
this.write24(clamp((int)(samples[i] * 8388608.0), -0x800000, 0x7fffff)); | |
break; | |
default: | |
throw new IOException("Unsupported bit depth: " + this.bitsPerSample); | |
} | |
} | |
/** | |
* ... the mighty clamp ... | |
* | |
* @param x x | |
* @param min min | |
* @param max max | |
* @return x < min ? min : x > max ? max : x | |
*/ | |
public static int clamp(final int x, final int min, final int max) | |
{ | |
return x < min ? min : x > max ? max : x; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment