001/* PulseAudioMixer.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.net.InetAddress; 041import java.net.UnknownHostException; 042import java.util.ArrayList; 043import java.util.HashMap; 044import java.util.LinkedList; 045import java.util.List; 046import java.util.Map; 047import java.util.concurrent.Semaphore; 048 049import javax.sound.sampled.AudioFormat; 050import javax.sound.sampled.AudioPermission; 051import javax.sound.sampled.AudioSystem; 052import javax.sound.sampled.Clip; 053import javax.sound.sampled.Control; 054import javax.sound.sampled.DataLine; 055import javax.sound.sampled.Line; 056import javax.sound.sampled.LineEvent; 057import javax.sound.sampled.LineListener; 058import javax.sound.sampled.LineUnavailableException; 059import javax.sound.sampled.Mixer; 060import javax.sound.sampled.Port; 061import javax.sound.sampled.SourceDataLine; 062import javax.sound.sampled.TargetDataLine; 063import javax.sound.sampled.AudioFormat.Encoding; 064import javax.sound.sampled.Control.Type; 065 066import org.classpath.icedtea.pulseaudio.Debug.DebugLevel; 067 068public final class PulseAudioMixer implements Mixer { 069 // singleton 070 071 private Thread eventLoopThread; 072 073 private List<Line.Info> sourceLineInfos = new ArrayList<Line.Info>(); 074 private List<Line.Info> staticSourceLineInfos = new ArrayList<Line.Info>(); 075 076 private List<Line.Info> targetLineInfos = new ArrayList<Line.Info>(); 077 private List<Line.Info> staticTargetLineInfos = new ArrayList<Line.Info>(); 078 079 private static PulseAudioMixer _instance = null; 080 081 private static final String DEFAULT_APP_NAME = "Java"; 082 static final String PULSEAUDIO_FORMAT_KEY = "PulseAudioFormatKey"; 083 084 private boolean isOpen = false; 085 086 private final List<PulseAudioLine> sourceLines = new ArrayList<PulseAudioLine>(); 087 private final List<PulseAudioLine> targetLines = new ArrayList<PulseAudioLine>(); 088 089 private final List<LineListener> lineListeners = new ArrayList<LineListener>(); 090 091 private PulseAudioMixer() { 092 093 Debug.println(DebugLevel.Verbose, "PulseAudioMixer.PulseAudioMixer(): " 094 + "Contructing PulseAudioMixer..."); 095 096 AudioFormat[] formats = getSupportedFormats(); 097 098 staticSourceLineInfos.add(new DataLine.Info(SourceDataLine.class, 099 formats, StreamBufferAttributes.MIN_VALUE, 100 StreamBufferAttributes.MAX_VALUE)); 101 staticSourceLineInfos.add(new DataLine.Info(Clip.class, formats, 102 StreamBufferAttributes.MIN_VALUE, 103 StreamBufferAttributes.MAX_VALUE)); 104 105 staticTargetLineInfos.add(new DataLine.Info(TargetDataLine.class, 106 formats, StreamBufferAttributes.MIN_VALUE, 107 StreamBufferAttributes.MAX_VALUE)); 108 109 refreshSourceAndTargetLines(); 110 111 Debug.println(DebugLevel.Verbose, "PulseAudioMixer.PulseAudioMixer(): " 112 + "Finished constructing PulseAudioMixer"); 113 114 } 115 116 synchronized public static PulseAudioMixer getInstance() { 117 if (_instance == null) { 118 _instance = new PulseAudioMixer(); 119 } 120 return _instance; 121 } 122 123 private AudioFormat[] getSupportedFormats() { 124 125 List<AudioFormat> supportedFormats = new ArrayList<AudioFormat>(); 126 127 Map<String, Object> properties; 128 129 /* 130 * frameSize = sample size (in bytes, not bits) x # of channels 131 * 132 * From PulseAudio's sources 133 * http://git.0pointer.de/?p=pulseaudio.git;a=blob 134 * ;f=src/pulse/sample.c;h=93da2465f4301e27af4976e82737c3a048124a68;hb= 135 * 82ea8dde8abc51165a781c69bc3b38034d62d969#l63 136 */ 137 138 /* 139 * technically, PulseAudio supports up to 16 channels, but things get 140 * interesting with channel maps 141 * 142 * PA_CHANNEL_MAP_DEFAULT (=PA_CHANNEL_MAP_AIFF) supports 1,2,3,4,5 or 6 143 * channels only 144 */ 145 int[] channelSizes = new int[] { 1, 2, 3, 4, 5, 6 }; 146 147 for (int channelSize : channelSizes) { 148 properties = new HashMap<String, Object>(); 149 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_ALAW"); 150 151 int sampleSize = 8; 152 final AudioFormat PA_SAMPLE_ALAW = new AudioFormat(Encoding.ALAW, // encoding 153 AudioSystem.NOT_SPECIFIED, // sample rate 154 sampleSize, // sample size 155 channelSize, // channels 156 sampleSize / 8 * channelSize, // frame size 157 AudioSystem.NOT_SPECIFIED, // frame rate 158 false, // big endian? 159 properties); 160 161 supportedFormats.add(PA_SAMPLE_ALAW); 162 } 163 164 for (int channelSize : channelSizes) { 165 properties = new HashMap<String, Object>(); 166 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_ULAW"); 167 168 int sampleSize = 8; 169 final AudioFormat PA_SAMPLE_ULAW = new AudioFormat(Encoding.ULAW, // encoding 170 AudioSystem.NOT_SPECIFIED, // sample rate 171 sampleSize, // sample size 172 channelSize, // channels 173 sampleSize / 8 * channelSize, // frame size 174 AudioSystem.NOT_SPECIFIED, // frame rate 175 false, // big endian? 176 properties); 177 178 supportedFormats.add(PA_SAMPLE_ULAW); 179 } 180 181 for (int channelSize : channelSizes) { 182 properties = new HashMap<String, Object>(); 183 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_S16BE"); 184 185 int sampleSize = 16; 186 final AudioFormat PA_SAMPLE_S16BE = new AudioFormat( 187 Encoding.PCM_SIGNED, // encoding 188 AudioSystem.NOT_SPECIFIED, // sample rate 189 sampleSize, // sample size 190 channelSize, // channels 191 sampleSize / 8 * channelSize, // frame size 192 AudioSystem.NOT_SPECIFIED, // frame rate 193 true, // big endian? 194 properties); 195 196 supportedFormats.add(PA_SAMPLE_S16BE); 197 } 198 199 for (int channelSize : channelSizes) { 200 properties = new HashMap<String, Object>(); 201 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_S16LE"); 202 203 int sampleSize = 16; 204 final AudioFormat A_SAMPLE_S16LE = new AudioFormat( 205 Encoding.PCM_SIGNED, // encoding 206 AudioSystem.NOT_SPECIFIED, // sample rate 207 sampleSize, // sample size 208 channelSize, // channels 209 sampleSize / 8 * channelSize, // frame size 210 AudioSystem.NOT_SPECIFIED, // frame rate 211 false, // big endian? 212 properties); 213 214 supportedFormats.add(A_SAMPLE_S16LE); 215 } 216 217 for (int channelSize : channelSizes) { 218 properties = new HashMap<String, Object>(); 219 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_S32BE"); 220 221 int sampleSize = 32; 222 final AudioFormat PA_SAMPLE_S32BE = new AudioFormat( 223 Encoding.PCM_SIGNED, // encoding 224 AudioSystem.NOT_SPECIFIED, // sample rate 225 sampleSize, // sample size 226 channelSize, // channels 227 sampleSize / 8 * channelSize, // frame size 228 AudioSystem.NOT_SPECIFIED, // frame rate 229 true, // big endian? 230 properties); 231 232 supportedFormats.add(PA_SAMPLE_S32BE); 233 } 234 235 for (int channelSize : channelSizes) { 236 properties = new HashMap<String, Object>(); 237 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_S32LE"); 238 239 int sampleSize = 32; 240 final AudioFormat PA_SAMPLE_S32LE = new AudioFormat( 241 Encoding.PCM_SIGNED, // encoding 242 AudioSystem.NOT_SPECIFIED, // sample rate 243 sampleSize, // sample size 244 channelSize, // channels 245 sampleSize / 8 * channelSize, // frame size 246 AudioSystem.NOT_SPECIFIED, // frame rate 247 false, // big endian? 248 properties); 249 250 supportedFormats.add(PA_SAMPLE_S32LE); 251 } 252 253 for (int channelSize : channelSizes) { 254 properties = new HashMap<String, Object>(); 255 properties.put(PULSEAUDIO_FORMAT_KEY, "PA_SAMPLE_U8"); 256 257 int sampleSize = 8; // in bits 258 AudioFormat PA_SAMPLE_U8 = new AudioFormat(Encoding.PCM_UNSIGNED, // encoding 259 AudioSystem.NOT_SPECIFIED, // sample rate 260 sampleSize, // sample size 261 channelSize, // channels 262 sampleSize / 8 * channelSize, // frame size in bytes 263 AudioSystem.NOT_SPECIFIED, // frame rate 264 false, // big endian? 265 properties); 266 267 supportedFormats.add(PA_SAMPLE_U8); 268 } 269 270 return supportedFormats.toArray(new AudioFormat[0]); 271 } 272 273 @Override 274 public Line getLine(Line.Info info) throws LineUnavailableException { 275 276 if (!isLineSupported(info)) { 277 throw new IllegalArgumentException("Line unsupported: " + info); 278 } 279 280 AudioFormat[] formats = null; 281 AudioFormat defaultFormat = null; 282 283 if (DataLine.Info.class.isInstance(info)) { 284 ArrayList<AudioFormat> formatList = new ArrayList<AudioFormat>(); 285 AudioFormat[] requestedFormats = ((DataLine.Info) info) 286 .getFormats(); 287 for (int i = 0; i < requestedFormats.length; i++) { 288 AudioFormat f1 = requestedFormats[i]; 289 for (AudioFormat f2 : getSupportedFormats()) { 290 291 if (f1.matches(f2)) { 292 formatList.add(f2); 293 defaultFormat = f1; 294 } 295 } 296 } 297 formats = formatList.toArray(new AudioFormat[0]); 298 299 } else { 300 formats = getSupportedFormats(); 301 defaultFormat = new AudioFormat(Encoding.PCM_UNSIGNED, 44100, 8, 2, 302 2, AudioSystem.NOT_SPECIFIED, false); 303 } 304 305 if ((info.getLineClass() == SourceDataLine.class)) { 306 return new PulseAudioSourceDataLine(formats, defaultFormat); 307 } 308 309 if ((info.getLineClass() == TargetDataLine.class)) { 310 /* check for permission to record audio */ 311 AudioPermission perm = new AudioPermission("record", null); 312 perm.checkGuard(null); 313 314 return new PulseAudioTargetDataLine(formats, defaultFormat); 315 } 316 317 if ((info.getLineClass() == Clip.class)) { 318 return new PulseAudioClip(formats, defaultFormat); 319 } 320 321 if (Port.Info.class.isInstance(info)) { 322 Port.Info portInfo = (Port.Info) info; 323 if (portInfo.isSource()) { 324 /* check for permission to record audio */ 325 AudioPermission perm = new AudioPermission("record", null); 326 perm.checkGuard(null); 327 328 return new PulseAudioSourcePort(portInfo.getName()); 329 } else { 330 return new PulseAudioTargetPort(portInfo.getName()); 331 } 332 } 333 334 Debug.println(DebugLevel.Info, "PulseAudioMixer.getLine(): " 335 + "No matching line supported by PulseAudio"); 336 337 throw new IllegalArgumentException("No matching lines found"); 338 339 } 340 341 @Override 342 public int getMaxLines(Line.Info info) { 343 /* 344 * PulseAudio supports (theoretically) unlimited number of streams for 345 * supported formats 346 */ 347 if (isLineSupported(info)) { 348 return AudioSystem.NOT_SPECIFIED; 349 } 350 351 return 0; 352 } 353 354 @Override 355 public Info getMixerInfo() { 356 return PulseAudioMixerInfo.getInfo(); 357 } 358 359 public Line.Info[] getSourceLineInfo() { 360 return sourceLineInfos.toArray(new Line.Info[0]); 361 } 362 363 @Override 364 public Line.Info[] getSourceLineInfo(Line.Info info) { 365 ArrayList<Line.Info> infos = new ArrayList<Line.Info>(); 366 367 for (Line.Info supportedInfo : sourceLineInfos) { 368 if (info.matches(supportedInfo)) { 369 infos.add(supportedInfo); 370 } 371 } 372 return infos.toArray(new Line.Info[0]); 373 } 374 375 @Override 376 public Line[] getSourceLines() { 377 return sourceLines.toArray(new Line[0]); 378 379 } 380 381 @Override 382 public Line.Info[] getTargetLineInfo() { 383 return targetLineInfos.toArray(new Line.Info[0]); 384 } 385 386 @Override 387 public Line.Info[] getTargetLineInfo(Line.Info info) { 388 ArrayList<Line.Info> infos = new ArrayList<Line.Info>(); 389 390 for (Line.Info supportedInfo : targetLineInfos) { 391 if (info.matches(supportedInfo)) { 392 infos.add(supportedInfo); 393 } 394 } 395 return infos.toArray(new Line.Info[0]); 396 } 397 398 @Override 399 public Line[] getTargetLines() { 400 401 /* check for permission to record audio */ 402 AudioPermission perm = new AudioPermission("record", null); 403 perm.checkGuard(null); 404 405 return (Line[]) targetLines.toArray(new Line[0]); 406 } 407 408 @Override 409 public boolean isLineSupported(Line.Info info) { 410 if (info != null) { 411 for (Line.Info myInfo : sourceLineInfos) { 412 if (info.matches(myInfo)) { 413 return true; 414 } 415 } 416 417 for (Line.Info myInfo : targetLineInfos) { 418 if (info.matches(myInfo)) { 419 return true; 420 } 421 } 422 423 } 424 return false; 425 426 } 427 428 @Override 429 public boolean isSynchronizationSupported(Line[] lines, boolean maintainSync) { 430 431 return false; 432 } 433 434 @Override 435 public void synchronize(Line[] lines, boolean maintainSync) { 436 437 throw new IllegalArgumentException( 438 "Mixer does not support synchronizing lines"); 439 440 // Line masterStream = null; 441 // for (Line line : lines) { 442 // if (line.isOpen()) { 443 // masterStream = line; 444 // break; 445 // } 446 // } 447 // if (masterStream == null) { 448 // // for now, can't synchronize lines if none of them is open (no 449 // // stream pointer to pass) 450 // // will see what to do about this later 451 // throw new IllegalArgumentException(); 452 // } 453 // 454 // try { 455 // 456 // for (Line line : lines) { 457 // if (line != masterStream) { 458 // 459 // ((PulseAudioDataLine) line) 460 // .reconnectforSynchronization(((PulseAudioDataLine) masterStream) 461 // .getStream()); 462 // 463 // } 464 // } 465 // } catch (LineUnavailableException e) { 466 // // we couldn't reconnect, so tell the user we failed by throwing an 467 // // exception 468 // throw new IllegalArgumentException(e); 469 // } 470 471 } 472 473 @Override 474 public void unsynchronize(Line[] lines) { 475 // FIXME should be able to implement this 476 throw new IllegalArgumentException(); 477 } 478 479 @Override 480 public void addLineListener(LineListener listener) { 481 lineListeners.add(listener); 482 } 483 484 @Override 485 synchronized public void close() { 486 487 /* 488 * only allow the mixer to be controlled if either playback or recording 489 * is allowed 490 */ 491 492 if (!this.isOpen) { 493 throw new IllegalStateException("Mixer is not open; cant close"); 494 } 495 496 List<Line> linesToClose = new LinkedList<Line>(); 497 linesToClose.addAll(sourceLines); 498 if (sourceLines.size() > 0) { 499 500 Debug.println(DebugLevel.Warning, "PulseAudioMixer.close(): " 501 + linesToClose.size() 502 + " source lines were not closed. closing them now."); 503 504 linesToClose.addAll(sourceLines); 505 for (Line line : linesToClose) { 506 if (line.isOpen()) { 507 line.close(); 508 } 509 } 510 } 511 linesToClose.clear(); 512 513 if (targetLines.size() > 0) { 514 Debug.println(DebugLevel.Warning, "PulseAudioMixer.close(): " 515 + linesToClose.size() 516 + " target lines have not been closed"); 517 518 linesToClose.addAll(targetLines); 519 for (Line line : linesToClose) { 520 if (line.isOpen()) { 521 line.close(); 522 } 523 } 524 } 525 526 synchronized (lineListeners) { 527 lineListeners.clear(); 528 } 529 530 eventLoopThread.interrupt(); 531 532 try { 533 eventLoopThread.join(); 534 } catch (InterruptedException e) { 535 System.out.println(this.getClass().getName() 536 + ": interrupted while waiting for eventloop to finish"); 537 } 538 539 isOpen = false; 540 541 refreshSourceAndTargetLines(); 542 543 Debug.println(DebugLevel.Verbose, "PulseAudioMixer.close(): " 544 + "Mixer closed"); 545 546 } 547 548 @Override 549 public Control getControl(Type control) { 550 // mixer supports no controls 551 throw new IllegalArgumentException(); 552 } 553 554 @Override 555 public Control[] getControls() { 556 // mixer supports no controls; return an array of length 0 557 return new Control[] {}; 558 } 559 560 @Override 561 public javax.sound.sampled.Line.Info getLineInfo() { 562 // System.out.println("DEBUG: PulseAudioMixer.getLineInfo() called"); 563 return new Line.Info(PulseAudioMixer.class); 564 } 565 566 @Override 567 public boolean isControlSupported(Type control) { 568 // mixer supports no controls 569 return false; 570 } 571 572 @Override 573 public boolean isOpen() { 574 return isOpen; 575 } 576 577 @Override 578 public void open() throws LineUnavailableException { 579 openLocal(); 580 581 } 582 583 public void openLocal() throws LineUnavailableException { 584 openLocal(DEFAULT_APP_NAME); 585 } 586 587 public void openLocal(String appName) throws LineUnavailableException { 588 openImpl(appName, null); 589 } 590 591 public void openRemote(String appName, String host) 592 throws UnknownHostException, LineUnavailableException { 593 if (host == null) { 594 throw new NullPointerException("hostname"); 595 } 596 597 final int PULSEAUDIO_DEFAULT_PORT = 4713; 598 599 /* 600 * If trying to connect to a remote machine, check for permissions 601 */ 602 SecurityManager sm = System.getSecurityManager(); 603 if (sm != null) { 604 sm.checkConnect(host,PULSEAUDIO_DEFAULT_PORT ); 605 } 606 607 openImpl(appName, host); 608 } 609 610 public void openRemote(String appName, String host, int port) 611 throws UnknownHostException, LineUnavailableException { 612 613 if ((port < 1) && (port != -1)) { 614 throw new IllegalArgumentException("Invalid value for port"); 615 } 616 617 if (host == null) { 618 throw new NullPointerException("hostname"); 619 } 620 621 /* 622 * If trying to connect to a remote machine, check for permissions 623 */ 624 SecurityManager sm = System.getSecurityManager(); 625 if (sm != null) { 626 sm.checkConnect(host, port); 627 } 628 629 InetAddress addr = InetAddress.getAllByName(host)[0]; 630 631 host = addr.getHostAddress(); 632 host = host + ":" + String.valueOf(port); 633 634 openImpl(appName, host); 635 636 } 637 638 /* 639 * 640 * @param appName name of the application 641 * 642 * @param hostAndIp a string consisting of the host and ip address of the 643 * server to connect to. Format: "<host>:<ip>". Set to null to indicate a 644 * local connection 645 */ 646 synchronized private void openImpl(String appName, String hostAndIp) 647 throws LineUnavailableException { 648 649 if (isOpen) { 650 throw new IllegalStateException("Mixer is already open"); 651 } 652 653 EventLoop eventLoop; 654 eventLoop = EventLoop.getEventLoop(); 655 eventLoop.setAppName(appName); 656 eventLoop.setServer(hostAndIp); 657 658 ContextListener generalEventListener = new ContextListener() { 659 @Override 660 public void update(ContextEvent e) { 661 if (e.getType() == ContextEvent.READY) { 662 fireEvent(new LineEvent(PulseAudioMixer.this, 663 LineEvent.Type.OPEN, AudioSystem.NOT_SPECIFIED)); 664 } else if (e.getType() == ContextEvent.FAILED 665 || e.getType() == ContextEvent.TERMINATED) { 666 fireEvent(new LineEvent(PulseAudioMixer.this, 667 LineEvent.Type.CLOSE, AudioSystem.NOT_SPECIFIED)); 668 } 669 } 670 }; 671 672 eventLoop.addContextListener(generalEventListener); 673 674 final Semaphore ready = new Semaphore(0); 675 676 ContextListener initListener = new ContextListener() { 677 678 @Override 679 public void update(ContextEvent e) { 680 if (e.getType() == ContextEvent.READY 681 || e.getType() == ContextEvent.FAILED 682 || e.getType() == ContextEvent.TERMINATED) { 683 ready.release(); 684 } 685 } 686 687 }; 688 689 eventLoop.addContextListener(initListener); 690 691 eventLoopThread = new Thread(eventLoop, "PulseAudio Eventloop Thread"); 692 693 /* 694 * Make the thread exit if by some weird error it is the only thread 695 * running. The application should be able to exit if the main thread 696 * doesn't or can't (perhaps an assert?) do a mixer.close(). 697 */ 698 eventLoopThread.setDaemon(true); 699 eventLoopThread.start(); 700 701 try { 702 // System.out.println("waiting..."); 703 ready.acquire(); 704 if (eventLoop.getStatus() != ContextEvent.READY) { 705 /* 706 * when exiting, wait for the thread to end otherwise we get one 707 * thread that inits the singleton with new data and the old 708 * thread then cleans up the singleton asserts fail all over the 709 * place 710 */ 711 eventLoop.removeContextListener(initListener); 712 eventLoopThread.interrupt(); 713 eventLoopThread.join(); 714 throw new LineUnavailableException(); 715 } 716 eventLoop.removeContextListener(initListener); 717 // System.out.println("got signal"); 718 } catch (InterruptedException e) { 719 System.out.println("PulseAudioMixer: got interrupted while waiting for the EventLoop to initialize"); 720 } 721 722 // System.out.println(this.getClass().getName() + ": ready"); 723 724 this.isOpen = true; 725 726 // sourceLineInfo and targetLineInfo need to be updated with 727 // port infos, which can only be obtained after EventLoop had started 728 729 refreshSourceAndTargetLines(); 730 731 for (String portName : eventLoop.updateSourcePortNameList()) { 732 sourceLineInfos.add(new Port.Info(Port.class, portName, true)); 733 } 734 735 for (String portName : eventLoop.updateTargetPortNameList()) { 736 targetLineInfos.add(new Port.Info(Port.class, portName, false)); 737 } 738 739 Debug.println(DebugLevel.Debug, "PulseAudioMixer.open(): " 740 + "Mixer opened"); 741 742 } 743 744 @Override 745 public void removeLineListener(LineListener listener) { 746 lineListeners.remove(listener); 747 } 748 749 /* 750 * Should this method be synchronized? I had a few reasons, but i forgot 751 * them Pros: - Thread safety? 752 * 753 * Cons: - eventListeners are run from other threads, if those then call 754 * fireEvent while a method is waiting on a listener, this synchronized 755 * block wont be entered: deadlock! 756 */ 757 private void fireEvent(final LineEvent e) { 758 synchronized (lineListeners) { 759 for (LineListener lineListener : lineListeners) { 760 lineListener.update(e); 761 } 762 } 763 } 764 765 void addSourceLine(PulseAudioLine line) { 766 sourceLines.add(line); 767 } 768 769 void removeSourceLine(PulseAudioLine line) { 770 sourceLines.remove(line); 771 } 772 773 void addTargetLine(PulseAudioLine line) { 774 targetLines.add(line); 775 } 776 777 void removeTargetLine(PulseAudioLine line) { 778 targetLines.remove(line); 779 } 780 781 void refreshSourceAndTargetLines() { 782 783 sourceLineInfos.clear(); 784 targetLineInfos.clear(); 785 786 sourceLineInfos.addAll(staticSourceLineInfos); 787 788 targetLineInfos.addAll(staticTargetLineInfos); 789 790 } 791 792}