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