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 channels = aiffFileFormat.getFormat().getChannels();
 129             int sampleSize = aiffFileFormat.getFormat().getSampleSizeInBits();
 130             int ssndBlockSize = channels * ((sampleSize + 7) / 8);
 131 
 132             int aiffLength=bytesWritten;
 133             int ssndChunkSize=aiffLength-aiffFileFormat.getHeaderSize()+16;
 134             long dataSize=ssndChunkSize-16;
 135             //TODO possibly incorrect round
 136             int numFrames = (int) (dataSize / ssndBlockSize);
 137 
 138             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             raf.close();
 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      */
 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 
 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 
 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         ByteArrayInputStream headerStream = null;
 293         ByteArrayOutputStream baos = null;
 294         DataOutputStream dos = null;
 295         SequenceInputStream aiffStream = null;
 296         InputStream codedAudioStream = audioStream;
 297 
 298         // if we need to do any format conversion, do it here....
 299 
 300         if( audioStream instanceof AudioInputStream ) {
 301 
 302             streamFormat = ((AudioInputStream)audioStream).getFormat();
 303             encoding = streamFormat.getEncoding();
 304 
 305 
 306             // $$jb: Note that AIFF samples are ALWAYS signed
 307             if( (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding)) ||
 308                 ( (AudioFormat.Encoding.PCM_SIGNED.equals(encoding)) && !streamFormat.isBigEndian() ) ) {
 309 
 310                 // plug in the transcoder to convert to PCM_SIGNED. big endian
 311                 codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat (
 312                                                                                      AudioFormat.Encoding.PCM_SIGNED,
 313                                                                                      streamFormat.getSampleRate(),
 314                                                                                      streamFormat.getSampleSizeInBits(),
 315                                                                                      streamFormat.getChannels(),
 316                                                                                      streamFormat.getFrameSize(),
 317                                                                                      streamFormat.getFrameRate(),
 318                                                                                      true ),
 319                                                                     (AudioInputStream)audioStream );
 320 
 321             } else if( (AudioFormat.Encoding.ULAW.equals(encoding)) ||
 322                        (AudioFormat.Encoding.ALAW.equals(encoding)) ) {
 323 
 324                 if( streamFormat.getSampleSizeInBits() != 8 ) {
 325                     throw new IllegalArgumentException("unsupported encoding");
 326                 }
 327 
 328                                 //$$fb 2001-07-13: this is probably not what we want:
 329                                 //     writing PCM when ULAW/ALAW is requested. AIFC is able to write ULAW !
 330 
 331                                 // plug in the transcoder to convert to PCM_SIGNED_BIG_ENDIAN
 332                 codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat (
 333                                                                                      AudioFormat.Encoding.PCM_SIGNED,
 334                                                                                      streamFormat.getSampleRate(),
 335                                                                                      streamFormat.getSampleSizeInBits() * 2,
 336                                                                                      streamFormat.getChannels(),
 337                                                                                      streamFormat.getFrameSize() * 2,
 338                                                                                      streamFormat.getFrameRate(),
 339                                                                                      true ),
 340                                                                     (AudioInputStream)audioStream );
 341             }
 342         }
 343 
 344 
 345         // Now create an AIFF stream header...
 346         baos = new ByteArrayOutputStream();
 347         dos = new DataOutputStream(baos);
 348 
 349         // Write the outer FORM chunk
 350         dos.writeInt(AiffFileFormat.AIFF_MAGIC);
 351         dos.writeInt( (aiffLength-8) );
 352         dos.writeInt(AiffFileFormat.AIFF_MAGIC2);
 353 
 354         // Write a FVER chunk - only for AIFC
 355         //dos.writeInt(FVER_MAGIC);
 356         //dos.writeInt( (fverChunkSize-8) );
 357         //dos.writeInt(FVER_TIMESTAMP);
 358 
 359         // Write a COMM chunk
 360         dos.writeInt(AiffFileFormat.COMM_MAGIC);
 361         dos.writeInt( (commChunkSize-8) );
 362         dos.writeShort(channels);
 363         dos.writeInt(numFrames);
 364         dos.writeShort(sampleSize);
 365         write_ieee_extended(dos, sampleFramesPerSecond);   // 10 bytes
 366 
 367         //Only for AIFC
 368         //dos.writeInt(compCode);
 369         //dos.writeInt(compCode);
 370         //dos.writeShort(0);
 371 
 372         // Write the SSND chunk header
 373         dos.writeInt(AiffFileFormat.SSND_MAGIC);
 374         dos.writeInt( (ssndChunkSize-8) );
 375         // ssndOffset and ssndBlockSize set to 0 upon
 376         // recommendation in "Sound Manager" chapter in
 377         // "Inside Macintosh Sound", pp 2-87  (from Babu)
 378         dos.writeInt(0);        // ssndOffset
 379         dos.writeInt(0);        // ssndBlockSize
 380 
 381         // Concat this with the audioStream and return it
 382 
 383         dos.close();
 384         header = baos.toByteArray();
 385         headerStream = new ByteArrayInputStream( header );
 386 
 387         aiffStream = new SequenceInputStream(headerStream,
 388                             new NoCloseInputStream(codedAudioStream));
 389 
 390         return aiffStream;
 391 
 392     }
 393 
 394     // HELPER METHODS
 395 
 396     private static final int DOUBLE_MANTISSA_LENGTH = 52;
 397     private static final int DOUBLE_EXPONENT_LENGTH = 11;
 398     private static final long DOUBLE_SIGN_MASK     = 0x8000000000000000L;
 399     private static final long DOUBLE_EXPONENT_MASK = 0x7FF0000000000000L;
 400     private static final long DOUBLE_MANTISSA_MASK = 0x000FFFFFFFFFFFFFL;
 401     private static final int DOUBLE_EXPONENT_OFFSET = 1023;
 402 
 403     private static final int EXTENDED_EXPONENT_OFFSET = 16383;
 404     private static final int EXTENDED_MANTISSA_LENGTH = 63;
 405     private static final int EXTENDED_EXPONENT_LENGTH = 15;
 406     private static final long EXTENDED_INTEGER_MASK = 0x8000000000000000L;
 407 
 408     /**
 409      * Extended precision IEEE floating-point conversion routine.
 410      * @argument DataOutputStream
 411      * @argument double
 412      * @exception IOException
 413      */
 414     private void write_ieee_extended(DataOutputStream dos, float f) throws IOException {
 415         /* The special cases NaN, Infinity and Zero are ignored, since
 416            they do not represent useful sample rates anyway.
 417            Denormalized number aren't handled, too. Below, there is a cast
 418            from float to double. We hope that in this conversion,
 419            numbers are normalized. Numbers that cannot be normalized are
 420            ignored, too, as they, too, do not represent useful sample rates. */
 421         long doubleBits = Double.doubleToLongBits((double) f);
 422 
 423         long sign = (doubleBits & DOUBLE_SIGN_MASK)
 424             >> (DOUBLE_EXPONENT_LENGTH + DOUBLE_MANTISSA_LENGTH);
 425         long doubleExponent = (doubleBits & DOUBLE_EXPONENT_MASK)
 426             >> DOUBLE_MANTISSA_LENGTH;
 427         long doubleMantissa = doubleBits & DOUBLE_MANTISSA_MASK;
 428 
 429         long extendedExponent = doubleExponent - DOUBLE_EXPONENT_OFFSET
 430             + EXTENDED_EXPONENT_OFFSET;
 431         long extendedMantissa = doubleMantissa
 432             << (EXTENDED_MANTISSA_LENGTH - DOUBLE_MANTISSA_LENGTH);
 433         long extendedSign = sign << EXTENDED_EXPONENT_LENGTH;
 434         short extendedBits79To64 = (short) (extendedSign | extendedExponent);
 435         long extendedBits63To0 = EXTENDED_INTEGER_MASK | extendedMantissa;
 436 
 437         dos.writeShort(extendedBits79To64);
 438         dos.writeLong(extendedBits63To0);
 439     }
 440 }