blob: d2775b2d5a8daf9a6624eb5d4514302170f3a01d [file] [log] [blame]
/**
* Copyright 2018 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.
*/
#include "EchoAudioEngine.h"
#include <audio_common.h>
#include <logging_macros.h>
#include <climits>
#include <assert.h>
/**
* Duplex is not very stable right after starting up:
* the callbacks may not be happening at the right times
* The time to get it stable varies on different systems. Half second
* is used for this sample, during the time this sample plays silence.
*/
const float kSystemWarmupTime = 0.5f;
EchoAudioEngine::EchoAudioEngine() {
assert(outputChannelCount_ == inputChannelCount_);
mixerEffect_ = std::unique_ptr<AudioMixer>(new AudioMixer);
}
EchoAudioEngine::~EchoAudioEngine() {
stopStream(playStream_);
stopStream(recordingStream_);
closeStream(playStream_);
closeStream(recordingStream_);
}
void EchoAudioEngine::setRecordingDeviceId(int32_t deviceId) {
recordingDeviceId_ = deviceId;
}
void EchoAudioEngine::setPlaybackDeviceId(int32_t deviceId) {
playbackDeviceId_ = deviceId;
}
bool EchoAudioEngine::isAAudioSupported() {
oboe::AudioStreamBuilder builder;
return builder.isAAudioSupported();
}
bool EchoAudioEngine::setAudioApi(oboe::AudioApi api) {
if (isEchoOn_)
return false;
audioApi_ = api;
return true;
}
void EchoAudioEngine::setEchoOn(bool isEchoOn) {
if (isEchoOn != isEchoOn_) {
isEchoOn_ = isEchoOn;
if (isEchoOn) {
openAllStreams();
} else {
closeAllStreams();
}
}
}
void EchoAudioEngine::openAllStreams() {
// Note: The order of stream creation is important. We create the playback
// stream first, then use properties from the playback stream
// (e.g. sample rate) to create the recording stream. By matching the
// properties we should get the lowest latency path
openPlaybackStream();
openRecordingStream();
// Now start the recording stream first so that we can read from it during the
// playback stream's dataCallback
if (recordingStream_ && playStream_) {
mixAudio_ = mixerEffect_->AudioFormatSupported(playStream_->getSampleRate(),
playStream_->getChannelCount(), playStream_->getFormat());
startStream(recordingStream_);
startStream(playStream_);
} else {
LOGE("Failed to create recording (%p) and/or playback (%p) stream",
recordingStream_, playStream_);
closeAllStreams();
}
}
/**
* Stops and closes the playback and recording streams.
*/
void EchoAudioEngine::closeAllStreams() {
/**
* Note: The order of events is important here.
* The playback stream must be closed before the recording stream. If the
* recording stream were to be closed first the playback stream's
* callback may attempt to read from the recording stream
* which would cause the app to crash since the recording stream would be
* null.
*/
if (playStream_ != nullptr) {
closeStream(playStream_); // Calling close will also stop the stream
playStream_ = nullptr;
}
if (recordingStream_ != nullptr) {
closeStream(recordingStream_);
recordingStream_ = nullptr;
}
mixAudio_ = false;
}
/**
* Creates an audio stream for recording. The audio device used will depend on
* recordingDeviceId_.
* If the value is set to oboe::Unspecified then the default recording device
* will be used.
*/
void EchoAudioEngine::openRecordingStream() {
// To create a stream we use a stream builder. This allows us to specify all
// the parameters for the stream prior to opening it
oboe::AudioStreamBuilder builder;
setupRecordingStreamParameters(&builder);
// Now that the parameters are set up we can open the stream
oboe::Result result = builder.openStream(&recordingStream_);
if (result == oboe::Result::OK && recordingStream_) {
assert(recordingStream_->getChannelCount() == inputChannelCount_);
assert(recordingStream_->getSampleRate() == sampleRate_);
assert(recordingStream_->getFormat() == oboe::AudioFormat::I16);
warnIfNotLowLatency(recordingStream_);
PrintAudioStreamInfo(recordingStream_);
} else {
LOGE("Failed to create recording stream. Error: %s", convertToText(result));
}
}
/**
* Creates an audio stream for playback. The audio device used will depend on
* playbackDeviceId_.
* If the value is set to oboe::Unspecified then the default playback device
* will be used.
*/
void EchoAudioEngine::openPlaybackStream() {
oboe::AudioStreamBuilder builder;
setupPlaybackStreamParameters(&builder);
oboe::Result result = builder.openStream(&playStream_);
if (result == oboe::Result::OK && playStream_) {
sampleRate_ = playStream_->getSampleRate();
assert(sampleRate_ == kLoopbackSampleRate);
assert(playStream_->getFormat() == oboe::AudioFormat::I16);
assert(outputChannelCount_ == playStream_->getChannelCount());
systemStartupFrames_ = static_cast<uint64_t>
(sampleRate_ * kSystemWarmupTime);
processedFrameCount_ = 0;
framesPerBurst_ = playStream_->getFramesPerBurst();
delayEffect_ = std::unique_ptr<AudioDelay>(new AudioDelay(
sampleRate_,outputChannelCount_, format_, echoDelay_, echoDecay_));
assert(delayEffect_ && mixerEffect_);
warnIfNotLowLatency(playStream_);
PrintAudioStreamInfo(playStream_);
} else {
LOGE("Failed to create playback stream. Error: %s",
oboe::convertToText(result));
}
}
/**
* Sets the stream parameters which are specific to recording,
* including the sample rate which is determined from the
* playback stream.
*
* @param builder The recording stream builder
*/
oboe::AudioStreamBuilder *EchoAudioEngine::setupRecordingStreamParameters(
oboe::AudioStreamBuilder *builder) {
// This sample uses blocking read() by setting callback to null
builder->setCallback(nullptr)
->setDeviceId(recordingDeviceId_)
->setDirection(oboe::Direction::Input)
->setSampleRate(sampleRate_)
->setChannelCount(inputChannelCount_);
return setupCommonStreamParameters(builder);
}
/**
* Sets the stream parameters which are specific to playback, including device
* id and the dataCallback function, which must be set for low latency
* playback.
* @param builder The playback stream builder
*/
oboe::AudioStreamBuilder *EchoAudioEngine::setupPlaybackStreamParameters(
oboe::AudioStreamBuilder *builder) {
builder->setCallback(this)
->setDeviceId(playbackDeviceId_)
->setDirection(oboe::Direction::Output)
->setChannelCount(outputChannelCount_)
->setSampleRate(sampleRate_)
->setFramesPerCallback(framesPerBurst_);
return setupCommonStreamParameters(builder);
}
/**
* Set the stream parameters which are common to both recording and playback
* streams.
* @param builder The playback or recording stream builder
*/
oboe::AudioStreamBuilder *EchoAudioEngine::setupCommonStreamParameters(
oboe::AudioStreamBuilder *builder) {
// We request EXCLUSIVE mode since this will give us the lowest possible
// latency.
// If EXCLUSIVE mode isn't available the builder will fall back to SHARED
// mode.
builder->setAudioApi(audioApi_)
->setFormat(format_)
->setSharingMode(oboe::SharingMode::Exclusive)
->setPerformanceMode(oboe::PerformanceMode::LowLatency);
return builder;
}
void EchoAudioEngine::startStream(oboe::AudioStream *stream) {
assert(stream);
if (stream) {
oboe::Result result = stream->requestStart();
if (result != oboe::Result::OK) {
LOGE("Error starting stream. %s", convertToText(result));
}
}
}
void EchoAudioEngine::stopStream(oboe::AudioStream *stream) {
if (stream) {
oboe::Result result = stream->start(0L);
if (result != oboe::Result::OK) {
LOGE("Error stopping stream. %s", oboe::convertToText(result));
}
}
}
/**
* Close the stream. AudioStream::close() is a blocking call so
* the application does not need to add synchronization between
* onAudioReady() function and the thread calling close().
* [the closing thread is the UI thread in this sample].
* @param stream the stream to close
*/
void EchoAudioEngine::closeStream(oboe::AudioStream *stream) {
if (stream) {
oboe::Result result = stream->close();
if (result != oboe::Result::OK) {
LOGE("Error closing stream. %s", convertToText(result));
}
}
}
/**
* Restart the streams. During the restart operation subsequent calls to this
* method will output a warning.
*/
void EchoAudioEngine::restartStreams() {
LOGI("Restarting streams");
if (restartingLock_.try_lock()) {
closeAllStreams();
openAllStreams();
restartingLock_.unlock();
} else {
LOGW(
"Restart stream operation already in progress - ignoring this request");
// We were unable to obtain the restarting lock which means the restart
// operation is currently
// active. This is probably because we received successive "stream
// disconnected" events.
// Internal issue b/63087953
}
}
/**
* Warn in logcat if non-low latency stream is created
* @param stream: newly created stream
*
*/
void EchoAudioEngine::warnIfNotLowLatency(oboe::AudioStream *stream) {
if (stream->getPerformanceMode() != oboe::PerformanceMode::LowLatency) {
LOGW(
"Stream is NOT low latency."
"Check your requested format, sample rate and channel count");
}
}
/**
* Handles playback stream's audio request. In this sample, we simply block-read
* from the record stream for the required samples.
*
* @param oboeStream: the playback stream that requesting additional samples
* @param audioData: the buffer to load audio samples for playback stream
* @param numFrames: number of frames to load to audioData buffer
* @return: DataCallbackResult::Continue.
*/
oboe::DataCallbackResult EchoAudioEngine::onAudioReady(
oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) {
assert(oboeStream == playStream_);
int32_t prevFrameRead = 0, framesRead = 0;
if (processedFrameCount_ < systemStartupFrames_) {
do {
// Drain the audio for the starting up period, half second for
// this sample.
prevFrameRead = framesRead;
oboe::ResultWithValue<int32_t> status =
recordingStream_->read(audioData, numFrames, 0);
framesRead = (!status) ? 0 : status.value();
if (framesRead == 0)
break;
} while (framesRead);
framesRead = prevFrameRead;
} else {
oboe::ResultWithValue<int32_t> status =
recordingStream_->read(audioData, numFrames, 0);
framesRead = (!status) ? 0 : status.value();
}
if (framesRead < numFrames) {
int32_t bytesPerFrame = recordingStream_->getChannelCount() *
SampleFormatToBpp(oboeStream->getFormat()) / 8;
uint8_t *padPos = static_cast<uint8_t*>(audioData) +
framesRead * bytesPerFrame;
memset(padPos, 0, (size_t)(numFrames - framesRead) * bytesPerFrame);
}
// Processing audio: padded silence audio treated as valid audio
// glitch would be felt by turning off mixer
delayEffect_->process(static_cast<int16_t *>(audioData),
outputChannelCount_, numFrames);
if (mixAudio_) {
mixerEffect_->process(static_cast<int16_t *>(audioData),
outputChannelCount_, numFrames);
}
processedFrameCount_ += numFrames;
return oboe::DataCallbackResult::Continue;
}
/**
* Oboe notifies the application for "about to close the stream".
*
* @param oboeStream: the stream to close
* @param error: oboe's reason for closing the stream
*/
void EchoAudioEngine::onErrorBeforeClose(oboe::AudioStream *oboeStream,
oboe::Result error) {
LOGE("%s stream Error before close: %s",
oboe::convertToText(oboeStream->getDirection()),
oboe::convertToText(error));
}
/**
* Oboe notifies application that "the stream is closed"
*
* @param oboeStream
* @param error
*/
void EchoAudioEngine::onErrorAfterClose(oboe::AudioStream *oboeStream,
oboe::Result error) {
LOGE("%s stream Error after close: %s",
oboe::convertToText(oboeStream->getDirection()),
oboe::convertToText(error));
}
void EchoAudioEngine::setBackgroundStream(
std::unique_ptr<int16_t[]> samples, size_t sampleCount,
int32_t sampleRate, int32_t channelCount) {
mixerEffect_->addStream(std::move(samples), sampleCount, sampleRate,
channelCount, oboe::AudioFormat::I16);
}
void EchoAudioEngine::setBackgroundMixer(float bgFactor) {
mixerEffect_->setBackgroundMixer(bgFactor);
}
/**
* Configure echo delay and decay value
* @param delay: delay in second
* @param decay: decay in second
*/
void EchoAudioEngine::setEchoControls(float delay, float decay) {
echoDelay_ = delay;
echoDecay_ = decay;
if (delayEffect_) {
delayEffect_->setDelay(echoDelay_);
delayEffect_->setDecay(echoDecay_);
}
}