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  * WAVE file writer.
  50  *
  51  * @author Jan Borgersen
  52  */
  53 public final class WaveFileWriter extends SunFileWriter {
  54 
  55     /**
  56      * Constructs a new WaveFileWriter object.
  57      */
  58     public WaveFileWriter() {
  59         super(new AudioFileFormat.Type[]{AudioFileFormat.Type.WAVE});
  60     }
  61 
  62     @Override
  63     public AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) {
  64 
  65         AudioFileFormat.Type[] filetypes = new AudioFileFormat.Type[types.length];
  66         System.arraycopy(types, 0, filetypes, 0, types.length);
  67 
  68         // make sure we can write this stream
  69         AudioFormat format = stream.getFormat();
  70         AudioFormat.Encoding encoding = format.getEncoding();
  71 
  72         if( AudioFormat.Encoding.ALAW.equals(encoding) ||
  73             AudioFormat.Encoding.ULAW.equals(encoding) ||
  74             AudioFormat.Encoding.PCM_SIGNED.equals(encoding) ||
  75             AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding) ) {
  76 
  77             return filetypes;
  78         }
  79 
  80         return new AudioFileFormat.Type[0];
  81     }
  82 
  83 
  84     @Override
  85     public int write(AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out) throws IOException {
  86         Objects.requireNonNull(stream);
  87         Objects.requireNonNull(fileType);
  88         Objects.requireNonNull(out);
  89 
  90         //$$fb the following check must come first ! Otherwise
  91         // the next frame length check may throw an IOException and
  92         // interrupt iterating File Writers. (see bug 4351296)
  93 
  94         // throws IllegalArgumentException if not supported
  95         WaveFileFormat waveFileFormat = (WaveFileFormat)getAudioFileFormat(fileType, stream);
  96 
  97         //$$fb when we got this far, we are committed to write this file
  98 
  99         // we must know the total data length to calculate the file length
 100         if( stream.getFrameLength() == AudioSystem.NOT_SPECIFIED ) {
 101             throw new IOException("stream length not specified");
 102         }
 103 
 104         return writeWaveFile(stream, waveFileFormat, out);
 105     }
 106 
 107 
 108     @Override
 109     public int write(AudioInputStream stream, AudioFileFormat.Type fileType, File out) throws IOException {
 110         Objects.requireNonNull(stream);
 111         Objects.requireNonNull(fileType);
 112         Objects.requireNonNull(out);
 113 
 114         // throws IllegalArgumentException if not supported
 115         WaveFileFormat waveFileFormat = (WaveFileFormat)getAudioFileFormat(fileType, stream);
 116 
 117         // first write the file without worrying about length fields
 118         FileOutputStream fos = new FileOutputStream( out );     // throws IOException
 119         BufferedOutputStream bos = new BufferedOutputStream( fos, bisBufferSize );
 120         int bytesWritten = writeWaveFile(stream, waveFileFormat, bos );
 121         bos.close();
 122 
 123         // now, if length fields were not specified, calculate them,
 124         // open as a random access file, write the appropriate fields,
 125         // close again....
 126         if( waveFileFormat.getByteLength()== AudioSystem.NOT_SPECIFIED ) {
 127 
 128             int dataLength=bytesWritten-waveFileFormat.getHeaderSize();
 129             int riffLength=dataLength + waveFileFormat.getHeaderSize() - 8;
 130 
 131             RandomAccessFile raf=new RandomAccessFile(out, "rw");
 132             // skip RIFF magic
 133             raf.skipBytes(4);
 134             raf.writeInt(big2little( riffLength ));
 135             // skip WAVE magic, fmt_ magic, fmt_ length, fmt_ chunk, data magic
 136             raf.skipBytes(4+4+4+WaveFileFormat.getFmtChunkSize(waveFileFormat.getWaveType())+4);
 137             raf.writeInt(big2little( dataLength ));
 138             // that's all
 139             raf.close();
 140         }
 141 
 142         return bytesWritten;
 143     }
 144 
 145     //--------------------------------------------------------------------
 146 
 147     /**
 148      * Returns the AudioFileFormat describing the file that will be written from this AudioInputStream.
 149      * Throws IllegalArgumentException if not supported.
 150      */
 151     private AudioFileFormat getAudioFileFormat(AudioFileFormat.Type type, AudioInputStream stream) {
 152         if (!isFileTypeSupported(type, stream)) {
 153             throw new IllegalArgumentException("File type " + type + " not supported.");
 154         }
 155         AudioFormat format = null;
 156         WaveFileFormat fileFormat = null;
 157         AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED;
 158 
 159         AudioFormat streamFormat = stream.getFormat();
 160         AudioFormat.Encoding streamEncoding = streamFormat.getEncoding();
 161 
 162         float sampleRate;
 163         int sampleSizeInBits;
 164         int channels;
 165         int frameSize;
 166         float frameRate;
 167         int fileSize;
 168 
 169         int waveType = WaveFileFormat.WAVE_FORMAT_PCM;
 170 
 171         if( AudioFormat.Encoding.ALAW.equals(streamEncoding) ||
 172             AudioFormat.Encoding.ULAW.equals(streamEncoding) ) {
 173 
 174             encoding = streamEncoding;
 175             sampleSizeInBits = streamFormat.getSampleSizeInBits();
 176             if (streamEncoding.equals(AudioFormat.Encoding.ALAW)) {
 177                 waveType = WaveFileFormat.WAVE_FORMAT_ALAW;
 178             } else {
 179                 waveType = WaveFileFormat.WAVE_FORMAT_MULAW;
 180             }
 181         } else if ( streamFormat.getSampleSizeInBits()==8 ) {
 182             encoding = AudioFormat.Encoding.PCM_UNSIGNED;
 183             sampleSizeInBits=8;
 184         } else {
 185             encoding = AudioFormat.Encoding.PCM_SIGNED;
 186             sampleSizeInBits=streamFormat.getSampleSizeInBits();
 187         }
 188 
 189 
 190         format = new AudioFormat( encoding,
 191                                   streamFormat.getSampleRate(),
 192                                   sampleSizeInBits,
 193                                   streamFormat.getChannels(),
 194                                   streamFormat.getFrameSize(),
 195                                   streamFormat.getFrameRate(),
 196                                   false);       // WAVE is little endian
 197 
 198         if( stream.getFrameLength()!=AudioSystem.NOT_SPECIFIED ) {
 199             fileSize = (int)stream.getFrameLength()*streamFormat.getFrameSize()
 200                 + WaveFileFormat.getHeaderSize(waveType);
 201         } else {
 202             fileSize = AudioSystem.NOT_SPECIFIED;
 203         }
 204 
 205         fileFormat = new WaveFileFormat( AudioFileFormat.Type.WAVE,
 206                                          fileSize,
 207                                          format,
 208                                          (int)stream.getFrameLength() );
 209 
 210         return fileFormat;
 211     }
 212 
 213 
 214     private int writeWaveFile(InputStream in, WaveFileFormat waveFileFormat, OutputStream out) throws IOException {
 215 
 216         int bytesRead = 0;
 217         int bytesWritten = 0;
 218         InputStream fileStream = getFileStream(waveFileFormat, in);
 219         byte buffer[] = new byte[bisBufferSize];
 220         int maxLength = waveFileFormat.getByteLength();
 221 
 222         while( (bytesRead = fileStream.read( buffer )) >= 0 ) {
 223 
 224             if (maxLength>0) {
 225                 if( bytesRead < maxLength ) {
 226                     out.write( buffer, 0, bytesRead );
 227                     bytesWritten += bytesRead;
 228                     maxLength -= bytesRead;
 229                 } else {
 230                     out.write( buffer, 0, maxLength );
 231                     bytesWritten += maxLength;
 232                     maxLength = 0;
 233                     break;
 234                 }
 235             } else {
 236                 out.write( buffer, 0, bytesRead );
 237                 bytesWritten += bytesRead;
 238             }
 239         }
 240 
 241         return bytesWritten;
 242     }
 243 
 244     private InputStream getFileStream(WaveFileFormat waveFileFormat, InputStream audioStream) throws IOException {
 245         // private method ... assumes audioFileFormat is a supported file type
 246 
 247         // WAVE header fields
 248         AudioFormat audioFormat = waveFileFormat.getFormat();
 249         int headerLength       = waveFileFormat.getHeaderSize();
 250         int riffMagic          = WaveFileFormat.RIFF_MAGIC;
 251         int waveMagic          = WaveFileFormat.WAVE_MAGIC;
 252         int fmtMagic           = WaveFileFormat.FMT_MAGIC;
 253         int fmtLength          = WaveFileFormat.getFmtChunkSize(waveFileFormat.getWaveType());
 254         short wav_type         = (short) waveFileFormat.getWaveType();
 255         short channels         = (short) audioFormat.getChannels();
 256         short sampleSizeInBits = (short) audioFormat.getSampleSizeInBits();
 257         int sampleRate         = (int) audioFormat.getSampleRate();
 258         int frameSizeInBytes   = audioFormat.getFrameSize();
 259         int frameRate              = (int) audioFormat.getFrameRate();
 260         int avgBytesPerSec     = channels * sampleSizeInBits * sampleRate / 8;;
 261         short blockAlign       = (short) ((sampleSizeInBits / 8) * channels);
 262         int dataMagic              = WaveFileFormat.DATA_MAGIC;
 263         int dataLength             = waveFileFormat.getFrameLength() * frameSizeInBytes;
 264         int length                         = waveFileFormat.getByteLength();
 265         int riffLength = dataLength + headerLength - 8;
 266 
 267         byte header[] = null;
 268         ByteArrayInputStream headerStream = null;
 269         ByteArrayOutputStream baos = null;
 270         DataOutputStream dos = null;
 271         SequenceInputStream waveStream = null;
 272 
 273         AudioFormat audioStreamFormat = null;
 274         AudioFormat.Encoding encoding = null;
 275         InputStream codedAudioStream = audioStream;
 276 
 277         // if audioStream is an AudioInputStream and we need to convert, do it here...
 278         if(audioStream instanceof AudioInputStream) {
 279             audioStreamFormat = ((AudioInputStream)audioStream).getFormat();
 280 
 281             encoding = audioStreamFormat.getEncoding();
 282 
 283             if(AudioFormat.Encoding.PCM_SIGNED.equals(encoding)) {
 284                 if( sampleSizeInBits==8 ) {
 285                     wav_type = WaveFileFormat.WAVE_FORMAT_PCM;
 286                     // plug in the transcoder to convert from PCM_SIGNED to PCM_UNSIGNED
 287                     codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat(
 288                                                                                         AudioFormat.Encoding.PCM_UNSIGNED,
 289                                                                                         audioStreamFormat.getSampleRate(),
 290                                                                                         audioStreamFormat.getSampleSizeInBits(),
 291                                                                                         audioStreamFormat.getChannels(),
 292                                                                                         audioStreamFormat.getFrameSize(),
 293                                                                                         audioStreamFormat.getFrameRate(),
 294                                                                                         false),
 295                                                                         (AudioInputStream)audioStream);
 296                 }
 297             }
 298             if( (AudioFormat.Encoding.PCM_SIGNED.equals(encoding) && audioStreamFormat.isBigEndian()) ||
 299                 (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding) && !audioStreamFormat.isBigEndian()) ||
 300                 (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding) && audioStreamFormat.isBigEndian()) ) {
 301                 if( sampleSizeInBits!=8) {
 302                     wav_type = WaveFileFormat.WAVE_FORMAT_PCM;
 303                     // plug in the transcoder to convert to PCM_SIGNED_LITTLE_ENDIAN
 304                     codedAudioStream = AudioSystem.getAudioInputStream( new AudioFormat(
 305                                                                                         AudioFormat.Encoding.PCM_SIGNED,
 306                                                                                         audioStreamFormat.getSampleRate(),
 307                                                                                         audioStreamFormat.getSampleSizeInBits(),
 308                                                                                         audioStreamFormat.getChannels(),
 309                                                                                         audioStreamFormat.getFrameSize(),
 310                                                                                         audioStreamFormat.getFrameRate(),
 311                                                                                         false),
 312                                                                         (AudioInputStream)audioStream);
 313                 }
 314             }
 315         }
 316 
 317 
 318         // Now push the header into a stream, concat, and return the new SequenceInputStream
 319 
 320         baos = new ByteArrayOutputStream();
 321         dos = new DataOutputStream(baos);
 322 
 323         // we write in littleendian...
 324         dos.writeInt(riffMagic);
 325         dos.writeInt(big2little( riffLength ));
 326         dos.writeInt(waveMagic);
 327         dos.writeInt(fmtMagic);
 328         dos.writeInt(big2little(fmtLength));
 329         dos.writeShort(big2littleShort(wav_type));
 330         dos.writeShort(big2littleShort(channels));
 331         dos.writeInt(big2little(sampleRate));
 332         dos.writeInt(big2little(avgBytesPerSec));
 333         dos.writeShort(big2littleShort(blockAlign));
 334         dos.writeShort(big2littleShort(sampleSizeInBits));
 335         //$$fb 2002-04-16: Fix for 4636355: RIFF audio headers could be _more_ spec compliant
 336         if (wav_type != WaveFileFormat.WAVE_FORMAT_PCM) {
 337             // add length 0 for "codec specific data length"
 338             dos.writeShort(0);
 339         }
 340 
 341         dos.writeInt(dataMagic);
 342         dos.writeInt(big2little(dataLength));
 343 
 344         dos.close();
 345         header = baos.toByteArray();
 346         headerStream = new ByteArrayInputStream( header );
 347         waveStream = new SequenceInputStream(headerStream,
 348                             new NoCloseInputStream(codedAudioStream));
 349 
 350         return (InputStream)waveStream;
 351     }
 352 }