001/* PulseAudioTargetDataLine.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 javax.sound.sampled.AudioFormat;
041import javax.sound.sampled.AudioPermission;
042import javax.sound.sampled.DataLine;
043import javax.sound.sampled.Line;
044import javax.sound.sampled.LineEvent;
045import javax.sound.sampled.LineUnavailableException;
046import javax.sound.sampled.TargetDataLine;
047
048import org.classpath.icedtea.pulseaudio.Debug.DebugLevel;
049
050public final class PulseAudioTargetDataLine extends PulseAudioDataLine
051        implements TargetDataLine {
052
053    /*
054     * This contains the data from the PulseAudio buffer that has since been
055     * dropped. If 20 bytes of a fragment of size 200 are read, the other 180
056     * are dumped in this
057     */
058    private byte[] fragmentBuffer;
059
060    /*
061     * these are set to true only by the respective functions (flush(), drain())
062     * set to false only by read()
063     */
064    private boolean flushed = false;
065    private boolean drained = false;
066
067    public static final String DEFAULT_TARGETDATALINE_NAME = "Audio Stream";
068
069    PulseAudioTargetDataLine(AudioFormat[] formats, AudioFormat defaultFormat) {
070        this.supportedFormats = formats;
071        this.defaultFormat = defaultFormat;
072        this.currentFormat = defaultFormat;
073        this.streamName = DEFAULT_TARGETDATALINE_NAME;
074
075    }
076
077    @Override
078    synchronized public void close() {
079        if (!isOpen()) {
080            // Probably due to some programmer error, we are being
081            // asked to close an already closed line.  Oh well.
082            Debug.println(DebugLevel.Verbose,
083                    "PulseAudioTargetDataLine.close(): "
084                    + "Line closed that wasn't open.");
085            return;
086        }
087
088        /* check for permission to record audio */
089        AudioPermission perm = new AudioPermission("record", null);
090        perm.checkGuard(null);
091
092        PulseAudioMixer parentMixer = PulseAudioMixer.getInstance();
093        parentMixer.removeTargetLine(this);
094
095        super.close();
096
097        Debug.println(DebugLevel.Verbose, "PulseAudioTargetDataLine.close(): "
098                + "Line closed");
099    }
100
101    @Override
102    synchronized public void open(AudioFormat format, int bufferSize)
103            throws LineUnavailableException {
104        /* check for permission to record audio */
105        AudioPermission perm = new AudioPermission("record", null);
106        perm.checkGuard(null);
107
108        if (isOpen()) {
109            throw new IllegalStateException("already open");
110        }
111        super.open(format, bufferSize);
112
113        /* initialize all the member variables */
114        framesSinceOpen = 0;
115        fragmentBuffer = null;
116        flushed = false;
117        drained = false;
118
119        /* add this open line to the mixer */
120        PulseAudioMixer parentMixer = PulseAudioMixer.getInstance();
121        parentMixer.addTargetLine(this);
122
123        Debug.println(DebugLevel.Verbose, "PulseAudioTargetDataLine.open(): "
124                + "Line opened");
125    }
126
127    @Override
128    synchronized public void open(AudioFormat format)
129            throws LineUnavailableException {
130        open(format, DEFAULT_BUFFER_SIZE);
131    }
132
133    @Override
134    protected void connectLine(int bufferSize, Stream masterStream)
135            throws LineUnavailableException {
136        int fs = currentFormat.getFrameSize();
137        float fr = currentFormat.getFrameRate();
138        int bps = (int)(fs*fr); // bytes per second.
139
140        // if 2 seconds' worth of data can fit in the buffer of the specified
141        // size, we don't have to adjust the latency. Otherwise we do, so as
142        // to avoid overruns.
143        long flags = Stream.FLAG_START_CORKED;
144        StreamBufferAttributes bufferAttributes;
145        if (bps*2 < bufferSize) {
146            // pulse audio completely ignores our fragmentSize attribute unless
147            // ADJUST_LATENCY is set, so we just leave it at -1.
148            bufferAttributes = new StreamBufferAttributes(bufferSize, -1, -1, -1, -1);
149        } else {
150            flags |= Stream.FLAG_ADJUST_LATENCY;
151            // in this case, the pulse audio docs:
152            // http://www.pulseaudio.org/wiki/LatencyControl
153            // say every field (including bufferSize) must be initialized
154            // to -1 except fragmentSize.
155            // XXX: but in my tests, it just sets it to about 4MB, which
156            // effectively makes it impossible to allocate a small buffer
157            // and nothing bad happens (yet) when you don't set it to -1
158            // so we just leave it at bufferSize.
159            // XXX: the java api has no way to specify latency, which probably
160            // means it should be as low as possible. Right now this method's
161            // primary concern is avoiding dropouts, and if the user-provided
162            // buffer size is large enough, we leave the latency up to pulse
163            // audio (which sets it to something extremely high - about 2
164            // seconds). We might want to always set a low latency.
165            int fragmentSize = bufferSize/2;
166            fragmentSize = Math.max((fragmentSize/fs)*fs, fs);
167            bufferAttributes = new StreamBufferAttributes(bufferSize, -1, -1, -1, fragmentSize);
168        }
169
170        synchronized (eventLoop.threadLock) {
171            stream.connectForRecording(Stream.DEFAULT_DEVICE, flags, bufferAttributes);
172        }
173    }
174
175    @Override
176    public int read(byte[] data, int offset, int length) {
177
178        /* check state and inputs */
179
180        if (!isOpen()) {
181            // A closed line can produce zero bytes of data.
182            return 0;
183        }
184
185        int frameSize = currentFormat.getFrameSize();
186
187        if (length % frameSize != 0) {
188            throw new IllegalArgumentException(
189                    "amount of data to read does not represent an integral number of frames");
190        }
191
192        if (length < 0) {
193            throw new IllegalArgumentException("length is negative");
194        }
195
196        if ( offset < 0 || offset > data.length - length) {
197            throw new ArrayIndexOutOfBoundsException("array size: " + data.length
198                    + " offset:" + offset + " length:" + length );
199        }
200
201        /* everything ok */
202
203        int position = offset;
204        int remainingLength = length;
205        int sizeRead = 0;
206
207        /* bytes read on each iteration of loop */
208        int bytesRead;
209
210        flushed = false;
211        drained = false;
212
213        /*
214         * to read, we first take stuff from the fragmentBuffer
215         */
216
217        /* on first read() of the line, fragmentBuffer is null */
218        synchronized (this) {
219            if (fragmentBuffer != null) {
220                boolean fragmentBufferSmaller = fragmentBuffer.length < length;
221                int smallerBufferLength = Math.min(fragmentBuffer.length,
222                        length);
223                System.arraycopy(fragmentBuffer, 0, data, position,
224                        smallerBufferLength);
225                framesSinceOpen += smallerBufferLength
226                        / currentFormat.getFrameSize();
227
228                if (!fragmentBufferSmaller) {
229                    /*
230                     * if fragment was larger, then we already have all the data
231                     * we need. clean up the buffer before returning. Make a new
232                     * fragmentBuffer from the remaining bytes
233                     */
234                    int remainingBytesInFragment = (fragmentBuffer.length - length);
235                    byte[] newFragmentBuffer = new byte[remainingBytesInFragment];
236                    System.arraycopy(fragmentBuffer, length, newFragmentBuffer,
237                            0, newFragmentBuffer.length);
238                    fragmentBuffer = newFragmentBuffer;
239                    return length;
240                }
241
242                /* done with fragment buffer, remove it */
243                bytesRead = smallerBufferLength;
244                sizeRead += bytesRead;
245                position += bytesRead;
246                remainingLength -= bytesRead;
247                fragmentBuffer = null;
248            }
249        }
250
251        /*
252         * if we need to read more data, then we read from PulseAudio's buffer
253         */
254        while (remainingLength != 0) {
255            synchronized (this) {
256
257                if (!isOpen() || !isStarted) {
258                    return sizeRead;
259                }
260
261                if (flushed) {
262                    flushed = false;
263                    return sizeRead;
264                }
265
266                if (drained) {
267                    drained = false;
268                    return sizeRead;
269                }
270
271                byte[] currentFragment;
272                synchronized (eventLoop.threadLock) {
273
274                    /* read a fragment, and drop it from the server */
275                    currentFragment = stream.peek();
276
277                    stream.drop();
278                    if (currentFragment == null) {
279                        Debug.println(DebugLevel.Verbose,
280                                "PulseAudioTargetDataLine.read(): "
281                                        + " error in stream.peek()");
282                        continue;
283                    }
284
285                    bytesRead = Math.min(currentFragment.length,
286                            remainingLength);
287
288                    /*
289                     * we read more than we required, save the rest of the data
290                     * in the fragmentBuffer
291                     */
292                    if (bytesRead < currentFragment.length) {
293                        /* allocate a buffer to store unsaved data */
294                        fragmentBuffer = new byte[currentFragment.length
295                                - bytesRead];
296
297                        /* copy over the unsaved data */
298                        System.arraycopy(currentFragment, bytesRead,
299                                fragmentBuffer, 0, currentFragment.length
300                                        - bytesRead);
301                    }
302
303                    System.arraycopy(currentFragment, 0, data, position,
304                            bytesRead);
305
306                    sizeRead += bytesRead;
307                    position += bytesRead;
308                    remainingLength -= bytesRead;
309                    framesSinceOpen += bytesRead / currentFormat.getFrameSize();
310                }
311            }
312        }
313
314        // all the data should have been played by now
315        assert (sizeRead == length);
316
317        return sizeRead;
318
319    }
320
321    @Override
322    public void drain() {
323
324        // blocks when there is data on the line
325        // http://www.jsresources.org/faq_audio.html#stop_drain_tdl
326        while (true) {
327            synchronized (this) {
328                if (!isStarted || !isOpen()) {
329                    break;
330                }
331            }
332            try {
333                //TODO: Is this the best length of sleep?
334                //Maybe in case this loop runs for a long time
335                //it would be good to switch to a longer
336                //sleep.  Like bump it up each iteration after
337                //the Nth iteration, up to a MAXSLEEP length.
338                Thread.sleep(100);
339            } catch (InterruptedException e) {
340                // do nothing
341            }
342        }
343
344        synchronized (this) {
345            drained = true;
346        }
347    }
348
349    @Override
350    public synchronized void flush() {
351        if (isOpen()) {
352
353            /* flush the buffer on pulseaudio's side */
354            Operation operation;
355            synchronized (eventLoop.threadLock) {
356                operation = stream.flush();
357            }
358            operation.waitForCompletion();
359            operation.releaseReference();
360        }
361
362        flushed = true;
363        /* flush the partial fragment we stored */
364        fragmentBuffer = null;
365    }
366
367    @Override
368    public int available() {
369        if (!isOpen()) {
370            // a closed line has 0 bytes available.
371            return 0;
372        }
373
374        synchronized (eventLoop.threadLock) {
375            return stream.getReableSize();
376        }
377    }
378
379    @Override
380    public int getFramePosition() {
381        return (int) framesSinceOpen;
382    }
383
384    @Override
385    public long getLongFramePosition() {
386        return framesSinceOpen;
387    }
388
389    @Override
390    public long getMicrosecondPosition() {
391        return (long) (framesSinceOpen / currentFormat.getFrameRate());
392    }
393
394    /*
395     * A TargetData starts when we ask it to and continues playing until we ask
396     * it to stop. There are no buffer underruns/overflows or anything so we
397     * will just fire the LineEvents manually
398     */
399
400    @Override
401    synchronized public void start() {
402        super.start();
403
404        fireLineEvent(new LineEvent(this, LineEvent.Type.START, framesSinceOpen));
405    }
406
407    @Override
408    synchronized public void stop() {
409        super.stop();
410
411        fireLineEvent(new LineEvent(this, LineEvent.Type.STOP, framesSinceOpen));
412    }
413
414    @Override
415    public Line.Info getLineInfo() {
416        return new DataLine.Info(TargetDataLine.class, supportedFormats,
417                StreamBufferAttributes.MIN_VALUE,
418                StreamBufferAttributes.MAX_VALUE);
419    }
420
421}