001/* PulseAudioTargetDataLine.java 002 Copyright (C) 2008 Red Hat, Inc. 003 004This file is part of IcedTea-Sound. 005 006IcedTea-Sound is free software; you can redistribute it and/or 007modify it under the terms of the GNU General Public License as published by 008the Free Software Foundation, version 2. 009 010IcedTea-Sound is distributed in the hope that it will be useful, 011but WITHOUT ANY WARRANTY; without even the implied warranty of 012MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013General Public License for more details. 014 015You should have received a copy of the GNU General Public License 016along with IcedTea-Sound; see the file COPYING. If not, write to 017the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 01802110-1301 USA. 019 020Linking this library statically or dynamically with other modules is 021making a combined work based on this library. Thus, the terms and 022conditions of the GNU General Public License cover the whole 023combination. 024 025As a special exception, the copyright holders of this library give you 026permission to link this library with independent modules to produce an 027executable, regardless of the license terms of these independent 028modules, and to copy and distribute the resulting executable under 029terms of your choice, provided that you also meet, for each linked 030independent module, the terms and conditions of the license of that 031module. An independent module is a module which is not derived from 032or based on this library. If you modify this library, you may extend 033this exception to your version of the library, but you are not 034obligated to do so. If you do not wish to do so, delete this 035exception statement from your version. 036 */ 037 038package org.classpath.icedtea.pulseaudio; 039 040import javax.sound.sampled.AudioFormat; 041import javax.sound.sampled.AudioPermission; 042import javax.sound.sampled.DataLine; 043import javax.sound.sampled.Line; 044import javax.sound.sampled.LineEvent; 045import javax.sound.sampled.LineUnavailableException; 046import javax.sound.sampled.TargetDataLine; 047 048import org.classpath.icedtea.pulseaudio.Debug.DebugLevel; 049 050public final class PulseAudioTargetDataLine extends PulseAudioDataLine 051 implements TargetDataLine { 052 053 /* 054 * This contains the data from the PulseAudio buffer that has since been 055 * dropped. If 20 bytes of a fragment of size 200 are read, the other 180 056 * are dumped in this 057 */ 058 private byte[] fragmentBuffer; 059 060 /* 061 * these are set to true only by the respective functions (flush(), drain()) 062 * set to false only by read() 063 */ 064 private boolean flushed = false; 065 private boolean drained = false; 066 067 public static final String DEFAULT_TARGETDATALINE_NAME = "Audio Stream"; 068 069 PulseAudioTargetDataLine(AudioFormat[] formats, AudioFormat defaultFormat) { 070 this.supportedFormats = formats; 071 this.defaultFormat = defaultFormat; 072 this.currentFormat = defaultFormat; 073 this.streamName = DEFAULT_TARGETDATALINE_NAME; 074 075 } 076 077 @Override 078 synchronized public void close() { 079 if (!isOpen()) { 080 // Probably due to some programmer error, we are being 081 // asked to close an already closed line. Oh well. 082 Debug.println(DebugLevel.Verbose, 083 "PulseAudioTargetDataLine.close(): " 084 + "Line closed that wasn't open."); 085 return; 086 } 087 088 /* check for permission to record audio */ 089 AudioPermission perm = new AudioPermission("record", null); 090 perm.checkGuard(null); 091 092 PulseAudioMixer parentMixer = PulseAudioMixer.getInstance(); 093 parentMixer.removeTargetLine(this); 094 095 super.close(); 096 097 Debug.println(DebugLevel.Verbose, "PulseAudioTargetDataLine.close(): " 098 + "Line closed"); 099 } 100 101 @Override 102 synchronized public void open(AudioFormat format, int bufferSize) 103 throws LineUnavailableException { 104 /* check for permission to record audio */ 105 AudioPermission perm = new AudioPermission("record", null); 106 perm.checkGuard(null); 107 108 if (isOpen()) { 109 throw new IllegalStateException("already open"); 110 } 111 super.open(format, bufferSize); 112 113 /* initialize all the member variables */ 114 framesSinceOpen = 0; 115 fragmentBuffer = null; 116 flushed = false; 117 drained = false; 118 119 /* add this open line to the mixer */ 120 PulseAudioMixer parentMixer = PulseAudioMixer.getInstance(); 121 parentMixer.addTargetLine(this); 122 123 Debug.println(DebugLevel.Verbose, "PulseAudioTargetDataLine.open(): " 124 + "Line opened"); 125 } 126 127 @Override 128 synchronized public void open(AudioFormat format) 129 throws LineUnavailableException { 130 open(format, DEFAULT_BUFFER_SIZE); 131 } 132 133 @Override 134 protected void connectLine(int bufferSize, Stream masterStream) 135 throws LineUnavailableException { 136 int fs = currentFormat.getFrameSize(); 137 float fr = currentFormat.getFrameRate(); 138 int bps = (int)(fs*fr); // bytes per second. 139 140 // if 2 seconds' worth of data can fit in the buffer of the specified 141 // size, we don't have to adjust the latency. Otherwise we do, so as 142 // to avoid overruns. 143 long flags = Stream.FLAG_START_CORKED; 144 StreamBufferAttributes bufferAttributes; 145 if (bps*2 < bufferSize) { 146 // pulse audio completely ignores our fragmentSize attribute unless 147 // ADJUST_LATENCY is set, so we just leave it at -1. 148 bufferAttributes = new StreamBufferAttributes(bufferSize, -1, -1, -1, -1); 149 } else { 150 flags |= Stream.FLAG_ADJUST_LATENCY; 151 // in this case, the pulse audio docs: 152 // http://www.pulseaudio.org/wiki/LatencyControl 153 // say every field (including bufferSize) must be initialized 154 // to -1 except fragmentSize. 155 // XXX: but in my tests, it just sets it to about 4MB, which 156 // effectively makes it impossible to allocate a small buffer 157 // and nothing bad happens (yet) when you don't set it to -1 158 // so we just leave it at bufferSize. 159 // XXX: the java api has no way to specify latency, which probably 160 // means it should be as low as possible. Right now this method's 161 // primary concern is avoiding dropouts, and if the user-provided 162 // buffer size is large enough, we leave the latency up to pulse 163 // audio (which sets it to something extremely high - about 2 164 // seconds). We might want to always set a low latency. 165 int fragmentSize = bufferSize/2; 166 fragmentSize = Math.max((fragmentSize/fs)*fs, fs); 167 bufferAttributes = new StreamBufferAttributes(bufferSize, -1, -1, -1, fragmentSize); 168 } 169 170 synchronized (eventLoop.threadLock) { 171 stream.connectForRecording(Stream.DEFAULT_DEVICE, flags, bufferAttributes); 172 } 173 } 174 175 @Override 176 public int read(byte[] data, int offset, int length) { 177 178 /* check state and inputs */ 179 180 if (!isOpen()) { 181 // A closed line can produce zero bytes of data. 182 return 0; 183 } 184 185 int frameSize = currentFormat.getFrameSize(); 186 187 if (length % frameSize != 0) { 188 throw new IllegalArgumentException( 189 "amount of data to read does not represent an integral number of frames"); 190 } 191 192 if (length < 0) { 193 throw new IllegalArgumentException("length is negative"); 194 } 195 196 if ( offset < 0 || offset > data.length - length) { 197 throw new ArrayIndexOutOfBoundsException("array size: " + data.length 198 + " offset:" + offset + " length:" + length ); 199 } 200 201 /* everything ok */ 202 203 int position = offset; 204 int remainingLength = length; 205 int sizeRead = 0; 206 207 /* bytes read on each iteration of loop */ 208 int bytesRead; 209 210 flushed = false; 211 drained = false; 212 213 /* 214 * to read, we first take stuff from the fragmentBuffer 215 */ 216 217 /* on first read() of the line, fragmentBuffer is null */ 218 synchronized (this) { 219 if (fragmentBuffer != null) { 220 boolean fragmentBufferSmaller = fragmentBuffer.length < length; 221 int smallerBufferLength = Math.min(fragmentBuffer.length, 222 length); 223 System.arraycopy(fragmentBuffer, 0, data, position, 224 smallerBufferLength); 225 framesSinceOpen += smallerBufferLength 226 / currentFormat.getFrameSize(); 227 228 if (!fragmentBufferSmaller) { 229 /* 230 * if fragment was larger, then we already have all the data 231 * we need. clean up the buffer before returning. Make a new 232 * fragmentBuffer from the remaining bytes 233 */ 234 int remainingBytesInFragment = (fragmentBuffer.length - length); 235 byte[] newFragmentBuffer = new byte[remainingBytesInFragment]; 236 System.arraycopy(fragmentBuffer, length, newFragmentBuffer, 237 0, newFragmentBuffer.length); 238 fragmentBuffer = newFragmentBuffer; 239 return length; 240 } 241 242 /* done with fragment buffer, remove it */ 243 bytesRead = smallerBufferLength; 244 sizeRead += bytesRead; 245 position += bytesRead; 246 remainingLength -= bytesRead; 247 fragmentBuffer = null; 248 } 249 } 250 251 /* 252 * if we need to read more data, then we read from PulseAudio's buffer 253 */ 254 while (remainingLength != 0) { 255 synchronized (this) { 256 257 if (!isOpen() || !isStarted) { 258 return sizeRead; 259 } 260 261 if (flushed) { 262 flushed = false; 263 return sizeRead; 264 } 265 266 if (drained) { 267 drained = false; 268 return sizeRead; 269 } 270 271 byte[] currentFragment; 272 synchronized (eventLoop.threadLock) { 273 274 /* read a fragment, and drop it from the server */ 275 currentFragment = stream.peek(); 276 277 stream.drop(); 278 if (currentFragment == null) { 279 Debug.println(DebugLevel.Verbose, 280 "PulseAudioTargetDataLine.read(): " 281 + " error in stream.peek()"); 282 continue; 283 } 284 285 bytesRead = Math.min(currentFragment.length, 286 remainingLength); 287 288 /* 289 * we read more than we required, save the rest of the data 290 * in the fragmentBuffer 291 */ 292 if (bytesRead < currentFragment.length) { 293 /* allocate a buffer to store unsaved data */ 294 fragmentBuffer = new byte[currentFragment.length 295 - bytesRead]; 296 297 /* copy over the unsaved data */ 298 System.arraycopy(currentFragment, bytesRead, 299 fragmentBuffer, 0, currentFragment.length 300 - bytesRead); 301 } 302 303 System.arraycopy(currentFragment, 0, data, position, 304 bytesRead); 305 306 sizeRead += bytesRead; 307 position += bytesRead; 308 remainingLength -= bytesRead; 309 framesSinceOpen += bytesRead / currentFormat.getFrameSize(); 310 } 311 } 312 } 313 314 // all the data should have been played by now 315 assert (sizeRead == length); 316 317 return sizeRead; 318 319 } 320 321 @Override 322 public void drain() { 323 324 // blocks when there is data on the line 325 // http://www.jsresources.org/faq_audio.html#stop_drain_tdl 326 while (true) { 327 synchronized (this) { 328 if (!isStarted || !isOpen()) { 329 break; 330 } 331 } 332 try { 333 //TODO: Is this the best length of sleep? 334 //Maybe in case this loop runs for a long time 335 //it would be good to switch to a longer 336 //sleep. Like bump it up each iteration after 337 //the Nth iteration, up to a MAXSLEEP length. 338 Thread.sleep(100); 339 } catch (InterruptedException e) { 340 // do nothing 341 } 342 } 343 344 synchronized (this) { 345 drained = true; 346 } 347 } 348 349 @Override 350 public synchronized void flush() { 351 if (isOpen()) { 352 353 /* flush the buffer on pulseaudio's side */ 354 Operation operation; 355 synchronized (eventLoop.threadLock) { 356 operation = stream.flush(); 357 } 358 operation.waitForCompletion(); 359 operation.releaseReference(); 360 } 361 362 flushed = true; 363 /* flush the partial fragment we stored */ 364 fragmentBuffer = null; 365 } 366 367 @Override 368 public int available() { 369 if (!isOpen()) { 370 // a closed line has 0 bytes available. 371 return 0; 372 } 373 374 synchronized (eventLoop.threadLock) { 375 return stream.getReableSize(); 376 } 377 } 378 379 @Override 380 public int getFramePosition() { 381 return (int) framesSinceOpen; 382 } 383 384 @Override 385 public long getLongFramePosition() { 386 return framesSinceOpen; 387 } 388 389 @Override 390 public long getMicrosecondPosition() { 391 return (long) (framesSinceOpen / currentFormat.getFrameRate()); 392 } 393 394 /* 395 * A TargetData starts when we ask it to and continues playing until we ask 396 * it to stop. There are no buffer underruns/overflows or anything so we 397 * will just fire the LineEvents manually 398 */ 399 400 @Override 401 synchronized public void start() { 402 super.start(); 403 404 fireLineEvent(new LineEvent(this, LineEvent.Type.START, framesSinceOpen)); 405 } 406 407 @Override 408 synchronized public void stop() { 409 super.stop(); 410 411 fireLineEvent(new LineEvent(this, LineEvent.Type.STOP, framesSinceOpen)); 412 } 413 414 @Override 415 public Line.Info getLineInfo() { 416 return new DataLine.Info(TargetDataLine.class, supportedFormats, 417 StreamBufferAttributes.MIN_VALUE, 418 StreamBufferAttributes.MAX_VALUE); 419 } 420 421}