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}