1 /* 2 * Copyright (c) 1999, 2018, 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 */ AiffFileWriter()58 public AiffFileWriter() { 59 super(new AudioFileFormat.Type[]{AudioFileFormat.Type.AIFF}); 60 } 61 62 // METHODS TO IMPLEMENT AudioFileWriter 63 64 @Override getAudioFileTypes(AudioInputStream stream)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 write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out)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 write(AudioInputStream stream, AudioFileFormat.Type fileType, File out)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 */ getAudioFileFormat(AudioFileFormat.Type type, AudioInputStream stream)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 writeAiffFile(InputStream in, AiffFileFormat aiffFileFormat, OutputStream out)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 getFileStream(AiffFileFormat aiffFileFormat, InputStream audioStream)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 */ write_ieee_extended(DataOutputStream dos, float f)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 } 424