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}