mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2025-07-21 04:05:45 +00:00
356 lines
12 KiB
C++
356 lines
12 KiB
C++
/*
|
|
* Copyright 2023 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.
|
|
*/
|
|
|
|
#ifndef OBOE_FULL_DUPLEX_STREAM_
|
|
#define OBOE_FULL_DUPLEX_STREAM_
|
|
|
|
#include <cstdint>
|
|
#include "oboe/Definitions.h"
|
|
#include "oboe/AudioStream.h"
|
|
#include "oboe/AudioStreamCallback.h"
|
|
|
|
namespace oboe {
|
|
|
|
/**
|
|
* FullDuplexStream can be used to synchronize an input and output stream.
|
|
*
|
|
* For the builder of the output stream, call setDataCallback() with this object.
|
|
*
|
|
* When both streams are ready, onAudioReady() of the output stream will call onBothStreamsReady().
|
|
* Callers must override onBothStreamsReady().
|
|
*
|
|
* To ensure best results, open an output stream before the input stream.
|
|
* Call inputBuilder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2).
|
|
* Also, call inputBuilder.setSampleRate(mOutputStream->getSampleRate()).
|
|
*
|
|
* Callers must call setInputStream() and setOutputStream().
|
|
* Call start() to start both streams and stop() to stop both streams.
|
|
* Caller is responsible for closing both streams.
|
|
*
|
|
* Callers should handle error callbacks with setErrorCallback() for the output stream.
|
|
* When an error callback occurs for the output stream, Oboe will stop and close the output stream.
|
|
* The caller is responsible for stopping and closing the input stream.
|
|
* The caller should also reopen and restart both streams when the error callback is ErrorDisconnected.
|
|
* See the LiveEffect sample as an example of this.
|
|
*
|
|
*/
|
|
class FullDuplexStream : public AudioStreamDataCallback {
|
|
public:
|
|
FullDuplexStream() {}
|
|
virtual ~FullDuplexStream() = default;
|
|
|
|
/**
|
|
* Sets the input stream.
|
|
*
|
|
* @deprecated Call setSharedInputStream(std::shared_ptr<AudioStream> &stream) instead.
|
|
* @param stream the output stream
|
|
*/
|
|
void setInputStream(AudioStream *stream) {
|
|
mRawInputStream = stream;
|
|
}
|
|
|
|
/**
|
|
* Sets the input stream. Calling this is mandatory.
|
|
*
|
|
* @param stream the output stream
|
|
*/
|
|
void setSharedInputStream(std::shared_ptr<AudioStream> &stream) {
|
|
mSharedInputStream = stream;
|
|
}
|
|
|
|
/**
|
|
* Gets the current input stream. This function tries to return the shared input stream if it
|
|
* is set before the raw input stream.
|
|
*
|
|
* @return pointer to an output stream or nullptr.
|
|
*/
|
|
AudioStream *getInputStream() {
|
|
if (mSharedInputStream) {
|
|
return mSharedInputStream.get();
|
|
} else {
|
|
return mRawInputStream;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the output stream.
|
|
*
|
|
* @deprecated Call setSharedOutputStream(std::shared_ptr<AudioStream> &stream) instead.
|
|
* @param stream the output stream
|
|
*/
|
|
void setOutputStream(AudioStream *stream) {
|
|
mRawOutputStream = stream;
|
|
}
|
|
|
|
/**
|
|
* Sets the output stream. Calling this is mandatory.
|
|
*
|
|
* @param stream the output stream
|
|
*/
|
|
void setSharedOutputStream(std::shared_ptr<AudioStream> &stream) {
|
|
mSharedOutputStream = stream;
|
|
}
|
|
|
|
/**
|
|
* Gets the current output stream. This function tries to return the shared output stream if it
|
|
* is set before the raw output stream.
|
|
*
|
|
* @return pointer to an output stream or nullptr.
|
|
*/
|
|
AudioStream *getOutputStream() {
|
|
if (mSharedOutputStream) {
|
|
return mSharedOutputStream.get();
|
|
} else {
|
|
return mRawOutputStream;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts to start both streams. Please call setInputStream() and setOutputStream() before
|
|
* calling this function.
|
|
*
|
|
* @return result of the operation
|
|
*/
|
|
virtual Result start() {
|
|
mCountCallbacksToDrain = kNumCallbacksToDrain;
|
|
mCountInputBurstsCushion = mNumInputBurstsCushion;
|
|
mCountCallbacksToDiscard = kNumCallbacksToDiscard;
|
|
|
|
// Determine maximum size that could possibly be called.
|
|
int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames()
|
|
* getOutputStream()->getChannelCount();
|
|
if (bufferSize > mBufferSize) {
|
|
mInputBuffer = std::make_unique<float[]>(bufferSize);
|
|
mBufferSize = bufferSize;
|
|
}
|
|
|
|
oboe::Result result = getInputStream()->requestStart();
|
|
if (result != oboe::Result::OK) {
|
|
return result;
|
|
}
|
|
return getOutputStream()->requestStart();
|
|
}
|
|
|
|
/**
|
|
* Stops both streams. Returns Result::OK if neither stream had an error during close.
|
|
*
|
|
* @return result of the operation
|
|
*/
|
|
virtual Result stop() {
|
|
Result outputResult = Result::OK;
|
|
Result inputResult = Result::OK;
|
|
if (getOutputStream()) {
|
|
outputResult = getOutputStream()->requestStop();
|
|
}
|
|
if (getInputStream()) {
|
|
inputResult = getOutputStream()->requestStop();
|
|
}
|
|
if (outputResult != Result::OK) {
|
|
return outputResult;
|
|
} else {
|
|
return inputResult;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads input from the input stream. Callers should not call this directly as this is called
|
|
* in onAudioReady().
|
|
*
|
|
* @param numFrames
|
|
* @return result of the operation
|
|
*/
|
|
virtual ResultWithValue<int32_t> readInput(int32_t numFrames) {
|
|
return getInputStream()->read(mInputBuffer.get(), numFrames, 0 /* timeout */);
|
|
}
|
|
|
|
/**
|
|
* Called when data is available on both streams.
|
|
* Caller should override this method.
|
|
* numInputFrames and numOutputFrames may be zero.
|
|
*
|
|
* @param inputData buffer containing input data
|
|
* @param numInputFrames number of input frames
|
|
* @param outputData a place to put output data
|
|
* @param numOutputFrames number of output frames
|
|
* @return DataCallbackResult::Continue or DataCallbackResult::Stop
|
|
*/
|
|
virtual DataCallbackResult onBothStreamsReady(
|
|
const void *inputData,
|
|
int numInputFrames,
|
|
void *outputData,
|
|
int numOutputFrames
|
|
) = 0;
|
|
|
|
/**
|
|
* Called when the output stream is ready to process audio.
|
|
* This in return calls onBothStreamsReady() when data is available on both streams.
|
|
* Callers should call this function when the output stream is ready.
|
|
* Callers must override onBothStreamsReady().
|
|
*
|
|
* @param audioStream pointer to the associated stream
|
|
* @param audioData a place to put output data
|
|
* @param numFrames number of frames to be processed
|
|
* @return DataCallbackResult::Continue or DataCallbackResult::Stop
|
|
*
|
|
*/
|
|
DataCallbackResult onAudioReady(
|
|
AudioStream * /*audioStream*/,
|
|
void *audioData,
|
|
int numFrames) {
|
|
DataCallbackResult callbackResult = DataCallbackResult::Continue;
|
|
int32_t actualFramesRead = 0;
|
|
|
|
// Silence the output.
|
|
int32_t numBytes = numFrames * getOutputStream()->getBytesPerFrame();
|
|
memset(audioData, 0 /* value */, numBytes);
|
|
|
|
if (mCountCallbacksToDrain > 0) {
|
|
// Drain the input.
|
|
int32_t totalFramesRead = 0;
|
|
do {
|
|
ResultWithValue<int32_t> result = readInput(numFrames);
|
|
if (!result) {
|
|
// Ignore errors because input stream may not be started yet.
|
|
break;
|
|
}
|
|
actualFramesRead = result.value();
|
|
totalFramesRead += actualFramesRead;
|
|
} while (actualFramesRead > 0);
|
|
// Only counts if we actually got some data.
|
|
if (totalFramesRead > 0) {
|
|
mCountCallbacksToDrain--;
|
|
}
|
|
|
|
} else if (mCountInputBurstsCushion > 0) {
|
|
// Let the input fill up a bit so we are not so close to the write pointer.
|
|
mCountInputBurstsCushion--;
|
|
|
|
} else if (mCountCallbacksToDiscard > 0) {
|
|
mCountCallbacksToDiscard--;
|
|
// Ignore. Allow the input to reach to equilibrium with the output.
|
|
ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
|
|
if (!resultAvailable) {
|
|
callbackResult = DataCallbackResult::Stop;
|
|
} else {
|
|
int32_t framesAvailable = resultAvailable.value();
|
|
if (framesAvailable >= mMinimumFramesBeforeRead) {
|
|
ResultWithValue<int32_t> resultRead = readInput(numFrames);
|
|
if (!resultRead) {
|
|
callbackResult = DataCallbackResult::Stop;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
int32_t framesRead = 0;
|
|
ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
|
|
if (!resultAvailable) {
|
|
callbackResult = DataCallbackResult::Stop;
|
|
} else {
|
|
int32_t framesAvailable = resultAvailable.value();
|
|
if (framesAvailable >= mMinimumFramesBeforeRead) {
|
|
// Read data into input buffer.
|
|
ResultWithValue<int32_t> resultRead = readInput(numFrames);
|
|
if (!resultRead) {
|
|
callbackResult = DataCallbackResult::Stop;
|
|
} else {
|
|
framesRead = resultRead.value();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (callbackResult == DataCallbackResult::Continue) {
|
|
callbackResult = onBothStreamsReady(mInputBuffer.get(), framesRead,
|
|
audioData, numFrames);
|
|
}
|
|
}
|
|
|
|
if (callbackResult == DataCallbackResult::Stop) {
|
|
getInputStream()->requestStop();
|
|
}
|
|
|
|
return callbackResult;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* This is a cushion between the DSP and the application processor cursors to prevent collisions.
|
|
* Typically 0 for latency measurements or 1 for glitch tests.
|
|
*
|
|
* @param numBursts number of bursts to leave in the input buffer as a cushion
|
|
*/
|
|
void setNumInputBurstsCushion(int32_t numBursts) {
|
|
mNumInputBurstsCushion = numBursts;
|
|
}
|
|
|
|
/**
|
|
* Get the number of bursts left in the input buffer as a cushion.
|
|
*
|
|
* @return number of bursts in the input buffer as a cushion
|
|
*/
|
|
int32_t getNumInputBurstsCushion() const {
|
|
return mNumInputBurstsCushion;
|
|
}
|
|
|
|
/**
|
|
* Minimum number of frames in the input stream buffer before calling readInput().
|
|
*
|
|
* @param numFrames number of bursts in the input buffer as a cushion
|
|
*/
|
|
void setMinimumFramesBeforeRead(int32_t numFrames) {
|
|
mMinimumFramesBeforeRead = numFrames;
|
|
}
|
|
|
|
/**
|
|
* Gets the minimum number of frames in the input stream buffer before calling readInput().
|
|
*
|
|
* @return minimum number of frames before reading
|
|
*/
|
|
int32_t getMinimumFramesBeforeRead() const {
|
|
return mMinimumFramesBeforeRead;
|
|
}
|
|
|
|
private:
|
|
|
|
// TODO add getters and setters
|
|
static constexpr int32_t kNumCallbacksToDrain = 20;
|
|
static constexpr int32_t kNumCallbacksToDiscard = 30;
|
|
|
|
// let input fill back up, usually 0 or 1
|
|
int32_t mNumInputBurstsCushion = 0;
|
|
int32_t mMinimumFramesBeforeRead = 0;
|
|
|
|
// We want to reach a state where the input buffer is empty and
|
|
// the output buffer is full.
|
|
// These are used in order.
|
|
// Drain several callback so that input is empty.
|
|
int32_t mCountCallbacksToDrain = kNumCallbacksToDrain;
|
|
// Let the input fill back up slightly so we don't run dry.
|
|
int32_t mCountInputBurstsCushion = mNumInputBurstsCushion;
|
|
// Discard some callbacks so the input and output reach equilibrium.
|
|
int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard;
|
|
|
|
AudioStream *mRawInputStream = nullptr;
|
|
AudioStream *mRawOutputStream = nullptr;
|
|
std::shared_ptr<AudioStream> mSharedInputStream;
|
|
std::shared_ptr<AudioStream> mSharedOutputStream;
|
|
|
|
int32_t mBufferSize = 0;
|
|
std::unique_ptr<float[]> mInputBuffer;
|
|
};
|
|
|
|
} // namespace oboe
|
|
|
|
#endif //OBOE_FULL_DUPLEX_STREAM_
|