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}