FileSynthesisCallback.java revision 563fd3ae1b9d388f883b77b98a641a57bbdb6aed
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package android.speech.tts;
17
18import android.media.AudioFormat;
19import android.os.FileUtils;
20import android.util.Log;
21
22import java.io.File;
23import java.io.IOException;
24import java.io.RandomAccessFile;
25import java.nio.ByteBuffer;
26import java.nio.ByteOrder;
27
28/**
29 * Speech synthesis request that writes the audio to a WAV file.
30 */
31class FileSynthesisCallback extends AbstractSynthesisCallback {
32
33    private static final String TAG = "FileSynthesisRequest";
34    private static final boolean DBG = false;
35
36    private static final int MAX_AUDIO_BUFFER_SIZE = 8192;
37
38    private static final int WAV_HEADER_LENGTH = 44;
39    private static final short WAV_FORMAT_PCM = 0x0001;
40
41    private final Object mStateLock = new Object();
42    private final File mFileName;
43    private int mSampleRateInHz;
44    private int mAudioFormat;
45    private int mChannelCount;
46    private RandomAccessFile mFile;
47    private boolean mStopped = false;
48    private boolean mDone = false;
49
50    FileSynthesisCallback(File fileName) {
51        mFileName = fileName;
52    }
53
54    @Override
55    void stop() {
56        synchronized (mStateLock) {
57            mStopped = true;
58            cleanUp();
59        }
60    }
61
62    /**
63     * Must be called while holding the monitor on {@link #mStateLock}.
64     */
65    private void cleanUp() {
66        closeFileAndWidenPermissions();
67        if (mFile != null) {
68            mFileName.delete();
69        }
70    }
71
72    /**
73     * Must be called while holding the monitor on {@link #mStateLock}.
74     */
75    private void closeFileAndWidenPermissions() {
76        try {
77            if (mFile != null) {
78                mFile.close();
79                mFile = null;
80            }
81        } catch (IOException ex) {
82            Log.e(TAG, "Failed to close " + mFileName + ": " + ex);
83        }
84
85        try {
86            // Make the written file readable and writeable by everyone.
87            // This allows the app that requested synthesis to read the file.
88            //
89            // Note that the directory this file was written must have already
90            // been world writeable in order it to have been
91            // written to in the first place.
92            FileUtils.setPermissions(mFileName.getAbsolutePath(), 0666, -1, -1); //-rw-rw-rw
93        } catch (SecurityException se) {
94            Log.e(TAG, "Security exception setting rw permissions on : " + mFileName);
95        }
96    }
97
98    @Override
99    public int getMaxBufferSize() {
100        return MAX_AUDIO_BUFFER_SIZE;
101    }
102
103    @Override
104    boolean isDone() {
105        return mDone;
106    }
107
108    @Override
109    public int start(int sampleRateInHz, int audioFormat, int channelCount) {
110        if (DBG) {
111            Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat
112                    + "," + channelCount + ")");
113        }
114        synchronized (mStateLock) {
115            if (mStopped) {
116                if (DBG) Log.d(TAG, "Request has been aborted.");
117                return TextToSpeech.ERROR;
118            }
119            if (mFile != null) {
120                cleanUp();
121                throw new IllegalArgumentException("FileSynthesisRequest.start() called twice");
122            }
123            mSampleRateInHz = sampleRateInHz;
124            mAudioFormat = audioFormat;
125            mChannelCount = channelCount;
126            try {
127                mFile = new RandomAccessFile(mFileName, "rw");
128                // Reserve space for WAV header
129                mFile.write(new byte[WAV_HEADER_LENGTH]);
130                return TextToSpeech.SUCCESS;
131            } catch (IOException ex) {
132                Log.e(TAG, "Failed to open " + mFileName + ": " + ex);
133                cleanUp();
134                return TextToSpeech.ERROR;
135            }
136        }
137    }
138
139    @Override
140    public int audioAvailable(byte[] buffer, int offset, int length) {
141        if (DBG) {
142            Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + buffer + "," + offset
143                    + "," + length + ")");
144        }
145        synchronized (mStateLock) {
146            if (mStopped) {
147                if (DBG) Log.d(TAG, "Request has been aborted.");
148                return TextToSpeech.ERROR;
149            }
150            if (mFile == null) {
151                Log.e(TAG, "File not open");
152                return TextToSpeech.ERROR;
153            }
154            try {
155                mFile.write(buffer, offset, length);
156                return TextToSpeech.SUCCESS;
157            } catch (IOException ex) {
158                Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
159                cleanUp();
160                return TextToSpeech.ERROR;
161            }
162        }
163    }
164
165    @Override
166    public int done() {
167        if (DBG) Log.d(TAG, "FileSynthesisRequest.done()");
168        synchronized (mStateLock) {
169            if (mStopped) {
170                if (DBG) Log.d(TAG, "Request has been aborted.");
171                return TextToSpeech.ERROR;
172            }
173            if (mFile == null) {
174                Log.e(TAG, "File not open");
175                return TextToSpeech.ERROR;
176            }
177            try {
178                // Write WAV header at start of file
179                mFile.seek(0);
180                int dataLength = (int) (mFile.length() - WAV_HEADER_LENGTH);
181                mFile.write(
182                        makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength));
183                closeFileAndWidenPermissions();
184                mDone = true;
185                return TextToSpeech.SUCCESS;
186            } catch (IOException ex) {
187                Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
188                cleanUp();
189                return TextToSpeech.ERROR;
190            }
191        }
192    }
193
194    @Override
195    public void error() {
196        if (DBG) Log.d(TAG, "FileSynthesisRequest.error()");
197        synchronized (mStateLock) {
198            cleanUp();
199        }
200    }
201
202    private byte[] makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount,
203            int dataLength) {
204        // TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT?
205        int sampleSizeInBytes = (audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2);
206        int byteRate = sampleRateInHz * sampleSizeInBytes * channelCount;
207        short blockAlign = (short) (sampleSizeInBytes * channelCount);
208        short bitsPerSample = (short) (sampleSizeInBytes * 8);
209
210        byte[] headerBuf = new byte[WAV_HEADER_LENGTH];
211        ByteBuffer header = ByteBuffer.wrap(headerBuf);
212        header.order(ByteOrder.LITTLE_ENDIAN);
213
214        header.put(new byte[]{ 'R', 'I', 'F', 'F' });
215        header.putInt(dataLength + WAV_HEADER_LENGTH - 8);  // RIFF chunk size
216        header.put(new byte[]{ 'W', 'A', 'V', 'E' });
217        header.put(new byte[]{ 'f', 'm', 't', ' ' });
218        header.putInt(16);  // size of fmt chunk
219        header.putShort(WAV_FORMAT_PCM);
220        header.putShort((short) channelCount);
221        header.putInt(sampleRateInHz);
222        header.putInt(byteRate);
223        header.putShort(blockAlign);
224        header.putShort(bitsPerSample);
225        header.put(new byte[]{ 'd', 'a', 't', 'a' });
226        header.putInt(dataLength);
227
228        return headerBuf;
229    }
230
231}
232