/*
This file is part of jpcsp.
Jpcsp 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.
Jpcsp 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 Jpcsp. If not, see <http://www.gnu.org/licenses/>.
*/
package jpcsp.sound;
import java.nio.ByteBuffer;
import jpcsp.HLE.modules.sceAudio;
import org.apache.log4j.Logger;
import org.lwjgl.LWJGLException;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.AL11;
public class SoundChannel {
private static Logger log = sceAudio.log;
private static volatile boolean isExit = false;
public static final int FORMAT_MONO = 0x10;
public static final int FORMAT_STEREO = 0x00;
//
// The PSP is using a buffer equal to the sampleSize.
// However, the audio data is not always streamed as fast on Jpcsp as on
// a real PSP which can lead to buffer underflows,
// causing discontinuities in the audio that are perceived as "clicks".
//
// So, we allocate several buffers of sampleSize, just enough to store
// a given amount of audio time.
// This has the disadvantage to introduce a small delay when playing
// a new sound: a PSP application is typically sending continuously
// sound data, even when nothing can be heard ("0" values are sent).
// And we have first to play these buffered blanks before hearing
// the real sound itself.
// E.g. BUFFER_SIZE_IN_MILLIS = 100 gives a 0.1 second delay.
private static final int BUFFER_SIZE_IN_MILLIS = 100;
public static final int MAX_VOLUME = 0x8000;
private static final int DEFAULT_VOLUME = MAX_VOLUME;
private static final int DEFAULT_SAMPLE_RATE = 44100;
private SoundBufferManager soundBufferManager;
private int index;
private boolean reserved;
private int leftVolume;
private int rightVolume;
private int alSource;
private int sampleRate;
private int sampleLength;
private int format;
private int numberBlockingBuffers;
private int minimumNumberBuffers;
private boolean busy;
public static void init() {
if (!AL.isCreated()) {
try {
AL.create();
isExit = false;
} catch (LWJGLException e) {
log.error(e);
}
}
}
public static void exit() {
if (AL.isCreated()) {
isExit = true;
AL.destroy();
}
}
public SoundChannel(int index) {
soundBufferManager = SoundBufferManager.getInstance();
this.index = index;
reserved = false;
leftVolume = DEFAULT_VOLUME;
rightVolume = DEFAULT_VOLUME;
alSource = AL10.alGenSources();
sampleRate = DEFAULT_SAMPLE_RATE;
updateNumberBlockingBuffers();
AL10.alSourcei(alSource, AL10.AL_LOOPING, AL10.AL_FALSE);
}
private void updateNumberBlockingBuffers() {
if (getSampleLength() > 0) {
// Compute the number of buffers required to store the required
// amount of audio time
float bufferSizeInSamples = getSampleRate() * BUFFER_SIZE_IN_MILLIS / 1000.f;
numberBlockingBuffers = Math.round(bufferSizeInSamples / getSampleLength());
}
// At least 1 blocking buffer
numberBlockingBuffers = Math.max(numberBlockingBuffers, 1);
// For very small sample length, wait for a minimum number of buffers
// before starting playing the audio otherwise, small cracks can be produced.
if (getSampleLength() <= 0x40) {
minimumNumberBuffers = 10;
} else {
minimumNumberBuffers = 0;
}
}
public int getIndex() {
return index;
}
public boolean isReserved() {
return reserved;
}
public void setReserved(boolean reserved) {
this.reserved = reserved;
}
public int getLeftVolume() {
return leftVolume;
}
public void setLeftVolume(int leftVolume) {
this.leftVolume = leftVolume;
}
public int getRightVolume() {
return rightVolume;
}
public void setRightVolume(int rightVolume) {
this.rightVolume = rightVolume;
}
public void setVolume(int volume) {
setLeftVolume(volume);
setRightVolume(volume);
}
public int getSampleLength() {
return sampleLength;
}
public void setSampleLength(int sampleLength) {
if (this.sampleLength != sampleLength) {
this.sampleLength = sampleLength;
updateNumberBlockingBuffers();
}
}
public int getFormat() {
return format;
}
public void setFormat(int format) {
this.format = format;
}
public boolean isFormatStereo() {
return (format & FORMAT_MONO) == FORMAT_STEREO;
}
public boolean isFormatMono() {
return (format & FORMAT_MONO) == FORMAT_MONO;
}
public int getSampleRate() {
return sampleRate;
}
public void setSampleRate(int sampleRate) {
if (this.sampleRate != sampleRate) {
this.sampleRate = sampleRate;
updateNumberBlockingBuffers();
}
}
private void alSourcePlay() {
int state = AL10.alGetSourcei(alSource, AL10.AL_SOURCE_STATE);
if (state != AL10.AL_PLAYING) {
if (minimumNumberBuffers <= 0 || getWaitingBuffers() >= minimumNumberBuffers) {
AL10.alSourcePlay(alSource);
}
}
}
private void alSourceQueueBuffer(byte[] buffer) {
int alBuffer = soundBufferManager.getBuffer();
ByteBuffer directBuffer = soundBufferManager.getDirectBuffer(buffer.length);
directBuffer.clear();
directBuffer.limit(buffer.length);
directBuffer.put(buffer);
directBuffer.rewind();
int alFormat = isFormatStereo() ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_MONO16;
AL10.alBufferData(alBuffer, alFormat, directBuffer, getSampleRate());
AL10.alSourceQueueBuffers(alSource, alBuffer);
soundBufferManager.releaseDirectBuffer(directBuffer);
alSourcePlay();
checkFreeBuffers();
if (log.isDebugEnabled()) {
log.debug(String.format("alSourceQueueBuffer buffer=%d, %s", alBuffer, toString()));
}
}
public void checkFreeBuffers() {
soundBufferManager.checkFreeBuffers(alSource);
}
public void release() {
AL10.alSourceStop(alSource);
checkFreeBuffers();
}
public void play(byte[] buffer) {
alSourceQueueBuffer(buffer);
}
private int getWaitingBuffers() {
checkFreeBuffers();
return AL10.alGetSourcei(alSource, AL10.AL_BUFFERS_QUEUED);
}
private int getSourceSampleOffset() {
int sampleOffset = AL10.alGetSourcei(alSource, AL11.AL_SAMPLE_OFFSET);
if (isFormatStereo()) {
sampleOffset /= 2;
}
return sampleOffset;
}
public boolean isOutputBlocking() {
if (isExit) {
return true;
}
return getWaitingBuffers() >= numberBlockingBuffers;
}
public boolean isDrained() {
if (isEnded()) {
return true;
}
if (getWaitingBuffers() > 1) {
return false;
}
return true;
}
public int getUnblockOutputDelayMicros(boolean waitForCompleteDrain) {
// Return the delay required for the processing of the playing buffer
if (isExit || isEnded()) {
return 0;
}
int samples;
if (waitForCompleteDrain) {
samples = getDrainLength();
} else {
samples = getSampleLength() - getSourceSampleOffset();
}
float delaySecs = samples / (float) getSampleRate();
int delayMicros = (int) (delaySecs * 1000000);
return delayMicros;
}
public int getDrainLength() {
int waitingBuffers = getWaitingBuffers();
if (waitingBuffers > 0) {
// getWaitingBuffers also returns the currently playing buffer,
// do not count it
waitingBuffers--;
}
int restLength = waitingBuffers * getSampleLength();
return restLength;
}
public int getRestLength() {
int restLength = getDrainLength();
if (!isEnded()) {
restLength += getSampleLength() - getSourceSampleOffset();
}
return restLength;
}
public boolean isEnded() {
checkFreeBuffers();
int state = AL10.alGetSourcei(alSource, AL10.AL_SOURCE_STATE);
if (state == AL10.AL_PLAYING) {
return false;
}
return true;
}
public static short adjustSample(short sample, int volume) {
return (short) ((((int) sample) * volume) >> 15);
}
public static void storeSample(short sample, byte[] data, int index) {
data[index] = (byte) sample;
data[index + 1] = (byte) (sample >> 8);
}
public boolean isBusy() {
return busy;
}
public void setBusy(boolean busy) {
this.busy = busy;
}
@Override
public String toString() {
StringBuilder s = new StringBuilder();
s.append(String.format("SoundChannel[%d](", index));
if (!isExit) {
s.append(String.format("sourceSampleOffset=%d", getSourceSampleOffset()));
s.append(String.format(", restLength=%d", getRestLength()));
s.append(String.format(", buffers queued=%d", getWaitingBuffers()));
s.append(String.format(", isOutputBlock=%b", isOutputBlocking()));
s.append(String.format(", %s", isFormatStereo() ? "Stereo" : "Mono"));
s.append(String.format(", reserved=%b", reserved));
s.append(String.format(", sampleLength=%d", getSampleLength()));
s.append(String.format(", sampleRate=%d", getSampleRate()));
s.append(String.format(", busy=%b", busy));
}
s.append(")");
return s.toString();
}
}