blob: 6f8b8ffdd0db7218ca49776f73762765b9e534fe [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hyphonate.megaaudio.player;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.util.Log;
import org.hyphonate.megaaudio.common.StreamBase;
/**
* Implementation of abstract Player class implemented for the Android Java-based audio playback
* API, i.e. AudioTrack.
*/
public class JavaPlayer extends Player {
@SuppressWarnings("unused") private static String TAG = JavaPlayer.class.getSimpleName();
@SuppressWarnings("unused") private static final boolean LOG = false;
/*
* Player infrastructure
*/
/* The AudioTrack for playing the audio stream */
private AudioTrack mAudioTrack;
private AudioSource mAudioSource;
// Playback state
/** <code>true</code> if currently playing audio data */
private boolean mPlaying;
/*
* Data buffers
*/
/** Number of FRAMES of audio data in a burst buffer */
private int mNumBufferFrames;
/** The Burst Buffer. This is the buffer we fill with audio and feed into the AudioTrack. */
private float[] mAudioBuffer;
// Player-specific extension
public AudioTrack getAudioTrack() { return mAudioTrack; }
public JavaPlayer(AudioSourceProvider sourceProvider) {
super(sourceProvider);
mNumBufferFrames = -1; // TODO need error defines
}
//
// Status
//
public boolean isPlaying() {
return mPlaying;
}
/**
* Allocates the array for the burst buffer.
*/
private void allocBurstBuffer() {
// pad it by 1 frame. This allows some sources to not have to worry about
// handling the end-of-buffer edge case. i.e. a "Guard Point" for interpolation.
mAudioBuffer = new float[(mNumBufferFrames + 1) * mChannelCount];
}
//
// Attributes
//
/**
* @return The number of frames of audio data contained in the internal buffer.
*/
@Override
public int getNumBufferFrames() {
return mNumBufferFrames;
}
@Override
public int getRoutedDeviceId() {
if (mAudioTrack != null) {
AudioDeviceInfo routedDevice = mAudioTrack.getRoutedDevice();
return routedDevice != null ? routedDevice.getId() : ROUTED_DEVICE_ID_INVALID;
} else {
return ROUTED_DEVICE_ID_INVALID;
}
}
/*
* State
*/
@Override
public int setupStream(int channelCount, int sampleRate, int numBufferFrames) {
if (LOG) {
Log.i(TAG, "setupStream(chans:" + channelCount + ", rate:" + sampleRate +
", frames:" + numBufferFrames);
}
mChannelCount = channelCount;
mSampleRate = sampleRate;
mNumBufferFrames = numBufferFrames;
mAudioSource = mSourceProvider.getJavaSource();
mAudioSource.init(mNumBufferFrames, mChannelCount);
try {
int bufferSizeInBytes = mNumBufferFrames * mChannelCount
* sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT);
mAudioTrack = new AudioTrack.Builder()
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
.setSampleRate(mSampleRate)
.setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount))
// .setChannelMask(channelMask)
.build())
.setBufferSizeInBytes(bufferSizeInBytes)
.build();
allocBurstBuffer();
mAudioTrack.setPreferredDevice(mRouteDevice);
} catch (UnsupportedOperationException ex) {
if (LOG) {
Log.i(TAG, "Couldn't open AudioTrack: " + ex);
}
mAudioTrack = null;
return ERROR_UNSUPPORTED;
}
return OK;
}
@Override
public int teardownStream() {
stopStream();
waitForStreamThreadToExit();
if (mAudioTrack != null) {
mAudioTrack.release();
mAudioTrack = null;
}
mChannelCount = 0;
mSampleRate = 0;
//TODO - Retrieve errors from above
return OK;
}
/**
* Allocates the underlying AudioTrack and begins Playback.
* @return True if the stream is successfully started.
*
* This method returns when the start operation is complete, but before the first
* call to the AudioSource.pull() method.
*/
@Override
public int startStream() {
if (mAudioTrack == null) {
return ERROR_INVALID_STATE;
}
waitForStreamThreadToExit(); // just to be sure.
mStreamThread = new Thread(new StreamPlayerRunnable(), "StreamPlayer Thread");
mPlaying = true;
mStreamThread.start();
return OK;
}
/**
* Marks the stream for stopping on the next callback from the underlying system.
*
* Returns immediately, though a call to AudioSource.pull() may be in progress.
*/
@Override
public int stopStream() {
mPlaying = false;
return OK;
}
//
// StreamPlayerRunnable
//
/**
* Implements the <code>run</code> method for the playback thread.
* Gets initial audio data and starts the AudioTrack. Then continuously provides audio data
* until the flag <code>mPlaying</code> is set to false (in the stop() method).
*/
private class StreamPlayerRunnable implements Runnable {
@Override
public void run() {
final int numBufferSamples = mNumBufferFrames * mChannelCount;
mAudioTrack.play();
while (mPlaying) {
mAudioSource.pull(mAudioBuffer, mNumBufferFrames, mChannelCount);
int numSamplesWritten = mAudioTrack.write(
mAudioBuffer,0, numBufferSamples, AudioTrack.WRITE_BLOCKING);
if (numSamplesWritten < 0) {
// error
if (LOG) {
Log.i(TAG, "AudioTrack write error: " + numSamplesWritten);
}
stopStream();
} else if (numSamplesWritten < numBufferSamples) {
// end of stream
if (LOG) {
Log.i(TAG, "Stream Complete.");
}
stopStream();
}
}
}
}
}