001/*******************************************************************************
002 * Copyright (c) 2024, 2026, Olivier Ayache.  All rights reserved.
003 *
004 * This file is part of AVPKit.
005 *
006 * AVPKit is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Lesser General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * AVPKit is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with AVPKit.  If not, see <http://www.gnu.org/licenses/>.
018 *******************************************************************************/
019
020package com.avpkit.core.demos;
021
022import javax.sound.sampled.AudioFormat;
023import javax.sound.sampled.AudioSystem;
024import javax.sound.sampled.DataLine;
025import javax.sound.sampled.LineUnavailableException;
026import javax.sound.sampled.SourceDataLine;
027
028import com.avpkit.core.IAudioSamples;
029import com.avpkit.core.IContainer;
030import com.avpkit.core.IPacket;
031import com.avpkit.core.IStream;
032import com.avpkit.core.IStreamCoder;
033import com.avpkit.core.ICodec;
034
035/**
036 * Takes a media container, finds the first audio stream,
037 * decodes that stream, and then plays
038 * it on the default system device.
039 * @author aclarke
040 *
041 */
042public class DecodeAndPlayAudio
043{
044
045  /**
046   * The audio line we'll output sound to; it'll be the default audio device on your system if available
047   */
048  private static SourceDataLine mLine;
049
050  /**
051   * Takes a media container (file) as the first argument, opens it,
052   * opens up the default audio device on your system, and plays back the audio.
053   *  
054   * @param args Must contain one string which represents a filename
055   */
056  public static void main(String[] args)
057  {
058    if (args.length <= 0)
059      throw new IllegalArgumentException("must pass in a filename as the first argument");
060    
061    String filename = args[0];
062    
063    // Create a AVPKit container object
064    IContainer container = IContainer.make();
065    
066    // Open up the container
067    if (container.open(filename, IContainer.Type.READ, null) < 0)
068      throw new IllegalArgumentException("could not open file: " + filename);
069    
070    // query how many streams the call to open found
071    int numStreams = container.getNumStreams();
072    
073    // and iterate through the streams to find the first audio stream
074    int audioStreamId = -1;
075    IStreamCoder audioCoder = null;
076    for(int i = 0; i < numStreams; i++)
077    {
078      // Find the stream object
079      IStream stream = container.getStream(i);
080      // Get the pre-configured decoder that can decode this stream;
081      IStreamCoder coder = stream.getStreamCoder();
082      
083      if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO)
084      {
085        audioStreamId = i;
086        audioCoder = coder;
087        break;
088      }
089    }
090    if (audioStreamId == -1)
091      throw new RuntimeException("could not find audio stream in container: "+filename);
092    
093    /*
094     * Now we have found the audio stream in this file.  Let's open up our decoder so it can
095     * do work.
096     */
097    if (audioCoder.open(null, null) < 0)
098      throw new RuntimeException("could not open audio decoder for container: "+filename);
099    
100    /*
101     * And once we have that, we ask the Java Sound System to get itself ready.
102     */
103    openJavaSound(audioCoder);
104    
105    /*
106     * Now, we start walking through the container looking at each packet.
107     */
108    IPacket packet = IPacket.make();
109    while(container.readNextPacket(packet) >= 0)
110    {
111      /*
112       * Now we have a packet, let's see if it belongs to our audio stream
113       */
114      if (packet.getStreamIndex() == audioStreamId)
115      {
116        /*
117         * We allocate a set of samples with the same number of channels as the
118         * coder tells us is in this buffer.
119         * 
120         * We also pass in a buffer size (1024 in our example), although AVPKit
121         * will probably allocate more space than just the 1024 (it's not important why).
122         */
123        IAudioSamples samples = IAudioSamples.make(1024, audioCoder.getChannels());
124        
125        /*
126         * A packet can actually contain multiple sets of samples (or frames of samples
127         * in audio-decoding speak).  So, we may need to call decode audio multiple
128         * times at different offsets in the packet's data.  We capture that here.
129         */
130        int offset = 0;
131        
132        /*
133         * Keep going until we've processed all data
134         */
135        while(offset < packet.getSize())
136        {
137          int bytesDecoded = audioCoder.decodeAudio(samples, packet, offset);
138          if (bytesDecoded < 0)
139            throw new RuntimeException("got error decoding audio in: " + filename);
140          offset += bytesDecoded;
141          /*
142           * Some decoder will consume data in a packet, but will not be able to construct
143           * a full set of samples yet.  Therefore you should always check if you
144           * got a complete set of samples from the decoder
145           */
146          if (samples.isComplete())
147          {
148            playJavaSound(samples);
149          }
150        }
151      }
152      else
153      {
154        /*
155         * This packet isn't part of our audio stream, so we just silently drop it.
156         */
157        do {} while(false);
158      }
159      
160    }
161    /*
162     * Technically since we're exiting anyway, these will be cleaned up by 
163     * the garbage collector... but because we're nice people and want
164     * to be invited places for Christmas, we're going to show how to clean up.
165     */
166    closeJavaSound();
167    
168    if (audioCoder != null)
169    {
170      audioCoder.close();
171      audioCoder = null;
172    }
173    if (container !=null)
174    {
175      container.close();
176      container = null;
177    }
178  }
179
180  private static void openJavaSound(IStreamCoder aAudioCoder)
181  {
182    AudioFormat audioFormat = new AudioFormat(aAudioCoder.getSampleRate(),
183        (int)IAudioSamples.findSampleBitDepth(aAudioCoder.getSampleFormat()),
184        aAudioCoder.getChannels(),
185        true, /* core defaults to signed 16 bit samples */
186        false);
187    DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
188    try
189    {
190      mLine = (SourceDataLine) AudioSystem.getLine(info);
191      /**
192       * if that succeeded, try opening the line.
193       */
194      mLine.open(audioFormat);
195      /**
196       * And if that succeed, start the line.
197       */
198      mLine.start();
199    }
200    catch (LineUnavailableException e)
201    {
202      throw new RuntimeException("could not open audio line");
203    }
204    
205    
206  }
207
208  private static void playJavaSound(IAudioSamples aSamples)
209  {
210    /**
211     * We're just going to dump all the samples into the line.
212     */
213    byte[] rawBytes = aSamples.getData().getByteArray(0, aSamples.getSize());
214    mLine.write(rawBytes, 0, aSamples.getSize());
215  }
216
217  private static void closeJavaSound()
218  {
219    if (mLine != null)
220    {
221      /*
222       * Wait for the line to finish playing
223       */
224      mLine.drain();
225      /*
226       * Close the line.
227       */
228      mLine.close();
229      mLine=null;
230    }
231  }
232}