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 }