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}