/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2016, Telestax Inc and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* @author pavel.shlupacek@spinoco.com
*/
package org.restcomm.media.resource.recorder.audio;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Sink, that assures the data are written to underlying file.
*
* Sink exists once the recording starts, and ceases to exists on recording deactivate.
*
* @author Pavel Chlupacek (pchlupacek)
*/
public class RecorderFileSink {
private static final Logger logger = Logger.getLogger(RecorderFileSink.class);
private static final int HDR_SIZE = 44;
private static final ByteBuffer EMPTY_HEADER = ByteBuffer.wrap(new byte[HDR_SIZE]).asReadOnlyBuffer();
// target and temp file used for recording
private final Path target;
private final Path temp;
// whether the recording shall be appended to target, if that target exists
private final boolean append;
// destination of write operation
private final FileChannel fout;
// when true, then this sink accepts new data false otherwise.
private final AtomicBoolean open;
/**
* Creates a sink. If append is true, and target exists, then when recording is finished the resulting recording is appended
* to current recorded file.
*
* @param target Target to write file to
* @param append Whether to append recording to `target`
*/
public RecorderFileSink(Path target, boolean append) throws IOException {
this.target = target;
this.temp = target.getParent().resolve(target.getFileName() + "~");
this.append = append;
this.open = new AtomicBoolean(true);
this.fout = FileChannel.open(temp, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
this.fout.write(EMPTY_HEADER);
}
/**
* Writes supplied data to the Sink (File).
*/
public void write(ByteBuffer data) throws IOException {
if (open.get()) {
fout.write(data);
}
}
/**
* Commit this sink. Causes to prevent any further write operations, and commits temporary file to target. When this
* returns, Sink is done and cannot be used again.
*/
public void commit() throws IOException {
// assures we perform the close operation only once.
if (open.compareAndSet(true, false)) {
// flush & close
fout.force(true);
fout.close();
// if the current file exists, and append is true, then append samples and remove temp file
// otherwise write header and move tmp file to target
boolean exists = Files.exists(target);
if (logger.isInfoEnabled()) {
logger.info("Finishing recording ...... append: " + append + " exists: " + exists + " target:" + target);
}
if (append && exists) {
appendSamples(target, temp);
writeHeader(target);
Files.delete(temp);
} else {
writeHeader(temp);
Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING);
}
}
}
@Override
public String toString() {
return "RecorderFileSink{" + "target=" + target + ", temp=" + temp + ", append=" + append + ", open=" + open.get()
+ '}';
}
/**
* Writes samples to file following WAVE format.
*
*
* @param file Recording where to write the header
*
* @throws IOException
*/
private static void writeHeader(Path file) throws IOException {
try (FileChannel fout = FileChannel.open(file, StandardOpenOption.WRITE)) {
long size = fout.size();
int sampleSize = (int) size - 44;
if (logger.isInfoEnabled()) {
logger.info("Size " + sampleSize + " of recording file " + file);
}
ByteBuffer headerBuffer = ByteBuffer.allocateDirect(44);
headerBuffer.clear();
// RIFF
headerBuffer.put((byte) 0x52);
headerBuffer.put((byte) 0x49);
headerBuffer.put((byte) 0x46);
headerBuffer.put((byte) 0x46);
int length = sampleSize + 36;
// Length
headerBuffer.put((byte) (length));
headerBuffer.put((byte) (length >> 8));
headerBuffer.put((byte) (length >> 16));
headerBuffer.put((byte) (length >> 24));
// WAVE
headerBuffer.put((byte) 0x57);
headerBuffer.put((byte) 0x41);
headerBuffer.put((byte) 0x56);
headerBuffer.put((byte) 0x45);
// fmt
headerBuffer.put((byte) 0x66);
headerBuffer.put((byte) 0x6d);
headerBuffer.put((byte) 0x74);
headerBuffer.put((byte) 0x20);
headerBuffer.put((byte) 0x10);
headerBuffer.put((byte) 0x00);
headerBuffer.put((byte) 0x00);
headerBuffer.put((byte) 0x00);
// format - PCM
headerBuffer.put((byte) 0x01);
headerBuffer.put((byte) 0x00);
// format - MONO
headerBuffer.put((byte) 0x01);
headerBuffer.put((byte) 0x00);
// sample rate:8000
headerBuffer.put((byte) 0x40);
headerBuffer.put((byte) 0x1F);
headerBuffer.put((byte) 0x00);
headerBuffer.put((byte) 0x00);
// byte rate
headerBuffer.put((byte) 0x80);
headerBuffer.put((byte) 0x3E);
headerBuffer.put((byte) 0x00);
headerBuffer.put((byte) 0x00);
// Block align
headerBuffer.put((byte) 0x02);
headerBuffer.put((byte) 0x00);
// Bits per sample: 16
headerBuffer.put((byte) 0x10);
headerBuffer.put((byte) 0x00);
// "data"
headerBuffer.put((byte) 0x64);
headerBuffer.put((byte) 0x61);
headerBuffer.put((byte) 0x74);
headerBuffer.put((byte) 0x61);
// len
headerBuffer.put((byte) (sampleSize));
headerBuffer.put((byte) (sampleSize >> 8));
headerBuffer.put((byte) (sampleSize >> 16));
headerBuffer.put((byte) (sampleSize >> 24));
headerBuffer.rewind();
// lets write header
fout.position(0);
fout.write(headerBuffer);
}
}
private static void appendSamples(Path appendTo, Path appendFrom) throws IOException {
try (FileChannel inChannel = FileChannel.open(appendFrom, StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(appendTo, StandardOpenOption.WRITE)) {
long count = inChannel.size() - HDR_SIZE;
inChannel.transferTo(HDR_SIZE, count, outChannel);
if (logger.isInfoEnabled()) {
logger.info("Appended " + count + " bytes from " + appendFrom + " to " + appendTo);
}
}
}
}