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