1 /*
2  * Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.sun.media.sound;
27 
28 import java.io.BufferedOutputStream;
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.DataOutputStream;
32 import java.io.File;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.io.RandomAccessFile;
38 import java.io.SequenceInputStream;
39 import java.util.Objects;
40 
41 import javax.sound.sampled.AudioFileFormat;
42 import javax.sound.sampled.AudioFormat;
43 import javax.sound.sampled.AudioInputStream;
44 import javax.sound.sampled.AudioSystem;
45 
46 //$$fb this class is buggy. Should be replaced in future.
47 
48 /**
49  * WAVE file writer.
50  *
51  * @author Jan Borgersen
52  */
53 public final class WaveFileWriter extends SunFileWriter {
54 
55     /**
56      * Constructs a new WaveFileWriter object.
57      */
WaveFileWriter()58     public WaveFileWriter() {
59         super(new AudioFileFormat.Type[]{AudioFileFormat.Type.WAVE});
60     }
61 
62     @Override
getAudioFileTypes(AudioInputStream stream)63     public AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) {
64 
65         AudioFileFormat.Type[] filetypes = new AudioFileFormat.Type[types.length];
66         System.arraycopy(types, 0, filetypes, 0, types.length);
67 
68         // make sure we can write this stream
69         AudioFormat format = stream.getFormat();
70         AudioFormat.Encoding encoding = format.getEncoding();
71 
72         if( AudioFormat.Encoding.ALAW.equals(encoding) ||
73             AudioFormat.Encoding.ULAW.equals(encoding) ||
74             AudioFormat.Encoding.PCM_SIGNED.equals(encoding) ||
75             AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding) ) {
76 
77             return filetypes;
78         }
79 
80         return new AudioFileFormat.Type[0];
81     }
82 
83     @Override
write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out)84     public int write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out) throws IOException {
85         Objects.requireNonNull(stream);
86         Objects.requireNonNull(fileType);
87         Objects.requireNonNull(out);
88 
89         //$$fb the following check must come first ! Otherwise
90         // the next frame length check may throw an IOException and
91         // interrupt iterating File Writers. (see bug 4351296)
92 
93         // throws IllegalArgumentException if not supported
94         WaveFileFormat waveFileFormat = (WaveFileFormat)getAudioFileFormat(fileType, stream);
95 
96         //$$fb when we got this far, we are committed to write this file
97 
98         // we must know the total data length to calculate the file length
99         if( stream.getFrameLength() == AudioSystem.NOT_SPECIFIED ) {
100             throw new IOException("stream length not specified");
101         }
102 
103         return writeWaveFile(stream, waveFileFormat, out);
104     }
105 
106     @Override
write(AudioInputStream stream, AudioFileFormat.Type fileType, File out)107     public int write(AudioInputStream stream, AudioFileFormat.Type fileType, File out) throws IOException {
108         Objects.requireNonNull(stream);
109         Objects.requireNonNull(fileType);
110         Objects.requireNonNull(out);
111 
112         // throws IllegalArgumentException if not supported
113         WaveFileFormat waveFileFormat = (WaveFileFormat)getAudioFileFormat(fileType, stream);
114 
115         // first write the file without worrying about length fields
116         final int bytesWritten;
117         try (final FileOutputStream fos = new FileOutputStream(out);
118              final BufferedOutputStream bos = new BufferedOutputStream(fos)) {
119             bytesWritten = writeWaveFile(stream, waveFileFormat, bos);
120         }
121 
122         // now, if length fields were not specified, calculate them,
123         // open as a random access file, write the appropriate fields,
124         // close again....
125         if( waveFileFormat.getByteLength()== AudioSystem.NOT_SPECIFIED ) {
126 
127             int dataLength=bytesWritten-waveFileFormat.getHeaderSize();
128             int riffLength=dataLength + waveFileFormat.getHeaderSize() - 8;
129             try (final RandomAccessFile raf = new RandomAccessFile(out, "rw")) {
130                 // skip RIFF magic
131                 raf.skipBytes(4);
132                 raf.writeInt(big2little(riffLength));
133                 // skip WAVE magic, fmt_ magic, fmt_ length, fmt_ chunk, data magic
134                 raf.skipBytes(4 + 4 + 4 + WaveFileFormat.getFmtChunkSize(
135                         waveFileFormat.getWaveType()) + 4);
136                 raf.writeInt(big2little(dataLength));
137                 // that's all
138             }
139         }
140 
141         return bytesWritten;
142     }
143 
144     //--------------------------------------------------------------------
145 
146     /**
147      * Returns the AudioFileFormat describing the file that will be written from this AudioInputStream.
148      * Throws IllegalArgumentException if not supported.
149      */
getAudioFileFormat(AudioFileFormat.Type type, AudioInputStream stream)150     private AudioFileFormat getAudioFileFormat(AudioFileFormat.Type type, AudioInputStream stream) {
151         if (!isFileTypeSupported(type, stream)) {
152             throw new IllegalArgumentException("File type " + type + " not supported.");
153         }
154         AudioFormat format = null;
155         WaveFileFormat fileFormat = null;
156         AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED;
157 
158         AudioFormat streamFormat = stream.getFormat();
159         AudioFormat.Encoding streamEncoding = streamFormat.getEncoding();
160 
161         float sampleRate;
162         int sampleSizeInBits;
163         int channels;
164         int frameSize;
165         float frameRate;
166         int fileSize;
167 
168         int waveType = WaveFileFormat.WAVE_FORMAT_PCM;
169 
170         if( AudioFormat.Encoding.ALAW.equals(streamEncoding) ||
171             AudioFormat.Encoding.ULAW.equals(streamEncoding) ) {
172 
173             encoding = streamEncoding;
174             sampleSizeInBits = streamFormat.getSampleSizeInBits();
175             if (streamEncoding.equals(AudioFormat.Encoding.ALAW)) {
176                 waveType = WaveFileFormat.WAVE_FORMAT_ALAW;
177             } else {
178                 waveType = WaveFileFormat.WAVE_FORMAT_MULAW;
179             }
180         } else if ( streamFormat.getSampleSizeInBits()==8 ) {
181             encoding = AudioFormat.Encoding.PCM_UNSIGNED;
182             sampleSizeInBits=8;
183         } else {
184             encoding = AudioFormat.Encoding.PCM_SIGNED;
185             sampleSizeInBits=streamFormat.getSampleSizeInBits();
186         }
187 
188 
189         format = new AudioFormat( encoding,
190                                   streamFormat.getSampleRate(),
191                                   sampleSizeInBits,
192                                   streamFormat.getChannels(),
193                                   streamFormat.getFrameSize(),
194                                   streamFormat.getFrameRate(),
195                                   false);       // WAVE is little endian
196 
197         if( stream.getFrameLength()!=AudioSystem.NOT_SPECIFIED ) {
198             fileSize = (int)stream.getFrameLength()*streamFormat.getFrameSize()
199                 + WaveFileFormat.getHeaderSize(waveType);
200         } else {
201             fileSize = AudioSystem.NOT_SPECIFIED;
202         }
203 
204         fileFormat = new WaveFileFormat( AudioFileFormat.Type.WAVE,
205                                          fileSize,
206                                          format,
207                                          (int)stream.getFrameLength() );
208 
209         return fileFormat;
210     }
211 
212 
writeWaveFile(InputStream in, WaveFileFormat waveFileFormat, OutputStream out)213     private int writeWaveFile(InputStream in, WaveFileFormat waveFileFormat, OutputStream out) throws IOException {
214 
215         int bytesRead = 0;
216         int bytesWritten = 0;
217         InputStream fileStream = getFileStream(waveFileFormat, in);
218         byte[] buffer = new byte[bisBufferSize];
219         int maxLength = waveFileFormat.getByteLength();
220 
221         while( (bytesRead = fileStream.read( buffer )) >= 0 ) {
222 
223             if (maxLength>0) {
224                 if( bytesRead < maxLength ) {
225                     out.write( buffer, 0, bytesRead );
226                     bytesWritten += bytesRead;
227                     maxLength -= bytesRead;
228                 } else {
229                     out.write( buffer, 0, maxLength );
230                     bytesWritten += maxLength;
231                     maxLength = 0;
232                     break;
233                 }
234             } else {
235                 out.write( buffer, 0, bytesRead );
236                 bytesWritten += bytesRead;
237             }
238         }
239 
240         return bytesWritten;
241     }
242 
getFileStream(WaveFileFormat waveFileFormat, InputStream audioStream)243     private InputStream getFileStream(WaveFileFormat waveFileFormat, InputStream audioStream) throws IOException {
244         // private method ... assumes audioFileFormat is a supported file type
245 
246         // WAVE header fields
247         AudioFormat audioFormat = waveFileFormat.getFormat();
248         int headerLength       = waveFileFormat.getHeaderSize();
249         int riffMagic          = WaveFileFormat.RIFF_MAGIC;
250         int waveMagic          = WaveFileFormat.WAVE_MAGIC;
251         int fmtMagic           = WaveFileFormat.FMT_MAGIC;
252         int fmtLength          = WaveFileFormat.getFmtChunkSize(waveFileFormat.getWaveType());
253         short wav_type         = (short) waveFileFormat.getWaveType();
254         short channels         = (short) audioFormat.getChannels();
255         short sampleSizeInBits = (short) audioFormat.getSampleSizeInBits();
256         int sampleRate         = (int) audioFormat.getSampleRate();
257         int frameSizeInBytes   = audioFormat.getFrameSize();
258         int frameRate              = (int) audioFormat.getFrameRate();
259         int avgBytesPerSec     = channels * sampleSizeInBits * sampleRate / 8;
260         short blockAlign       = (short) ((sampleSizeInBits / 8) * channels);
261         int dataMagic              = WaveFileFormat.DATA_MAGIC;
262         int dataLength             = waveFileFormat.getFrameLength() * frameSizeInBytes;
263         int length                         = waveFileFormat.getByteLength();
264         int riffLength = dataLength + headerLength - 8;
265 
266         AudioFormat audioStreamFormat = null;
267         AudioFormat.Encoding encoding = null;
268         InputStream codedAudioStream = audioStream;
269 
270         // if audioStream is an AudioInputStream and we need to convert, do it here...
271         if(audioStream instanceof AudioInputStream) {
272             audioStreamFormat = ((AudioInputStream)audioStream).getFormat();
273 
274             encoding = audioStreamFormat.getEncoding();
275 
276             if(AudioFormat.Encoding.PCM_SIGNED.equals(encoding)) {
277                 if( sampleSizeInBits==8 ) {
278                     wav_type = WaveFileFormat.WAVE_FORMAT_PCM;
279                     // plug in the transcoder to convert from PCM_SIGNED to PCM_UNSIGNED
280                     codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat(
281                                                                                         AudioFormat.Encoding.PCM_UNSIGNED,
282                                                                                         audioStreamFormat.getSampleRate(),
283                                                                                         audioStreamFormat.getSampleSizeInBits(),
284                                                                                         audioStreamFormat.getChannels(),
285                                                                                         audioStreamFormat.getFrameSize(),
286                                                                                         audioStreamFormat.getFrameRate(),
287                                                                                         false),
288                                                                         (AudioInputStream)audioStream);
289                 }
290             }
291             if( (AudioFormat.Encoding.PCM_SIGNED.equals(encoding) && audioStreamFormat.isBigEndian()) ||
292                 (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding) && !audioStreamFormat.isBigEndian()) ||
293                 (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding) && audioStreamFormat.isBigEndian()) ) {
294                 if( sampleSizeInBits!=8) {
295                     wav_type = WaveFileFormat.WAVE_FORMAT_PCM;
296                     // plug in the transcoder to convert to PCM_SIGNED_LITTLE_ENDIAN
297                     codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat(
298                                                                                         AudioFormat.Encoding.PCM_SIGNED,
299                                                                                         audioStreamFormat.getSampleRate(),
300                                                                                         audioStreamFormat.getSampleSizeInBits(),
301                                                                                         audioStreamFormat.getChannels(),
302                                                                                         audioStreamFormat.getFrameSize(),
303                                                                                         audioStreamFormat.getFrameRate(),
304                                                                                         false),
305                                                                         (AudioInputStream)audioStream);
306                 }
307             }
308         }
309 
310 
311         // Now push the header into a stream, concat, and return the new SequenceInputStream
312         final byte[] header;
313         try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
314              final DataOutputStream dos = new DataOutputStream(baos)) {
315             // we write in littleendian...
316             dos.writeInt(riffMagic);
317             dos.writeInt(big2little(riffLength));
318             dos.writeInt(waveMagic);
319             dos.writeInt(fmtMagic);
320             dos.writeInt(big2little(fmtLength));
321             dos.writeShort(big2littleShort(wav_type));
322             dos.writeShort(big2littleShort(channels));
323             dos.writeInt(big2little(sampleRate));
324             dos.writeInt(big2little(avgBytesPerSec));
325             dos.writeShort(big2littleShort(blockAlign));
326             dos.writeShort(big2littleShort(sampleSizeInBits));
327             //$$fb 2002-04-16: Fix for 4636355: RIFF audio headers could be _more_ spec compliant
328             if (wav_type != WaveFileFormat.WAVE_FORMAT_PCM) {
329                 // add length 0 for "codec specific data length"
330                 dos.writeShort(0);
331             }
332             dos.writeInt(dataMagic);
333             dos.writeInt(big2little(dataLength));
334             header = baos.toByteArray();
335         }
336         return new SequenceInputStream(new ByteArrayInputStream(header),
337                                        new NoCloseInputStream(codedAudioStream));
338     }
339 }
340