001/* PulseAudioSourceDataLine.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 java.util.ArrayList; 041 042import javax.sound.sampled.AudioFormat; 043import javax.sound.sampled.DataLine; 044import javax.sound.sampled.Line; 045import javax.sound.sampled.LineListener; 046import javax.sound.sampled.LineUnavailableException; 047import javax.sound.sampled.SourceDataLine; 048 049import org.classpath.icedtea.pulseaudio.Debug.DebugLevel; 050 051public final class PulseAudioSourceDataLine extends PulseAudioDataLine 052 implements SourceDataLine, PulseAudioPlaybackLine { 053 054 private PulseAudioVolumeControl volumeControl; 055 056 public static final String DEFAULT_SOURCEDATALINE_NAME = "Audio Stream"; 057 058 /* 059 * Package-private constructor only called by PulseAudioMixer 060 */ 061 PulseAudioSourceDataLine(AudioFormat[] formats, AudioFormat defaultFormat) { 062 063 this.supportedFormats = formats; 064 this.lineListeners = new ArrayList<LineListener>(); 065 this.defaultFormat = defaultFormat; 066 this.currentFormat = defaultFormat; 067 this.streamName = DEFAULT_SOURCEDATALINE_NAME; 068 069 } 070 071 @Override 072 synchronized public void open(AudioFormat format, int bufferSize) 073 throws LineUnavailableException { 074 075 super.open(format, bufferSize); 076 077 volumeControl = new PulseAudioVolumeControl(this, eventLoop); 078 controls.add(volumeControl); 079 080 PulseAudioMixer parentMixer = PulseAudioMixer.getInstance(); 081 parentMixer.addSourceLine(this); 082 083 Debug.println(DebugLevel.Verbose, "PulseAudioSourceDataLine.open(): " 084 + "line opened"); 085 086 } 087 088 @Override 089 public void open(AudioFormat format) throws LineUnavailableException { 090 open(format, DEFAULT_BUFFER_SIZE); 091 } 092 093 // FIXME 094 public byte[] native_set_volume(float value) { 095 synchronized (eventLoop.threadLock) { 096 return stream.native_set_volume(value); 097 } 098 } 099 100 public byte[] native_update_volume() { 101 synchronized (eventLoop.threadLock) { 102 return stream.native_update_volume(); 103 } 104 } 105 106 @Override 107 public float getCachedVolume() { 108 return stream.getCachedVolume(); 109 } 110 111 @Override 112 synchronized public void setCachedVolume(float value) { 113 stream.setCachedVolume(value); 114 } 115 116 @Override 117 protected void connectLine(int bufferSize, Stream masterStream) 118 throws LineUnavailableException { 119 StreamBufferAttributes bufferAttributes = 120 new StreamBufferAttributes( 121 bufferSize, 122 bufferSize / 4, 123 bufferSize / 8, 124 Math.max(bufferSize / 10, 100), 125 0); 126 127 if (masterStream != null) { 128 synchronized (eventLoop.threadLock) { 129 stream.connectForPlayback(Stream.DEFAULT_DEVICE, 130 bufferAttributes, masterStream.getStreamPointer()); 131 } 132 } else { 133 synchronized (eventLoop.threadLock) { 134 stream.connectForPlayback(Stream.DEFAULT_DEVICE, 135 bufferAttributes, null); 136 } 137 } 138 } 139 140 @Override 141 public int write(byte[] data, int offset, int length) { 142 // can't call write() without open()ing first, but can call write() 143 // without start()ing 144 synchronized (this) { 145 writeInterrupted = false; 146 } 147 148 if (!isOpen()) { 149 // A closed line can write exactly 0 bytes. 150 return 0; 151 } 152 153 int frameSize = currentFormat.getFrameSize(); 154 if (length % frameSize != 0) { 155 throw new IllegalArgumentException( 156 "amount of data to write does not represent an integral number of frames"); 157 } 158 159 if (length < 0) { 160 throw new IllegalArgumentException("length is negative"); 161 } 162 163 if (length < 0 || offset < 0 || offset > data.length - length) { 164 throw new ArrayIndexOutOfBoundsException( 165 "Overflow condition: buffer.length=" + data.length + 166 " offset= " + offset + " length=" + length ); 167 } 168 169 int position = offset; 170 int remainingLength = length; 171 int availableSize = 0; 172 173 int sizeWritten = 0; 174 175 boolean interrupted = false; 176 177 while (remainingLength != 0) { 178 179 synchronized (eventLoop.threadLock) { 180 181 do { 182 synchronized (this) { 183 if (writeInterrupted) { 184 return sizeWritten; 185 } 186 } 187 188 if (availableSize == -1) { 189 return sizeWritten; 190 } 191 availableSize = stream.getWritableSize(); 192 193 if (availableSize == 0) { 194 try { 195 eventLoop.threadLock.wait(100); 196 } catch (InterruptedException e) { 197 // ignore for now 198 interrupted = true; 199 } 200 201 } 202 203 } while (availableSize == 0); 204 205 if (availableSize > remainingLength) { 206 availableSize = remainingLength; 207 } 208 209 // only write entire frames, so round down avialableSize to 210 // a multiple of frameSize 211 availableSize = (availableSize / frameSize) * frameSize; 212 213 synchronized (this) { 214 if (writeInterrupted) { 215 return sizeWritten; 216 } 217 /* write a little bit of the buffer */ 218 stream.write(data, position, availableSize); 219 } 220 221 sizeWritten += availableSize; 222 position += availableSize; 223 remainingLength -= availableSize; 224 225 framesSinceOpen += availableSize / frameSize; 226 227 } 228 } 229 230 // all the data should have been played by now 231 assert (sizeWritten == length); 232 233 if (interrupted) { 234 Thread.currentThread().interrupt(); 235 } 236 237 return sizeWritten; 238 } 239 240 @Override 241 public int available() { 242 synchronized (eventLoop.threadLock) { 243 return stream.getWritableSize(); 244 } 245 }; 246 247 @Override 248 public int getFramePosition() { 249 return (int) framesSinceOpen; 250 } 251 252 @Override 253 public long getLongFramePosition() { 254 return framesSinceOpen; 255 } 256 257 @Override 258 public long getMicrosecondPosition() { 259 260 float frameRate = currentFormat.getFrameRate(); 261 float time = framesSinceOpen / frameRate; // seconds 262 long microseconds = (long) (time * SECONDS_TO_MICROSECONDS); 263 return microseconds; 264 } 265 266 @Override 267 public void drain() { 268 269 synchronized (this) { 270 writeInterrupted = true; 271 } 272 273 do { 274 synchronized (this) { 275 if (!isOpen()) { 276 return; 277 } 278 if (getBytesInBuffer() == 0) { 279 return; 280 } 281 if (isStarted) { 282 break; 283 } 284 try { 285 this.wait(100); 286 } catch (InterruptedException e) { 287 return; 288 } 289 } 290 } while (!isStarted); 291 292 Operation operation; 293 294 synchronized (eventLoop.threadLock) { 295 operation = stream.drain(); 296 } 297 298 operation.waitForCompletion(); 299 operation.releaseReference(); 300 301 } 302 303 @Override 304 public void flush() { 305 synchronized (this) { 306 writeInterrupted = true; 307 } 308 309 if (isOpen()) { 310 Operation operation; 311 synchronized (eventLoop.threadLock) { 312 operation = stream.flush(); 313 } 314 315 operation.waitForCompletion(); 316 operation.releaseReference(); 317 } 318 319 } 320 321 @Override 322 synchronized public void close() { 323 324 if (!isOpen()) { 325 return; 326 } 327 328 writeInterrupted = true; 329 330 PulseAudioMixer parent = PulseAudioMixer.getInstance(); 331 parent.removeSourceLine(this); 332 333 super.close(); 334 335 Debug.println(DebugLevel.Verbose, "PulseAudioSourceDataLine.close():" 336 + " line closed"); 337 338 } 339 340 @Override 341 public Line.Info getLineInfo() { 342 return new DataLine.Info(SourceDataLine.class, supportedFormats, 343 StreamBufferAttributes.MIN_VALUE, 344 StreamBufferAttributes.MAX_VALUE); 345 } 346 347}