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  * AIFF file writer.
50  *
51  * @author Jan Borgersen
52  */
53 public final class AiffFileWriter extends SunFileWriter {
54 
55     /**
56      * Constructs a new AiffFileWriter object.
57      */
AiffFileWriter()58     public AiffFileWriter() {
59         super(new AudioFileFormat.Type[]{AudioFileFormat.Type.AIFF});
60     }
61 
62     // METHODS TO IMPLEMENT AudioFileWriter
63 
64     @Override
getAudioFileTypes(AudioInputStream stream)65     public AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) {
66 
67         AudioFileFormat.Type[] filetypes = new AudioFileFormat.Type[types.length];
68         System.arraycopy(types, 0, filetypes, 0, types.length);
69 
70         // make sure we can write this stream
71         AudioFormat format = stream.getFormat();
72         AudioFormat.Encoding encoding = format.getEncoding();
73 
74         if( (AudioFormat.Encoding.ALAW.equals(encoding)) ||
75             (AudioFormat.Encoding.ULAW.equals(encoding)) ||
76             (AudioFormat.Encoding.PCM_SIGNED.equals(encoding)) ||
77             (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding)) ) {
78 
79             return filetypes;
80         }
81 
82         return new AudioFileFormat.Type[0];
83     }
84 
85     @Override
write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out)86     public int write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out) throws IOException {
87         Objects.requireNonNull(stream);
88         Objects.requireNonNull(fileType);
89         Objects.requireNonNull(out);
90 
91         //$$fb the following check must come first ! Otherwise
92         // the next frame length check may throw an IOException and
93         // interrupt iterating File Writers. (see bug 4351296)
94 
95         // throws IllegalArgumentException if not supported
96         AiffFileFormat aiffFileFormat = (AiffFileFormat)getAudioFileFormat(fileType, stream);
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 writeAiffFile(stream, aiffFileFormat, 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         AiffFileFormat aiffFileFormat = (AiffFileFormat)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 = writeAiffFile(stream, aiffFileFormat, 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( aiffFileFormat.getByteLength()== AudioSystem.NOT_SPECIFIED ) {
126 
127             // $$kk: 10.22.99: jan: please either implement this or throw an exception!
128             // $$fb: 2001-07-13: done. Fixes Bug 4479981
129             int channels = aiffFileFormat.getFormat().getChannels();
130             int sampleSize = aiffFileFormat.getFormat().getSampleSizeInBits();
131             int ssndBlockSize = channels * ((sampleSize + 7) / 8);
132 
133             int aiffLength=bytesWritten;
134             int ssndChunkSize=aiffLength-aiffFileFormat.getHeaderSize()+16;
135             long dataSize=ssndChunkSize-16;
136             //TODO possibly incorrect round
137             int numFrames = (int) (dataSize / ssndBlockSize);
138             try (final RandomAccessFile raf = new RandomAccessFile(out, "rw")) {
139                 // skip FORM magic
140                 raf.skipBytes(4);
141                 raf.writeInt(aiffLength - 8);
142                 // skip aiff2 magic, fver chunk, comm magic, comm size, channel count,
143                 raf.skipBytes(4 + aiffFileFormat.getFverChunkSize() + 4 + 4 + 2);
144                 // write frame count
145                 raf.writeInt(numFrames);
146                 // skip sample size, samplerate, SSND magic
147                 raf.skipBytes(2 + 10 + 4);
148                 raf.writeInt(ssndChunkSize - 8);
149                 // that's all
150             }
151         }
152 
153         return bytesWritten;
154     }
155 
156 
157     // -----------------------------------------------------------------------
158 
159     /**
160      * Returns the AudioFileFormat describing the file that will be written from this AudioInputStream.
161      * Throws IllegalArgumentException if not supported.
162      */
getAudioFileFormat(AudioFileFormat.Type type, AudioInputStream stream)163     private AudioFileFormat getAudioFileFormat(AudioFileFormat.Type type, AudioInputStream stream) {
164         if (!isFileTypeSupported(type, stream)) {
165             throw new IllegalArgumentException("File type " + type + " not supported.");
166         }
167 
168         AudioFormat format = null;
169         AiffFileFormat fileFormat = null;
170         AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED;
171 
172         AudioFormat streamFormat = stream.getFormat();
173         AudioFormat.Encoding streamEncoding = streamFormat.getEncoding();
174 
175         int sampleSizeInBits;
176         int fileSize;
177         boolean convert8to16 = false;
178 
179         if( (AudioFormat.Encoding.ALAW.equals(streamEncoding)) ||
180             (AudioFormat.Encoding.ULAW.equals(streamEncoding)) ) {
181 
182             if( streamFormat.getSampleSizeInBits()==8 ) {
183 
184                 encoding = AudioFormat.Encoding.PCM_SIGNED;
185                 sampleSizeInBits=16;
186                 convert8to16 = true;
187 
188             } else {
189 
190                 // can't convert non-8-bit ALAW,ULAW
191                 throw new IllegalArgumentException("Encoding " + streamEncoding + " supported only for 8-bit data.");
192             }
193         } else if ( streamFormat.getSampleSizeInBits()==8 ) {
194 
195             encoding = AudioFormat.Encoding.PCM_UNSIGNED;
196             sampleSizeInBits=8;
197 
198         } else {
199 
200             encoding = AudioFormat.Encoding.PCM_SIGNED;
201             sampleSizeInBits=streamFormat.getSampleSizeInBits();
202         }
203 
204 
205         format = new AudioFormat( encoding,
206                                   streamFormat.getSampleRate(),
207                                   sampleSizeInBits,
208                                   streamFormat.getChannels(),
209                                   streamFormat.getFrameSize(),
210                                   streamFormat.getFrameRate(),
211                                   true);        // AIFF is big endian
212 
213 
214         if( stream.getFrameLength()!=AudioSystem.NOT_SPECIFIED ) {
215             if( convert8to16 ) {
216                 fileSize = (int)stream.getFrameLength()*streamFormat.getFrameSize()*2 + AiffFileFormat.AIFF_HEADERSIZE;
217             } else {
218                 fileSize = (int)stream.getFrameLength()*streamFormat.getFrameSize() + AiffFileFormat.AIFF_HEADERSIZE;
219             }
220         } else {
221             fileSize = AudioSystem.NOT_SPECIFIED;
222         }
223 
224         fileFormat = new AiffFileFormat( AudioFileFormat.Type.AIFF,
225                                          fileSize,
226                                          format,
227                                          (int)stream.getFrameLength() );
228 
229         return fileFormat;
230     }
231 
writeAiffFile(InputStream in, AiffFileFormat aiffFileFormat, OutputStream out)232     private int writeAiffFile(InputStream in, AiffFileFormat aiffFileFormat, OutputStream out) throws IOException {
233 
234         int bytesRead = 0;
235         int bytesWritten = 0;
236         InputStream fileStream = getFileStream(aiffFileFormat, in);
237         byte[] buffer = new byte[bisBufferSize];
238         int maxLength = aiffFileFormat.getByteLength();
239 
240         while( (bytesRead = fileStream.read( buffer )) >= 0 ) {
241             if (maxLength>0) {
242                 if( bytesRead < maxLength ) {
243                     out.write( buffer, 0, bytesRead );
244                     bytesWritten += bytesRead;
245                     maxLength -= bytesRead;
246                 } else {
247                     out.write( buffer, 0, maxLength );
248                     bytesWritten += maxLength;
249                     maxLength = 0;
250                     break;
251                 }
252 
253             } else {
254                 out.write( buffer, 0, bytesRead );
255                 bytesWritten += bytesRead;
256             }
257         }
258 
259         return bytesWritten;
260     }
261 
getFileStream(AiffFileFormat aiffFileFormat, InputStream audioStream)262     private InputStream getFileStream(AiffFileFormat aiffFileFormat, InputStream audioStream) throws IOException  {
263 
264         // private method ... assumes aiffFileFormat is a supported file format
265 
266         AudioFormat format = aiffFileFormat.getFormat();
267         AudioFormat streamFormat = null;
268         AudioFormat.Encoding encoding = null;
269 
270         //$$fb a little bit nicer handling of constants
271         int headerSize          = aiffFileFormat.getHeaderSize();
272         //int fverChunkSize       = 0;
273         int fverChunkSize       = aiffFileFormat.getFverChunkSize();
274         int commChunkSize       = aiffFileFormat.getCommChunkSize();
275         int aiffLength          = -1;
276         int ssndChunkSize       = -1;
277         int ssndOffset                  = aiffFileFormat.getSsndChunkOffset();
278         short channels = (short) format.getChannels();
279         short sampleSize = (short) format.getSampleSizeInBits();
280         int ssndBlockSize = channels * ((sampleSize + 7) / 8);
281         int numFrames = aiffFileFormat.getFrameLength();
282         long dataSize = -1;
283         if( numFrames != AudioSystem.NOT_SPECIFIED) {
284             dataSize = (long) numFrames * ssndBlockSize;
285             ssndChunkSize = (int)dataSize + 16;
286             aiffLength = (int)dataSize+headerSize;
287         }
288         float sampleFramesPerSecond = format.getSampleRate();
289         int compCode = AiffFileFormat.AIFC_PCM;
290 
291         byte[] header = null;
292         InputStream codedAudioStream = audioStream;
293 
294         // if we need to do any format conversion, do it here....
295 
296         if( audioStream instanceof AudioInputStream ) {
297 
298             streamFormat = ((AudioInputStream)audioStream).getFormat();
299             encoding = streamFormat.getEncoding();
300 
301 
302             // $$jb: Note that AIFF samples are ALWAYS signed
303             if( (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding)) ||
304                 ( (AudioFormat.Encoding.PCM_SIGNED.equals(encoding)) && !streamFormat.isBigEndian() ) ) {
305 
306                 // plug in the transcoder to convert to PCM_SIGNED. big endian
307                 codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat (
308                                                                                      AudioFormat.Encoding.PCM_SIGNED,
309                                                                                      streamFormat.getSampleRate(),
310                                                                                      streamFormat.getSampleSizeInBits(),
311                                                                                      streamFormat.getChannels(),
312                                                                                      streamFormat.getFrameSize(),
313                                                                                      streamFormat.getFrameRate(),
314                                                                                      true ),
315                                                                     (AudioInputStream)audioStream );
316 
317             } else if( (AudioFormat.Encoding.ULAW.equals(encoding)) ||
318                        (AudioFormat.Encoding.ALAW.equals(encoding)) ) {
319 
320                 if( streamFormat.getSampleSizeInBits() != 8 ) {
321                     throw new IllegalArgumentException("unsupported encoding");
322                 }
323 
324                                 //$$fb 2001-07-13: this is probably not what we want:
325                                 //     writing PCM when ULAW/ALAW is requested. AIFC is able to write ULAW !
326 
327                                 // plug in the transcoder to convert to PCM_SIGNED_BIG_ENDIAN
328                 codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat (
329                                                                                      AudioFormat.Encoding.PCM_SIGNED,
330                                                                                      streamFormat.getSampleRate(),
331                                                                                      streamFormat.getSampleSizeInBits() * 2,
332                                                                                      streamFormat.getChannels(),
333                                                                                      streamFormat.getFrameSize() * 2,
334                                                                                      streamFormat.getFrameRate(),
335                                                                                      true ),
336                                                                     (AudioInputStream)audioStream );
337             }
338         }
339 
340 
341         // Now create an AIFF stream header...
342         try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
343              final DataOutputStream dos = new DataOutputStream(baos)) {
344             // Write the outer FORM chunk
345             dos.writeInt(AiffFileFormat.AIFF_MAGIC);
346             dos.writeInt((aiffLength - 8));
347             dos.writeInt(AiffFileFormat.AIFF_MAGIC2);
348             // Write a FVER chunk - only for AIFC
349             //dos.writeInt(FVER_MAGIC);
350             //dos.writeInt( (fverChunkSize-8) );
351             //dos.writeInt(FVER_TIMESTAMP);
352             // Write a COMM chunk
353             dos.writeInt(AiffFileFormat.COMM_MAGIC);
354             dos.writeInt((commChunkSize - 8));
355             dos.writeShort(channels);
356             dos.writeInt(numFrames);
357             dos.writeShort(sampleSize);
358             write_ieee_extended(dos, sampleFramesPerSecond);   // 10 bytes
359             //Only for AIFC
360             //dos.writeInt(compCode);
361             //dos.writeInt(compCode);
362             //dos.writeShort(0);
363             // Write the SSND chunk header
364             dos.writeInt(AiffFileFormat.SSND_MAGIC);
365             dos.writeInt((ssndChunkSize - 8));
366             // ssndOffset and ssndBlockSize set to 0 upon
367             // recommendation in "Sound Manager" chapter in
368             // "Inside Macintosh Sound", pp 2-87  (from Babu)
369             dos.writeInt(0);        // ssndOffset
370             dos.writeInt(0);        // ssndBlockSize
371             header = baos.toByteArray();
372         }
373         return new SequenceInputStream(new ByteArrayInputStream(header),
374                                        new NoCloseInputStream(codedAudioStream));
375     }
376 
377     // HELPER METHODS
378 
379     private static final int DOUBLE_MANTISSA_LENGTH = 52;
380     private static final int DOUBLE_EXPONENT_LENGTH = 11;
381     private static final long DOUBLE_SIGN_MASK     = 0x8000000000000000L;
382     private static final long DOUBLE_EXPONENT_MASK = 0x7FF0000000000000L;
383     private static final long DOUBLE_MANTISSA_MASK = 0x000FFFFFFFFFFFFFL;
384     private static final int DOUBLE_EXPONENT_OFFSET = 1023;
385 
386     private static final int EXTENDED_EXPONENT_OFFSET = 16383;
387     private static final int EXTENDED_MANTISSA_LENGTH = 63;
388     private static final int EXTENDED_EXPONENT_LENGTH = 15;
389     private static final long EXTENDED_INTEGER_MASK = 0x8000000000000000L;
390 
391     /**
392      * Extended precision IEEE floating-point conversion routine.
393      * @argument DataOutputStream
394      * @argument double
395      * @exception IOException
396      */
write_ieee_extended(DataOutputStream dos, float f)397     private void write_ieee_extended(DataOutputStream dos, float f) throws IOException {
398         /* The special cases NaN, Infinity and Zero are ignored, since
399            they do not represent useful sample rates anyway.
400            Denormalized number aren't handled, too. Below, there is a cast
401            from float to double. We hope that in this conversion,
402            numbers are normalized. Numbers that cannot be normalized are
403            ignored, too, as they, too, do not represent useful sample rates. */
404         long doubleBits = Double.doubleToLongBits((double) f);
405 
406         long sign = (doubleBits & DOUBLE_SIGN_MASK)
407             >> (DOUBLE_EXPONENT_LENGTH + DOUBLE_MANTISSA_LENGTH);
408         long doubleExponent = (doubleBits & DOUBLE_EXPONENT_MASK)
409             >> DOUBLE_MANTISSA_LENGTH;
410         long doubleMantissa = doubleBits & DOUBLE_MANTISSA_MASK;
411 
412         long extendedExponent = doubleExponent - DOUBLE_EXPONENT_OFFSET
413             + EXTENDED_EXPONENT_OFFSET;
414         long extendedMantissa = doubleMantissa
415             << (EXTENDED_MANTISSA_LENGTH - DOUBLE_MANTISSA_LENGTH);
416         long extendedSign = sign << EXTENDED_EXPONENT_LENGTH;
417         short extendedBits79To64 = (short) (extendedSign | extendedExponent);
418         long extendedBits63To0 = EXTENDED_INTEGER_MASK | extendedMantissa;
419 
420         dos.writeShort(extendedBits79To64);
421         dos.writeLong(extendedBits63To0);
422     }
423 }
424