blob: 4f4b3fb6f2038b9d98e4e9a97584f7886d15d3b5 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package android.speech.tts;
import android.media.AudioFormat;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Speech synthesis request that writes the audio to a WAV file.
*/
class FileSynthesisCallback extends AbstractSynthesisCallback {
private static final String TAG = "FileSynthesisRequest";
private static final boolean DBG = false;
private static final int MAX_AUDIO_BUFFER_SIZE = 8192;
private static final int WAV_HEADER_LENGTH = 44;
private static final short WAV_FORMAT_PCM = 0x0001;
private final Object mStateLock = new Object();
private final File mFileName;
private int mSampleRateInHz;
private int mAudioFormat;
private int mChannelCount;
private RandomAccessFile mFile;
private boolean mStopped = false;
private boolean mDone = false;
FileSynthesisCallback(File fileName) {
mFileName = fileName;
}
@Override
void stop() {
synchronized (mStateLock) {
mStopped = true;
cleanUp();
}
}
/**
* Must be called while holding the monitor on {@link #mStateLock}.
*/
private void cleanUp() {
closeFile();
if (mFile != null) {
mFileName.delete();
}
}
/**
* Must be called while holding the monitor on {@link #mStateLock}.
*/
private void closeFile() {
try {
if (mFile != null) {
mFile.close();
mFile = null;
}
} catch (IOException ex) {
Log.e(TAG, "Failed to close " + mFileName + ": " + ex);
}
}
@Override
public int getMaxBufferSize() {
return MAX_AUDIO_BUFFER_SIZE;
}
@Override
boolean isDone() {
return mDone;
}
@Override
public int start(int sampleRateInHz, int audioFormat, int channelCount) {
if (DBG) {
Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat
+ "," + channelCount + ")");
}
synchronized (mStateLock) {
if (mStopped) {
if (DBG) Log.d(TAG, "Request has been aborted.");
return TextToSpeech.ERROR;
}
if (mFile != null) {
cleanUp();
throw new IllegalArgumentException("FileSynthesisRequest.start() called twice");
}
mSampleRateInHz = sampleRateInHz;
mAudioFormat = audioFormat;
mChannelCount = channelCount;
try {
mFile = new RandomAccessFile(mFileName, "rw");
// Reserve space for WAV header
mFile.write(new byte[WAV_HEADER_LENGTH]);
return TextToSpeech.SUCCESS;
} catch (IOException ex) {
Log.e(TAG, "Failed to open " + mFileName + ": " + ex);
cleanUp();
return TextToSpeech.ERROR;
}
}
}
@Override
public int audioAvailable(byte[] buffer, int offset, int length) {
if (DBG) {
Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + buffer + "," + offset
+ "," + length + ")");
}
synchronized (mStateLock) {
if (mStopped) {
if (DBG) Log.d(TAG, "Request has been aborted.");
return TextToSpeech.ERROR;
}
if (mFile == null) {
Log.e(TAG, "File not open");
return TextToSpeech.ERROR;
}
try {
mFile.write(buffer, offset, length);
return TextToSpeech.SUCCESS;
} catch (IOException ex) {
Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
cleanUp();
return TextToSpeech.ERROR;
}
}
}
@Override
public int done() {
if (DBG) Log.d(TAG, "FileSynthesisRequest.done()");
synchronized (mStateLock) {
if (mStopped) {
if (DBG) Log.d(TAG, "Request has been aborted.");
return TextToSpeech.ERROR;
}
if (mFile == null) {
Log.e(TAG, "File not open");
return TextToSpeech.ERROR;
}
try {
// Write WAV header at start of file
mFile.seek(0);
int dataLength = (int) (mFile.length() - WAV_HEADER_LENGTH);
mFile.write(
makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength));
closeFile();
mDone = true;
return TextToSpeech.SUCCESS;
} catch (IOException ex) {
Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
cleanUp();
return TextToSpeech.ERROR;
}
}
}
@Override
public void error() {
if (DBG) Log.d(TAG, "FileSynthesisRequest.error()");
synchronized (mStateLock) {
cleanUp();
}
}
@Override
public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount,
byte[] buffer, int offset, int length) {
synchronized (mStateLock) {
if (mStopped) {
if (DBG) Log.d(TAG, "Request has been aborted.");
return TextToSpeech.ERROR;
}
}
FileOutputStream out = null;
try {
out = new FileOutputStream(mFileName);
out.write(makeWavHeader(sampleRateInHz, audioFormat, channelCount, length));
out.write(buffer, offset, length);
mDone = true;
return TextToSpeech.SUCCESS;
} catch (IOException ex) {
Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
mFileName.delete();
return TextToSpeech.ERROR;
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException ex) {
Log.e(TAG, "Failed to close " + mFileName + ": " + ex);
}
}
}
private byte[] makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount,
int dataLength) {
// TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT?
int sampleSizeInBytes = (audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2);
int byteRate = sampleRateInHz * sampleSizeInBytes * channelCount;
short blockAlign = (short) (sampleSizeInBytes * channelCount);
short bitsPerSample = (short) (sampleSizeInBytes * 8);
byte[] headerBuf = new byte[WAV_HEADER_LENGTH];
ByteBuffer header = ByteBuffer.wrap(headerBuf);
header.order(ByteOrder.LITTLE_ENDIAN);
header.put(new byte[]{ 'R', 'I', 'F', 'F' });
header.putInt(dataLength + WAV_HEADER_LENGTH - 8); // RIFF chunk size
header.put(new byte[]{ 'W', 'A', 'V', 'E' });
header.put(new byte[]{ 'f', 'm', 't', ' ' });
header.putInt(16); // size of fmt chunk
header.putShort(WAV_FORMAT_PCM);
header.putShort((short) channelCount);
header.putInt(sampleRateInHz);
header.putInt(byteRate);
header.putShort(blockAlign);
header.putShort(bitsPerSample);
header.put(new byte[]{ 'd', 'a', 't', 'a' });
header.putInt(dataLength);
return headerBuf;
}
}