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