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 }