| /* |
| * 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.annotation.NonNull; |
| import android.media.AudioFormat; |
| import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; |
| import android.util.Log; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.channels.FileChannel; |
| import java.util.Arrays; |
| |
| /** |
| * 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 int mSampleRateInHz; |
| private int mAudioFormat; |
| private int mChannelCount; |
| |
| private FileChannel mFileChannel; |
| |
| private final UtteranceProgressDispatcher mDispatcher; |
| |
| private boolean mStarted = false; |
| private boolean mDone = false; |
| |
| /** Status code of synthesis */ |
| protected int mStatusCode; |
| |
| FileSynthesisCallback(@NonNull FileChannel fileChannel, |
| @NonNull UtteranceProgressDispatcher dispatcher, boolean clientIsUsingV2) { |
| super(clientIsUsingV2); |
| mFileChannel = fileChannel; |
| mDispatcher = dispatcher; |
| mStatusCode = TextToSpeech.SUCCESS; |
| } |
| |
| @Override |
| void stop() { |
| synchronized (mStateLock) { |
| if (mDone) { |
| return; |
| } |
| if (mStatusCode == TextToSpeech.STOPPED) { |
| return; |
| } |
| |
| mStatusCode = TextToSpeech.STOPPED; |
| cleanUp(); |
| mDispatcher.dispatchOnStop(); |
| } |
| } |
| |
| /** |
| * Must be called while holding the monitor on {@link #mStateLock}. |
| */ |
| private void cleanUp() { |
| closeFile(); |
| } |
| |
| /** |
| * Must be called while holding the monitor on {@link #mStateLock}. |
| */ |
| private void closeFile() { |
| // File will be closed by the SpeechItem in the speech service. |
| mFileChannel = null; |
| } |
| |
| @Override |
| public int getMaxBufferSize() { |
| return MAX_AUDIO_BUFFER_SIZE; |
| } |
| |
| @Override |
| public int start(int sampleRateInHz, int audioFormat, int channelCount) { |
| if (DBG) { |
| Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat |
| + "," + channelCount + ")"); |
| } |
| if (audioFormat != AudioFormat.ENCODING_PCM_8BIT && |
| audioFormat != AudioFormat.ENCODING_PCM_16BIT && |
| audioFormat != AudioFormat.ENCODING_PCM_FLOAT) { |
| Log.e(TAG, "Audio format encoding " + audioFormat + " not supported. Please use one " + |
| "of AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT or " + |
| "AudioFormat.ENCODING_PCM_FLOAT"); |
| } |
| mDispatcher.dispatchOnBeginSynthesis(sampleRateInHz, audioFormat, channelCount); |
| |
| FileChannel fileChannel = null; |
| synchronized (mStateLock) { |
| if (mStatusCode == TextToSpeech.STOPPED) { |
| if (DBG) Log.d(TAG, "Request has been aborted."); |
| return errorCodeOnStop(); |
| } |
| if (mStatusCode != TextToSpeech.SUCCESS) { |
| if (DBG) Log.d(TAG, "Error was raised"); |
| return TextToSpeech.ERROR; |
| } |
| if (mStarted) { |
| Log.e(TAG, "Start called twice"); |
| return TextToSpeech.ERROR; |
| } |
| mStarted = true; |
| mSampleRateInHz = sampleRateInHz; |
| mAudioFormat = audioFormat; |
| mChannelCount = channelCount; |
| |
| mDispatcher.dispatchOnStart(); |
| fileChannel = mFileChannel; |
| } |
| |
| try { |
| fileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); |
| return TextToSpeech.SUCCESS; |
| } catch (IOException ex) { |
| Log.e(TAG, "Failed to write wav header to output file descriptor", ex); |
| synchronized (mStateLock) { |
| cleanUp(); |
| mStatusCode = TextToSpeech.ERROR_OUTPUT; |
| } |
| return TextToSpeech.ERROR; |
| } |
| } |
| |
| @Override |
| public int audioAvailable(byte[] buffer, int offset, int length) { |
| if (DBG) { |
| Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + Arrays.toString(buffer) |
| + "," + offset + "," + length + ")"); |
| } |
| FileChannel fileChannel = null; |
| synchronized (mStateLock) { |
| if (mStatusCode == TextToSpeech.STOPPED) { |
| if (DBG) Log.d(TAG, "Request has been aborted."); |
| return errorCodeOnStop(); |
| } |
| if (mStatusCode != TextToSpeech.SUCCESS) { |
| if (DBG) Log.d(TAG, "Error was raised"); |
| return TextToSpeech.ERROR; |
| } |
| if (mFileChannel == null) { |
| Log.e(TAG, "File not open"); |
| mStatusCode = TextToSpeech.ERROR_OUTPUT; |
| return TextToSpeech.ERROR; |
| } |
| if (!mStarted) { |
| Log.e(TAG, "Start method was not called"); |
| return TextToSpeech.ERROR; |
| } |
| fileChannel = mFileChannel; |
| } |
| |
| final byte[] bufferCopy = new byte[length]; |
| System.arraycopy(buffer, offset, bufferCopy, 0, length); |
| mDispatcher.dispatchOnAudioAvailable(bufferCopy); |
| |
| try { |
| fileChannel.write(ByteBuffer.wrap(buffer, offset, length)); |
| return TextToSpeech.SUCCESS; |
| } catch (IOException ex) { |
| Log.e(TAG, "Failed to write to output file descriptor", ex); |
| synchronized (mStateLock) { |
| cleanUp(); |
| mStatusCode = TextToSpeech.ERROR_OUTPUT; |
| } |
| return TextToSpeech.ERROR; |
| } |
| } |
| |
| @Override |
| public int done() { |
| if (DBG) Log.d(TAG, "FileSynthesisRequest.done()"); |
| FileChannel fileChannel = null; |
| |
| int sampleRateInHz = 0; |
| int audioFormat = 0; |
| int channelCount = 0; |
| |
| synchronized (mStateLock) { |
| if (mDone) { |
| Log.w(TAG, "Duplicate call to done()"); |
| // This is not an error that would prevent synthesis. Hence no |
| // setStatusCode is set. |
| return TextToSpeech.ERROR; |
| } |
| if (mStatusCode == TextToSpeech.STOPPED) { |
| if (DBG) Log.d(TAG, "Request has been aborted."); |
| return errorCodeOnStop(); |
| } |
| if (mStatusCode != TextToSpeech.SUCCESS && mStatusCode != TextToSpeech.STOPPED) { |
| mDispatcher.dispatchOnError(mStatusCode); |
| return TextToSpeech.ERROR; |
| } |
| if (mFileChannel == null) { |
| Log.e(TAG, "File not open"); |
| return TextToSpeech.ERROR; |
| } |
| mDone = true; |
| fileChannel = mFileChannel; |
| sampleRateInHz = mSampleRateInHz; |
| audioFormat = mAudioFormat; |
| channelCount = mChannelCount; |
| } |
| |
| try { |
| // Write WAV header at start of file |
| fileChannel.position(0); |
| int dataLength = (int) (fileChannel.size() - WAV_HEADER_LENGTH); |
| fileChannel.write( |
| makeWavHeader(sampleRateInHz, audioFormat, channelCount, dataLength)); |
| |
| synchronized (mStateLock) { |
| closeFile(); |
| mDispatcher.dispatchOnSuccess(); |
| return TextToSpeech.SUCCESS; |
| } |
| } catch (IOException ex) { |
| Log.e(TAG, "Failed to write to output file descriptor", ex); |
| synchronized (mStateLock) { |
| cleanUp(); |
| } |
| return TextToSpeech.ERROR; |
| } |
| } |
| |
| @Override |
| public void error() { |
| error(TextToSpeech.ERROR_SYNTHESIS); |
| } |
| |
| @Override |
| public void error(int errorCode) { |
| if (DBG) Log.d(TAG, "FileSynthesisRequest.error()"); |
| synchronized (mStateLock) { |
| if (mDone) { |
| return; |
| } |
| cleanUp(); |
| mStatusCode = errorCode; |
| } |
| } |
| |
| @Override |
| public boolean hasStarted() { |
| synchronized (mStateLock) { |
| return mStarted; |
| } |
| } |
| |
| @Override |
| public boolean hasFinished() { |
| synchronized (mStateLock) { |
| return mDone; |
| } |
| } |
| |
| private ByteBuffer makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount, |
| int dataLength) { |
| int sampleSizeInBytes = AudioFormat.getBytesPerSample(audioFormat); |
| 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); |
| header.flip(); |
| |
| return header; |
| } |
| |
| @Override |
| public void rangeStart(int markerInFrames, int start, int end) { |
| mDispatcher.dispatchOnRangeStart(markerInFrames, start, end); |
| } |
| } |