AudioBlockRecordingWriter.java
package org.hammer.audio.recording;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;
/**
* Writes {@link AudioBlock}s to a binary recording file as documented in {@link
* AudioBlockRecordingFormat}.
*
* <p>The writer derives its format header from the first {@link #write(AudioBlock)} call. All
* subsequent blocks must use the same {@link AudioFormatDescriptor}.
*
* <p>Instances are <strong>not thread-safe</strong>.
*/
public final class AudioBlockRecordingWriter implements Closeable {
private final DataOutputStream out;
private AudioFormatDescriptor format;
private long blocksWritten;
private boolean closed;
/** Open a writer that writes to the given file (creating/truncating it). */
public static AudioBlockRecordingWriter open(Path file) throws IOException {
Objects.requireNonNull(file, "file");
return new AudioBlockRecordingWriter(Files.newOutputStream(file));
}
/** Wrap an existing output stream. The stream will be closed by {@link #close()}. */
public AudioBlockRecordingWriter(OutputStream stream) {
this.out = new DataOutputStream(Objects.requireNonNull(stream, "stream"));
}
/**
* @return the format header that was written (or {@code null} if no block was written yet)
*/
public AudioFormatDescriptor format() {
return format;
}
/**
* @return number of blocks successfully written so far
*/
public long blocksWritten() {
return blocksWritten;
}
/**
* Append one block to the recording. The first call writes the file header.
*
* @param block block to write; must not be {@code null}
* @throws IOException if the underlying stream fails
* @throws IllegalStateException if the block's format differs from a previously written block
*/
public void write(AudioBlock block) throws IOException {
Objects.requireNonNull(block, "block");
if (closed) {
throw new IllegalStateException("writer is closed");
}
if (format == null) {
writeHeader(block.format());
format = block.format();
} else if (!format.equals(block.format())) {
throw new IllegalStateException(
"format mismatch: expected " + format + " but block was " + block.format());
}
int frames = block.frames();
int channels = block.channels();
out.writeInt(frames);
out.writeLong(block.frameIndex());
out.writeLong(block.timestampNanos());
for (int ch = 0; ch < channels; ch++) {
float[] samples = block.channelView(ch);
for (int i = 0; i < frames; i++) {
out.writeFloat(samples[i]);
}
}
blocksWritten++;
}
private void writeHeader(AudioFormatDescriptor fmt) throws IOException {
out.writeInt(AudioBlockRecordingFormat.MAGIC);
out.writeShort(AudioBlockRecordingFormat.VERSION);
out.writeShort(fmt.channels());
out.writeFloat(fmt.sampleRate());
out.writeShort(fmt.sourceSampleSizeInBits());
out.writeShort(0); // reserved
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
out.close();
}
}