1 /*
   2  * Copyright (c) 1999, 2017, 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         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      */
 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         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      */
 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 }