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 } --- EOF ---