001/* PulseAudioClip.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.io.IOException; 041 042import javax.sound.sampled.AudioFormat; 043import javax.sound.sampled.AudioInputStream; 044import javax.sound.sampled.AudioSystem; 045import javax.sound.sampled.Clip; 046import javax.sound.sampled.DataLine; 047import javax.sound.sampled.Line; 048import javax.sound.sampled.LineUnavailableException; 049 050import org.classpath.icedtea.pulseaudio.Debug.DebugLevel; 051import org.classpath.icedtea.pulseaudio.Stream.WriteListener; 052 053public final class PulseAudioClip extends PulseAudioDataLine implements Clip, 054 PulseAudioPlaybackLine { 055 056 private byte[] data = null; 057 058 // these are frame indices. so counted from 0 059 // the current frame index 060 private int currentFrame = 0; 061 062 // total number of frames in this clip 063 private int frameCount = 0; 064 065 // the starting frame of the loop 066 private int startFrame = 0; 067 // the ending frame of the loop 068 private int endFrame = 0; 069 070 public static final String DEFAULT_CLIP_NAME = "Audio Clip"; 071 072 private Object clipLock = new Object(); 073 private int loopsLeft = 0; 074 075 // private Semaphore clipSemaphore = new Semaphore(1); 076 077 /** 078 * This thread runs 079 * 080 */ 081 private final class ClipThread extends Thread { 082 @Override 083 public void run() { 084 085 /* 086 * The while loop below only works with LOOP_CONTINUOUSLY because we 087 * abuse the fact that loopsLeft's initial value is -1 088 * (=LOOP_CONTINUOUSLY) and it keeps on going lower without hitting 089 * 0. So do a sanity check 090 */ 091 if (Clip.LOOP_CONTINUOUSLY != -1) { 092 throw new UnsupportedOperationException( 093 "LOOP_CONTINUOUSLY has changed; things are going to break"); 094 } 095 096 while (true) { 097 writeFrames(currentFrame, endFrame + 1); 098 if (Thread.interrupted()) { 099 // Thread.currentThread().interrupt(); 100 // System.out.println("returned from interrupted 101 // writeFrames"); 102 break; 103 } 104 105 // if loop(0) has been called from the mainThread, 106 // wait until loopsLeft has been set 107 if (loopsLeft == 0) { 108 // System.out.println("Reading to the end of the file"); 109 // System.out.println("endFrame: " + endFrame); 110 writeFrames(endFrame, getFrameLength()); 111 break; 112 } else { 113 synchronized (clipLock) { 114 currentFrame = startFrame; 115 if (loopsLeft != Integer.MIN_VALUE) { 116 loopsLeft--; 117 } 118 } 119 } 120 121 } 122 123 // drain 124 Operation operation; 125 126 synchronized (eventLoop.threadLock) { 127 operation = stream.drain(); 128 } 129 130 operation.waitForCompletion(); 131 operation.releaseReference(); 132 133 } 134 } 135 136 private ClipThread clipThread; 137 138 private void writeFrames(int startingFrame, int lastFrame) { 139 140 WriteListener writeListener = new WriteListener() { 141 @Override 142 public void update() { 143 synchronized (eventLoop.threadLock) { 144 eventLoop.threadLock.notifyAll(); 145 } 146 } 147 }; 148 149 stream.addWriteListener(writeListener); 150 151 Debug.println(DebugLevel.Verbose, 152 "PulseAudioClip$ClipThread.writeFrames(): Writing"); 153 154 int remainingFrames = lastFrame - startingFrame - 1; 155 while (remainingFrames > 0) { 156 synchronized (eventLoop.threadLock) { 157 int availableSize; 158 159 do { 160 availableSize = stream.getWritableSize(); 161 if (availableSize < 0) { 162 Thread.currentThread().interrupt(); 163 stream.removeWriteListener(writeListener); 164 return; 165 } 166 if (availableSize == 0) { 167 try { 168 eventLoop.threadLock.wait(); 169 } catch (InterruptedException e) { 170 // System.out 171 // .println("interrupted while waiting for 172 // getWritableSize"); 173 // clean up and return 174 Thread.currentThread().interrupt(); 175 stream.removeWriteListener(writeListener); 176 return; 177 } 178 } 179 180 } while (availableSize == 0); 181 182 int framesToWrite = Math.min(remainingFrames, availableSize 183 / getFormat().getFrameSize()); 184 stream.write(data, currentFrame * getFormat().getFrameSize(), 185 framesToWrite * getFormat().getFrameSize()); 186 remainingFrames -= framesToWrite; 187 currentFrame += framesToWrite; 188 framesSinceOpen += framesToWrite; 189 if (Thread.interrupted()) { 190 Thread.currentThread().interrupt(); 191 break; 192 } 193 // System.out.println("remaining frames" + remainingFrames); 194 // System.out.println("currentFrame: " + currentFrame); 195 // System.out.println("framesSinceOpen: " + framesSinceOpen); 196 } 197 } 198 199 stream.removeWriteListener(writeListener); 200 } 201 202 PulseAudioClip(AudioFormat[] formats, AudioFormat defaultFormat) { 203 this.supportedFormats = formats; 204 this.defaultFormat = defaultFormat; 205 this.currentFormat = defaultFormat; 206 this.streamName = DEFAULT_CLIP_NAME; 207 208 clipThread = new ClipThread(); 209 210 } 211 212 @Override 213 protected void connectLine(int bufferSize, Stream masterStream) 214 throws LineUnavailableException { 215 StreamBufferAttributes bufferAttributes = new StreamBufferAttributes( 216 bufferSize, bufferSize / 4, bufferSize / 8, 217 ((bufferSize / 10) > 100 ? bufferSize / 10 : 100), 0); 218 219 if (masterStream != null) { 220 synchronized (eventLoop.threadLock) { 221 stream.connectForPlayback(Stream.DEFAULT_DEVICE, 222 bufferAttributes, masterStream.getStreamPointer()); 223 } 224 } else { 225 synchronized (eventLoop.threadLock) { 226 stream.connectForPlayback(Stream.DEFAULT_DEVICE, 227 bufferAttributes, null); 228 } 229 } 230 } 231 232 @Override 233 public int available() { 234 return 0; // a clip always returns 0 235 } 236 237 @Override 238 public void close() { 239 240 if (!isOpen) { 241 throw new IllegalStateException("line already closed"); 242 } 243 244 clipThread.interrupt(); 245 246 try { 247 clipThread.join(); 248 } catch (InterruptedException e) { 249 e.printStackTrace(); 250 } 251 252 currentFrame = 0; 253 framesSinceOpen = 0; 254 255 PulseAudioMixer mixer = PulseAudioMixer.getInstance(); 256 mixer.removeSourceLine(this); 257 258 super.close(); 259 260 Debug.println(DebugLevel.Verbose, "PulseAudioClip.close(): " 261 + "Clip closed"); 262 263 } 264 265 /* 266 * 267 * drain() on a Clip should block until the entire clip has finished playing 268 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4732218 269 */ 270 @Override 271 public void drain() { 272 if (!isOpen) { 273 throw new IllegalStateException("line not open"); 274 } 275 276 while (clipThread != null && clipThread.isAlive()) { 277 try { 278 clipThread.join(); 279 } catch (InterruptedException e) { 280 // ignore 281 } 282 } 283 284 Operation operation; 285 286 synchronized (eventLoop.threadLock) { 287 operation = stream.drain(); 288 } 289 290 operation.waitForCompletion(); 291 operation.releaseReference(); 292 293 } 294 295 @Override 296 public void flush() { 297 if (!isOpen) { 298 throw new IllegalStateException("line not open"); 299 } 300 301 Operation operation; 302 synchronized (eventLoop.threadLock) { 303 operation = stream.flush(); 304 operation.waitForCompletion(); 305 } 306 operation.releaseReference(); 307 308 } 309 310 @Override 311 public int getFrameLength() { 312 if (!isOpen) { 313 return AudioSystem.NOT_SPECIFIED; 314 } 315 316 return frameCount; 317 } 318 319 @Override 320 public int getFramePosition() { 321 if (!isOpen) { 322 throw new IllegalStateException("Line not open"); 323 } 324 synchronized (clipLock) { 325 return (int) framesSinceOpen; 326 } 327 } 328 329 @Override 330 public long getLongFramePosition() { 331 if (!isOpen) { 332 throw new IllegalStateException("Line not open"); 333 } 334 335 synchronized (clipLock) { 336 return framesSinceOpen; 337 } 338 } 339 340 @Override 341 public long getMicrosecondLength() { 342 if (!isOpen) { 343 return AudioSystem.NOT_SPECIFIED; 344 } 345 synchronized (clipLock) { 346 return (long) (frameCount / currentFormat.getFrameRate() * SECONDS_TO_MICROSECONDS); 347 } 348 } 349 350 @Override 351 public long getMicrosecondPosition() { 352 if (!isOpen) { 353 throw new IllegalStateException("Line not open"); 354 } 355 356 synchronized (clipLock) { 357 return (long) (framesSinceOpen / currentFormat.getFrameRate() * SECONDS_TO_MICROSECONDS); 358 } 359 } 360 361 @Override 362 public void loop(int count) { 363 if (!isOpen) { 364 throw new IllegalStateException("Line not open"); 365 } 366 367 if (count < 0 && count != LOOP_CONTINUOUSLY) { 368 throw new IllegalArgumentException("invalid value for count:" 369 + count); 370 } 371 372 if (clipThread.isAlive() && count != 0) { 373 // Do nothing; behavior not specified by the Java API 374 return; 375 } 376 377 super.start(); 378 379 synchronized (clipLock) { 380 if (currentFrame > endFrame) { 381 loopsLeft = 0; 382 } else { 383 loopsLeft = count; 384 } 385 } 386 if (!clipThread.isAlive()) { 387 clipThread = new ClipThread(); 388 clipThread.start(); 389 } 390 391 } 392 393 @Override 394 public void open() throws LineUnavailableException { 395 throw new IllegalArgumentException("open() on a Clip is not allowed"); 396 } 397 398 @Override 399 public void open(AudioFormat format, byte[] data, int offset, int bufferSize) 400 throws LineUnavailableException { 401 402 super.open(format); 403 this.data = new byte[bufferSize]; 404 System.arraycopy(data, offset, this.data, 0, bufferSize); 405 406 frameCount = bufferSize / format.getFrameSize(); 407 currentFrame = 0; 408 framesSinceOpen = 0; 409 startFrame = 0; 410 endFrame = frameCount - 1; 411 loopsLeft = 0; 412 413 PulseAudioVolumeControl volumeControl = new PulseAudioVolumeControl( 414 this, eventLoop); 415 controls.add(volumeControl); 416 417 PulseAudioMixer mixer = PulseAudioMixer.getInstance(); 418 mixer.addSourceLine(this); 419 420 isOpen = true; 421 Debug.println(DebugLevel.Verbose, "PulseAudioClip.open(): Clip opened"); 422 423 } 424 425 // FIXME 426 @Override 427 public byte[] native_set_volume(float value) { 428 return stream.native_set_volume(value); 429 } 430 431 public byte[] native_update_volume() { 432 return stream.native_update_volume(); 433 } 434 435 @Override 436 public float getCachedVolume() { 437 return stream.getCachedVolume(); 438 } 439 440 @Override 441 public void setCachedVolume(float value) { 442 stream.setCachedVolume(value); 443 444 } 445 446 @Override 447 public void open(AudioInputStream stream) throws LineUnavailableException, 448 IOException { 449 byte[] buffer = new byte[(int) (stream.getFrameLength() * stream 450 .getFormat().getFrameSize())]; 451 stream.read(buffer, 0, buffer.length); 452 453 open(stream.getFormat(), buffer, 0, buffer.length); 454 455 } 456 457 @Override 458 public void setFramePosition(int frames) { 459 if (!isOpen) { 460 throw new IllegalStateException("Line not open"); 461 } 462 463 if (frames < 0 || frames > frameCount) { 464 throw new IllegalArgumentException("incorreft frame value"); 465 } 466 467 synchronized (clipLock) { 468 currentFrame = frames; 469 } 470 471 } 472 473 @Override 474 public void setLoopPoints(int start, int end) { 475 if (!isOpen) { 476 throw new IllegalStateException("Line not open"); 477 } 478 479 if (end == -1) { 480 end = frameCount - 1; 481 } 482 483 if (end < start) { 484 throw new IllegalArgumentException( 485 "ending point must be greater than or equal to the starting point"); 486 } 487 488 if (start < 0) { 489 throw new IllegalArgumentException( 490 "starting point must be greater than or equal to 0"); 491 } 492 493 synchronized (clipLock) { 494 startFrame = start; 495 endFrame = end; 496 } 497 498 } 499 500 @Override 501 public void setMicrosecondPosition(long microseconds) { 502 if (!isOpen) { 503 throw new IllegalStateException("Line not open"); 504 } 505 506 float frameIndex = microseconds * currentFormat.getFrameRate() / SECONDS_TO_MICROSECONDS; 507 508 /* make frameIndex positive */ 509 while (frameIndex < 0) { 510 frameIndex += frameCount; 511 } 512 513 /* frameIndex is in the range [0, frameCount-1], inclusive */ 514 frameIndex = frameIndex % frameCount; 515 516 synchronized (clipLock) { 517 currentFrame = (int) frameIndex; 518 } 519 520 } 521 522 @Override 523 public void start() { 524 if (isStarted) { 525 return; 526 } 527 528 super.start(); 529 530 if (!clipThread.isAlive()) { 531 synchronized (clipLock) { 532 loopsLeft = 0; 533 } 534 clipThread = new ClipThread(); 535 clipThread.start(); 536 } 537 538 } 539 540 @Override 541 public void stop() { 542 if (!isOpen) { 543 throw new IllegalStateException("Line not open"); 544 } 545 546 /* do what start does and ignore if called at the wrong time */ 547 if (!isStarted) { 548 return; 549 } 550 551 if (clipThread.isAlive()) { 552 clipThread.interrupt(); 553 } 554 try { 555 clipThread.join(); 556 } catch (InterruptedException e) { 557 e.printStackTrace(); 558 } 559 synchronized (clipLock) { 560 loopsLeft = 0; 561 } 562 563 super.stop(); 564 565 } 566 567 @Override 568 public Line.Info getLineInfo() { 569 return new DataLine.Info(Clip.class, supportedFormats, 570 StreamBufferAttributes.MIN_VALUE, 571 StreamBufferAttributes.MAX_VALUE); 572 } 573 574}