PipeDecoder.java (12033B)
1 /* 2 * _______ _____ _____ _____ 3 * |__ __| | __ \ / ____| __ \ 4 * | | __ _ _ __ ___ ___ ___| | | | (___ | |__) | 5 * | |/ _` | '__/ __|/ _ \/ __| | | |\___ \| ___/ 6 * | | (_| | | \__ \ (_) \__ \ |__| |____) | | 7 * |_|\__,_|_| |___/\___/|___/_____/|_____/|_| 8 * 9 * ------------------------------------------------------------- 10 * 11 * TarsosDSP is developed by Joren Six at IPEM, University Ghent 12 * 13 * ------------------------------------------------------------- 14 * 15 * Info: http://0110.be/tag/TarsosDSP 16 * Github: https://github.com/JorenSix/TarsosDSP 17 * Releases: http://0110.be/releases/TarsosDSP/ 18 * 19 * TarsosDSP includes modified source code by various authors, 20 * for credits and info, see README. 21 * 22 */ 23 24 package be.tarsos.dsp.io; 25 26 import java.io.BufferedInputStream; 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.nio.ByteOrder; 33 import java.util.logging.Logger; 34 import java.util.regex.Matcher; 35 import java.util.regex.Pattern; 36 37 import be.tarsos.dsp.util.FFMPEGDownloader; 38 39 /** 40 * <p> 41 * Decode audio files to PCM, mono, 16bits per sample, at any sample rate using 42 * an external program. By default ffmpeg is used. Other 43 * command Line programs that are able to decode audio and pipe binary PCM 44 * samples to STDOUT are possible as well (avconv, mplayer). 45 * To install ffmpeg on Debian: <code>apt-get install ffmpeg</code>. 46 * </p> 47 * <p> 48 * This adds support for a lot of audio formats and video container formats with 49 * relatively little effort. Depending on the program used also http streams, 50 * rtpm streams, ... are supported as well. 51 * </p> 52 * <p> 53 * To see which audio decoders are supported, check 54 * </p> 55 * <code><pre>ffmpeg -decoders | grep -E "^A" | sort 56 avconv version 9.8, Copyright (c) 2000-2013 the Libav developers 57 built on Aug 26 2013 09:52:20 with gcc 4.4.3 (Ubuntu 4.4.3-4ubuntu5.1) 58 A... 8svx_exp 8SVX exponential 59 A... 8svx_fib 8SVX fibonacci 60 A... aac AAC (Advanced Audio Coding) 61 A... aac_latm AAC LATM (Advanced Audio Coding LATM syntax) 62 A... ac3 ATSC A/52A (AC-3) 63 A... adpcm_4xm ADPCM 4X Movie 64 ... 65 </pre></code> 66 * 67 * @author Joren Six 68 */ 69 public class PipeDecoder { 70 71 private final static Logger LOG = Logger.getLogger(PipeDecoder.class.getName()); 72 private final String pipeEnvironment; 73 private final String pipeArgument; 74 private final String pipeCommand; 75 private final int pipeBuffer; 76 77 private boolean printErrorstream = false; 78 79 private String decoderBinaryAbsolutePath; 80 81 public PipeDecoder(){ 82 pipeBuffer = 10000; 83 84 //Use sensible defaults depending on the platform 85 if(System.getProperty("os.name").indexOf("indows") > 0 ){ 86 pipeEnvironment = "cmd.exe"; 87 pipeArgument = "/C"; 88 }else if(new File("/bin/bash").exists()){ 89 pipeEnvironment = "/bin/bash"; 90 pipeArgument = "-c"; 91 }else if (new File("/system/bin/sh").exists()){ 92 //probably we are on android here 93 pipeEnvironment = "/system/bin/sh"; 94 pipeArgument = "-c"; 95 }else{ 96 LOG.severe("Coud not find a command line environment (cmd.exe or /bin/bash)"); 97 throw new Error("Decoding via a pipe will not work: Coud not find a command line environment (cmd.exe or /bin/bash)"); 98 } 99 100 String path = System.getenv("PATH"); 101 String arguments = " -ss %input_seeking% %number_of_seconds% -i \"%resource%\" -vn -ar %sample_rate% -ac %channels% -sample_fmt s16 -f s16le pipe:1"; 102 if(isAvailable("ffmpeg")){ 103 LOG.info("found ffmpeg on the path (" + path + "). Will use ffmpeg for decoding media files."); 104 pipeCommand = "ffmpeg" + arguments; 105 }else if (isAvailable("avconv")){ 106 LOG.info("found avconv on your path(" + path + "). Will use avconv for decoding media files."); 107 pipeCommand = "avconv" + arguments; 108 }else { 109 if(isAndroid()) { 110 String tempDirectory = System.getProperty("java.io.tmpdir"); 111 printErrorstream=true; 112 File f = new File(tempDirectory, "ffmpeg"); 113 if (f.exists() && f.length() > 1000000 && f.canExecute()) { 114 decoderBinaryAbsolutePath = f.getAbsolutePath(); 115 } else { 116 LOG.severe("Could not find an ffmpeg binary for your Android system. Did you forget calling: 'new AndroidFFMPEGLocator(this);' ?"); 117 LOG.severe("Tried to unpack a statically compiled ffmpeg binary for your architecture to: " + f.getAbsolutePath()); 118 } 119 }else{ 120 LOG.warning("Dit not find ffmpeg or avconv on your path(" + path + "), will try to download it automatically."); 121 FFMPEGDownloader downloader = new FFMPEGDownloader(); 122 decoderBinaryAbsolutePath = downloader.ffmpegBinary(); 123 if(decoderBinaryAbsolutePath==null){ 124 LOG.severe("Could not download an ffmpeg binary automatically for your system."); 125 } 126 } 127 if(decoderBinaryAbsolutePath == null){ 128 pipeCommand = "false"; 129 throw new Error("Decoding via a pipe will not work: Could not find an ffmpeg binary for your system"); 130 }else{ 131 pipeCommand = '"' + decoderBinaryAbsolutePath + '"' + arguments; 132 } 133 } 134 } 135 136 private boolean isAvailable(String command){ 137 try{ 138 Runtime.getRuntime().exec(command + " -version"); 139 return true; 140 }catch (Exception e){ 141 return false; 142 } 143 } 144 145 public PipeDecoder(String pipeEnvironment,String pipeArgument,String pipeCommand,String pipeLogFile,int pipeBuffer){ 146 this.pipeEnvironment = pipeEnvironment; 147 this.pipeArgument = pipeArgument; 148 this.pipeCommand = pipeCommand; 149 this.pipeBuffer = pipeBuffer; 150 } 151 152 153 public InputStream getDecodedStream(final String resource,final int targetSampleRate,final double timeOffset, double numberOfSeconds) { 154 155 try { 156 String command = pipeCommand; 157 command = command.replace("%input_seeking%",String.valueOf(timeOffset)); 158 //defines the number of seconds to process 159 // -t 10.000 e.g. specifies to process ten seconds 160 // from the specified time offset (which is often zero). 161 if(numberOfSeconds>0){ 162 command = command.replace("%number_of_seconds%","-t " + String.valueOf(numberOfSeconds)); 163 } else { 164 command = command.replace("%number_of_seconds%",""); 165 } 166 command = command.replace("%resource%", resource); 167 command = command.replace("%sample_rate%", String.valueOf(targetSampleRate)); 168 command = command.replace("%channels%","1"); 169 170 ProcessBuilder pb; 171 pb= new ProcessBuilder(pipeEnvironment, pipeArgument , command); 172 173 LOG.info("Starting piped decoding process for " + resource); 174 LOG.info(" with command: " + command); 175 final Process process = pb.start(); 176 177 final InputStream stdOut = new BufferedInputStream(process.getInputStream(), pipeBuffer){ 178 @Override 179 public void close() throws IOException{ 180 super.close(); 181 // try to destroy the ffmpeg command after close 182 process.destroy(); 183 } 184 }; 185 186 //print std error if requested 187 if(printErrorstream) { 188 new ErrorStreamGobbler(process.getErrorStream(),LOG).start(); 189 } 190 191 new Thread(new Runnable(){ 192 @Override 193 public void run() { 194 try { 195 process.waitFor(); 196 LOG.info("Finished piped decoding process"); 197 } catch (InterruptedException e) { 198 LOG.severe("Interrupted while waiting for decoding sub process exit."); 199 e.printStackTrace(); 200 } 201 }},"Decoding Pipe").start(); 202 return stdOut; 203 } catch (IOException e) { 204 LOG.warning("IO exception while decoding audio via sub process." + e.getMessage() ); 205 e.printStackTrace(); 206 } 207 return null; 208 } 209 210 public double getDuration(final String resource) { 211 double duration = -1; 212 try { 213 //use " for windows compatibility! 214 String command = "ffmpeg -i \"%resource%\""; 215 216 command = command.replace("%resource%", resource); 217 218 ProcessBuilder pb; 219 pb= new ProcessBuilder(pipeEnvironment, pipeArgument , command); 220 221 LOG.info("Starting duration command for " + resource); 222 LOG.fine(" with command: " + command); 223 final Process process = pb.start(); 224 225 final InputStream stdOut = new BufferedInputStream(process.getInputStream(), pipeBuffer){ 226 @Override 227 public void close() throws IOException{ 228 super.close(); 229 // try to destroy the ffmpeg command after close 230 process.destroy(); 231 } 232 }; 233 234 ErrorStreamStringGlobber essg = new ErrorStreamStringGlobber(process.getErrorStream()); 235 essg.start(); 236 237 new Thread(new Runnable(){ 238 @Override 239 public void run() { 240 try { 241 process.waitFor(); 242 LOG.info("Finished piped decoding process"); 243 } catch (InterruptedException e) { 244 LOG.severe("Interrupted while waiting for decoding sub process exit."); 245 e.printStackTrace(); 246 } 247 }},"Decoding Pipe").run(); 248 249 String stdError = essg.getErrorStreamAsString(); 250 Pattern regex = Pattern.compile(".*\\s.*Duration:\\s+(\\d\\d):(\\d\\d):(\\d\\d)\\.(\\d\\d), .*", Pattern.DOTALL | Pattern.MULTILINE); 251 Matcher regexMatcher = regex.matcher(stdError); 252 if (regexMatcher.find()) { 253 duration = Integer.valueOf(regexMatcher.group(1)) * 3600+ 254 Integer.valueOf(regexMatcher.group(2)) * 60+ 255 Integer.valueOf(regexMatcher.group(3)) * 1 + 256 Double.valueOf("." + regexMatcher.group(4) ); 257 } 258 } catch (IOException e) { 259 LOG.warning("IO exception while decoding audio via sub process." + e.getMessage() ); 260 e.printStackTrace(); 261 } 262 return duration; 263 } 264 265 public void printBinaryInfo(){ 266 try { 267 Process p = Runtime.getRuntime().exec(decoderBinaryAbsolutePath); 268 BufferedReader input = new BufferedReader(new InputStreamReader(p.getErrorStream())); 269 String line = null; 270 while ((line = input.readLine()) != null) { 271 System.out.println(line); 272 } 273 input.close(); 274 //int exitVal = 275 p.waitFor(); 276 } catch (InterruptedException e) { 277 e.printStackTrace(); 278 } catch (IOException e) { 279 e.printStackTrace(); 280 } 281 } 282 283 /** 284 * Constructs the target audio format. The audio format is one channel 285 * signed PCM of a given sample rate. 286 * 287 * @param targetSampleRate 288 * The sample rate to convert to. 289 * @return The audio format after conversion. 290 */ 291 public static TarsosDSPAudioFormat getTargetAudioFormat(int targetSampleRate) { 292 TarsosDSPAudioFormat audioFormat = new TarsosDSPAudioFormat(TarsosDSPAudioFormat.Encoding.PCM_SIGNED, 293 targetSampleRate, 294 2 * 8, 295 1, 296 2 * 1, 297 targetSampleRate, 298 ByteOrder.BIG_ENDIAN.equals(ByteOrder.nativeOrder())); 299 return audioFormat; 300 } 301 302 303 private boolean isAndroid(){ 304 try { 305 // This class is only available on android 306 Class.forName("android.app.Activity"); 307 System.out.println("Running on Android!"); 308 return true; 309 } catch(ClassNotFoundException e) { 310 //the class is not found when running JVM 311 return false; 312 } 313 } 314 315 316 private class ErrorStreamGobbler extends Thread { 317 private final InputStream is; 318 private final Logger logger; 319 320 private ErrorStreamGobbler(InputStream is, Logger logger) { 321 this.is = is; 322 this.logger = logger; 323 } 324 325 @Override 326 public void run() { 327 try { 328 InputStreamReader isr = new InputStreamReader(is); 329 BufferedReader br = new BufferedReader(isr); 330 String line = null; 331 while ((line = br.readLine()) != null) { 332 logger.info(line); 333 } 334 } 335 catch (IOException ioe) { 336 ioe.printStackTrace(); 337 } 338 } 339 } 340 341 private class ErrorStreamStringGlobber extends Thread { 342 private final InputStream is; 343 private final StringBuilder sb; 344 345 private ErrorStreamStringGlobber(InputStream is) { 346 this.is = is; 347 this.sb = new StringBuilder(); 348 } 349 350 @Override 351 public void run() { 352 try { 353 InputStreamReader isr = new InputStreamReader(is); 354 BufferedReader br = new BufferedReader(isr); 355 String line = null; 356 while ((line = br.readLine()) != null) { 357 sb.append(line); 358 } 359 } 360 catch (IOException ioe) { 361 ioe.printStackTrace(); 362 } 363 } 364 365 public String getErrorStreamAsString(){ 366 return sb.toString(); 367 } 368 } 369 }