plectrum

Plectrum: instrument tuner for Android
Log | Files | Refs | README | LICENSE

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 }