|
|
|
/*
|
|
|
|
This file is part of Airsonic.
|
|
|
|
|
|
|
|
Airsonic is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Airsonic is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
Copyright 2016 (C) Airsonic Authors
|
|
|
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
|
|
|
*/
|
|
|
|
package org.airsonic.player.service.jukebox;
|
|
|
|
|
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
|
|
import javax.sound.sampled.AudioFormat;
|
|
|
|
import javax.sound.sampled.AudioSystem;
|
|
|
|
import javax.sound.sampled.FloatControl;
|
|
|
|
import javax.sound.sampled.SourceDataLine;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
|
|
|
|
import static org.airsonic.player.service.jukebox.AudioPlayer.State.*;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A simple wrapper for playing sound from an input stream.
|
|
|
|
* <p/>
|
|
|
|
* Supports pause and resume, but not restarting.
|
|
|
|
*
|
|
|
|
* @author Sindre Mehus
|
|
|
|
* @version $Id$
|
|
|
|
*/
|
|
|
|
public class AudioPlayer {
|
|
|
|
|
|
|
|
public static final float DEFAULT_GAIN = 0.75f;
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(AudioPlayer.class);
|
|
|
|
|
|
|
|
private final InputStream in;
|
|
|
|
private final Listener listener;
|
|
|
|
private final SourceDataLine line;
|
|
|
|
private final AtomicReference<State> state = new AtomicReference<State>(PAUSED);
|
|
|
|
private FloatControl gainControl;
|
|
|
|
|
|
|
|
public AudioPlayer(InputStream in, Listener listener) throws Exception {
|
|
|
|
this.in = in;
|
|
|
|
this.listener = listener;
|
|
|
|
|
|
|
|
AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100.0F, 16, 2, 4, 44100.0F, true);
|
|
|
|
line = AudioSystem.getSourceDataLine(format);
|
|
|
|
line.open(format);
|
|
|
|
LOG.debug("Opened line " + line);
|
|
|
|
|
|
|
|
if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
|
|
|
|
gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
|
|
|
|
setGain(DEFAULT_GAIN);
|
|
|
|
}
|
|
|
|
new AudioDataWriter();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts (or resumes) the player. This only has effect if the current state is
|
|
|
|
* {@link State#PAUSED}.
|
|
|
|
*/
|
|
|
|
public synchronized void play() {
|
|
|
|
if (state.get() == PAUSED) {
|
|
|
|
line.start();
|
|
|
|
setState(PLAYING);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pauses the player. This only has effect if the current state is
|
|
|
|
* {@link State#PLAYING}.
|
|
|
|
*/
|
|
|
|
public synchronized void pause() {
|
|
|
|
if (state.get() == PLAYING) {
|
|
|
|
setState(PAUSED);
|
|
|
|
line.stop();
|
|
|
|
line.flush();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the player, releasing all resources. After this the player state is
|
|
|
|
* {@link State#CLOSED} (unless the current state is {@link State#EOM}).
|
|
|
|
*/
|
|
|
|
public synchronized void close() {
|
|
|
|
if (state.get() != CLOSED && state.get() != EOM) {
|
|
|
|
setState(CLOSED);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
line.stop();
|
|
|
|
} catch (Throwable x) {
|
|
|
|
LOG.warn("Failed to stop player: " + x, x);
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
if (line.isOpen()) {
|
|
|
|
line.close();
|
|
|
|
LOG.debug("Closed line " + line);
|
|
|
|
}
|
|
|
|
} catch (Throwable x) {
|
|
|
|
LOG.warn("Failed to close player: " + x, x);
|
|
|
|
}
|
|
|
|
IOUtils.closeQuietly(in);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the player state.
|
|
|
|
*/
|
|
|
|
public State getState() {
|
|
|
|
return state.get();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the gain.
|
|
|
|
*
|
|
|
|
* @param gain The gain between 0.0 and 1.0.
|
|
|
|
*/
|
|
|
|
public void setGain(float gain) {
|
|
|
|
if (gainControl != null) {
|
|
|
|
|
|
|
|
double minGainDB = gainControl.getMinimum();
|
|
|
|
double maxGainDB = Math.min(0.0, gainControl.getMaximum()); // Don't use positive gain to avoid distortion.
|
|
|
|
double ampGainDB = 0.5f * maxGainDB - minGainDB;
|
|
|
|
double cste = Math.log(10.0) / 20;
|
|
|
|
double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * gain);
|
|
|
|
|
|
|
|
valueDB = Math.min(valueDB, maxGainDB);
|
|
|
|
valueDB = Math.max(valueDB, minGainDB);
|
|
|
|
|
|
|
|
gainControl.setValue((float) valueDB);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the position in seconds.
|
|
|
|
*/
|
|
|
|
public int getPosition() {
|
|
|
|
return (int) (line.getMicrosecondPosition() / 1000000L);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void setState(State state) {
|
|
|
|
if (this.state.getAndSet(state) != state && listener != null) {
|
|
|
|
listener.stateChanged(this, state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class AudioDataWriter implements Runnable {
|
|
|
|
|
|
|
|
public AudioDataWriter() {
|
|
|
|
new Thread(this).start();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void run() {
|
|
|
|
try {
|
|
|
|
byte[] buffer = new byte[line.getBufferSize()];
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
|
|
switch (state.get()) {
|
|
|
|
case CLOSED:
|
|
|
|
case EOM:
|
|
|
|
return;
|
|
|
|
case PAUSED:
|
|
|
|
Thread.sleep(250);
|
|
|
|
break;
|
|
|
|
case PLAYING:
|
|
|
|
// Fill buffer in order to ensure that write() receives an integral number of frames.
|
|
|
|
int n = fill(buffer);
|
|
|
|
if (n == -1) {
|
|
|
|
setState(EOM);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
line.write(buffer, 0, n);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Throwable x) {
|
|
|
|
LOG.warn("Error when copying audio data: " + x, x);
|
|
|
|
} finally {
|
|
|
|
close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private int fill(byte[] buffer) throws IOException {
|
|
|
|
int bytesRead = 0;
|
|
|
|
while (bytesRead < buffer.length) {
|
|
|
|
int n = in.read(buffer, bytesRead, buffer.length - bytesRead);
|
|
|
|
if (n == -1) {
|
|
|
|
return bytesRead == 0 ? -1 : bytesRead;
|
|
|
|
}
|
|
|
|
bytesRead += n;
|
|
|
|
}
|
|
|
|
return bytesRead;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public interface Listener {
|
|
|
|
void stateChanged(AudioPlayer player, State state);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static enum State {
|
|
|
|
PAUSED,
|
|
|
|
PLAYING,
|
|
|
|
CLOSED,
|
|
|
|
EOM
|
|
|
|
}
|
|
|
|
}
|