portaudio: refactor to support split stream design

Allows the start/stop of individual streams as well as both input
and output simultaneously. When both streams are to be started,
full duplex mode is attempted first.

Resets device indices to -1 in the case that the host api has
changed, or if the dring audio config is scrapped somehow.

Change-Id: I1655a34d2111222b6add19f1b36b53bc4a2838ec
This commit is contained in:
Andreas Traczyk
2021-03-01 17:17:38 -05:00
parent e787de4ea5
commit 972ed1932e
2 changed files with 293 additions and 144 deletions

View File

@ -98,7 +98,7 @@ public:
virtual int getIndexRingtone() const = 0;
/**
* Determine wether or not the audio layer is active (i.e. stream opened)
* Determine whether or not the audio layer is active (i.e. playback opened)
*/
inline bool isStarted() const { return status_ == Status::Started; }
@ -106,7 +106,7 @@ public:
bool waitForStart(const std::chrono::duration<Rep, Period>& rel_time) const
{
std::unique_lock<std::mutex> lk(mutex_);
startedCv_.wait_for(lk, rel_time, [&] { return isStarted(); });
startedCv_.wait_for(lk, rel_time, [this] { return isStarted(); });
return isStarted();
}
@ -252,7 +252,7 @@ protected:
std::unique_ptr<AudioFrameResizer> playbackQueue_;
/**
* Whether or not the audio layer stream is started
* Whether or not the audio layer's playback stream is started
*/
std::atomic<Status> status_ {Status::Idle};
mutable std::condition_variable startedCv_;

View File

@ -42,16 +42,22 @@ struct PortAudioLayer::PortAudioLayerImpl
~PortAudioLayerImpl();
void init(PortAudioLayer&);
void initInput(PortAudioLayer&);
void initOutput(PortAudioLayer&);
void terminate() const;
void initStream(PortAudioLayer&);
bool initInputStream(PortAudioLayer&);
bool initOutputStream(PortAudioLayer&);
bool initFullDuplexStream(PortAudioLayer&);
std::vector<std::string> getDeviceByType(AudioDeviceType type) const;
int getIndexByType(AudioDeviceType type);
int getInternalIndexByType(const int index, AudioDeviceType type);
PaDeviceIndex indexIn_;
bool inputInitialized_ {false};
PaDeviceIndex indexOut_;
PaDeviceIndex indexRing_;
bool outputInitialized_ {false};
AudioBuffer playbackBuff_;
@ -84,7 +90,18 @@ struct PortAudioLayer::PortAudioLayerImpl
PortAudioLayer::PortAudioLayer(const AudioPreference& pref)
: AudioLayer {pref}
, pimpl_ {new PortAudioLayerImpl(*this, pref)}
{}
{
auto numDevices = Pa_GetDeviceCount();
if (numDevices < 0) {
JAMI_ERR("Pa_CountDevices returned 0x%x", numDevices);
return;
}
const PaDeviceInfo* deviceInfo;
for (auto i = 0; i < numDevices; i++) {
deviceInfo = Pa_GetDeviceInfo(i);
JAMI_DBG("PortAudio device: %d, %s", i, deviceInfo->name);
}
}
PortAudioLayer::~PortAudioLayer()
{
@ -154,43 +171,104 @@ PortAudioLayer::getIndexRingtone() const
void
PortAudioLayer::startStream(AudioDeviceType stream)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (status_ != Status::Idle)
return;
status_ = Status::Started;
}
pimpl_->initStream(*this);
auto startPlayback = [this](bool fullDuplexMode = false) -> bool {
std::unique_lock<std::mutex> lock(mutex_);
if (status_.load() != Status::Idle)
return false;
bool ret {false};
if (fullDuplexMode)
ret = pimpl_->initFullDuplexStream(*this);
else
ret = pimpl_->initOutputStream(*this);
if (ret) {
status_.store(Status::Started);
lock.unlock();
flushUrgent();
flushMain();
}
return ret;
};
flushUrgent();
flushMain();
switch (stream) {
case AudioDeviceType::ALL:
if (!startPlayback(true)) {
pimpl_->initInputStream(*this);
startPlayback();
}
break;
case AudioDeviceType::CAPTURE:
pimpl_->initInputStream(*this);
break;
case AudioDeviceType::PLAYBACK:
case AudioDeviceType::RINGTONE:
startPlayback();
break;
}
}
void
PortAudioLayer::stopStream(AudioDeviceType stream)
{
{
std::lock_guard<std::mutex> lock {mutex_};
if (status_ != Status::Started)
return;
JAMI_DBG("Stop PortAudio Streams");
for (auto& st_ptr : pimpl_->streams_) {
if (!st_ptr)
continue;
auto err = Pa_StopStream(st_ptr);
if (err != paNoError)
JAMI_ERR("Pa_StopStream error : %s", Pa_GetErrorText(err));
err = Pa_CloseStream(st_ptr);
if (err != paNoError)
JAMI_ERR("Pa_StopStream error : %s", Pa_GetErrorText(err));
auto stopPaStream = [](PaStream* stream) -> bool {
if (!stream)
return false;
auto err = Pa_StopStream(stream);
if (err != paNoError) {
JAMI_ERR("Pa_StopStream error : %s", Pa_GetErrorText(err));
return false;
}
err = Pa_CloseStream(stream);
if (err != paNoError) {
JAMI_ERR("Pa_CloseStream error : %s", Pa_GetErrorText(err));
return false;
}
return true;
};
status_ = Status::Idle;
auto stopPlayback = [this, &stopPaStream](bool fullDuplexMode = false) -> bool {
std::lock_guard<std::mutex> lock(mutex_);
if (status_.load() != Status::Started)
return false;
bool stopped = false;
if (fullDuplexMode)
stopped = stopPaStream(pimpl_->streams_[Direction::IO]);
else
stopped = stopPaStream(pimpl_->streams_[Direction::Output]);
if (stopped)
status_.store(Status::Idle);
return stopped;
};
bool stopped = false;
switch (stream) {
case AudioDeviceType::ALL:
if (pimpl_->streams_[Direction::IO]) {
stopped = stopPlayback(true);
} else {
stopped = stopPaStream(pimpl_->streams_[Direction::Input]) && stopPlayback();
}
if (stopped) {
recordChanged(false);
playbackChanged(false);
JAMI_DBG("PortAudioLayer I/O streams stopped");
} else
return;
break;
case AudioDeviceType::CAPTURE:
if (stopPaStream(pimpl_->streams_[Direction::Input])) {
recordChanged(false);
JAMI_DBG("PortAudioLayer input stream stopped");
} else
return;
break;
case AudioDeviceType::PLAYBACK:
case AudioDeviceType::RINGTONE:
if (stopPlayback()) {
playbackChanged(false);
JAMI_DBG("PortAudioLayer output stream stopped");
} else
return;
break;
}
// Flush the ring buffers
@ -234,6 +312,72 @@ PortAudioLayer::PortAudioLayerImpl::~PortAudioLayerImpl()
terminate();
}
void
PortAudioLayer::PortAudioLayerImpl::initInput(PortAudioLayer& parent)
{
auto numDevices = Pa_GetDeviceCount();
if (indexIn_ <= paNoDevice || indexIn_ >= numDevices) {
indexIn_ = Pa_GetDefaultInputDevice();
}
// Pa_GetDefaultInputDevice returned paNoDevice or we already initialized the device
if (indexIn_ == paNoDevice || inputInitialized_)
return;
if (const auto inputDeviceInfo = Pa_GetDeviceInfo(indexIn_)) {
if (inputDeviceInfo->maxInputChannels <= 0) {
indexIn_ = paNoDevice;
return initInput(parent);
}
parent.audioInputFormat_.sample_rate = inputDeviceInfo->defaultSampleRate;
parent.audioInputFormat_.nb_channels = inputDeviceInfo->maxInputChannels;
parent.hardwareInputFormatAvailable(parent.audioInputFormat_);
JAMI_DBG("PortAudioLayer initialized input: %s {%d Hz, %d channels}",
inputDeviceInfo->name,
parent.audioInputFormat_.sample_rate,
parent.audioInputFormat_.nb_channels);
inputInitialized_ = true;
} else {
JAMI_WARN("PortAudioLayer could not initialize input");
indexIn_ = paNoDevice;
inputInitialized_ = true;
}
}
void
PortAudioLayer::PortAudioLayerImpl::initOutput(PortAudioLayer& parent)
{
auto numDevices = Pa_GetDeviceCount();
if (indexOut_ <= paNoDevice || indexOut_ >= numDevices) {
indexRing_ = indexOut_ = Pa_GetDefaultOutputDevice();
} else {
indexRing_ = indexOut_;
}
// Pa_GetDefaultOutputDevice returned paNoDevice or we already initialized the device
if (indexOut_ == paNoDevice || outputInitialized_)
return;
if (const auto outputDeviceInfo = Pa_GetDeviceInfo(indexOut_)) {
if (outputDeviceInfo->maxOutputChannels <= 0) {
indexOut_ = paNoDevice;
return initOutput(parent);
}
parent.audioFormat_.sample_rate = outputDeviceInfo->defaultSampleRate;
parent.audioFormat_.nb_channels = outputDeviceInfo->maxOutputChannels;
parent.hardwareFormatAvailable(parent.audioFormat_);
JAMI_DBG("PortAudioLayer initialized output: %s {%d Hz, %d channels}",
outputDeviceInfo->name,
parent.audioFormat_.sample_rate,
parent.audioFormat_.nb_channels);
outputInitialized_ = true;
} else {
JAMI_WARN("PortAudioLayer could not initialize output");
indexOut_ = paNoDevice;
outputInitialized_ = true;
}
}
std::vector<std::string>
PortAudioLayer::PortAudioLayerImpl::getDeviceByType(AudioDeviceType type) const
{
@ -268,40 +412,14 @@ PortAudioLayer::PortAudioLayerImpl::init(PortAudioLayer& parent)
terminate();
}
auto numDevices = Pa_GetDeviceCount();
if (indexOut_ <= paNoDevice || indexOut_ >= numDevices) {
indexRing_ = indexOut_ = Pa_GetDefaultOutputDevice();
} else {
indexRing_ = indexOut_;
}
if (indexIn_ <= paNoDevice || indexIn_ >= numDevices) {
indexIn_ = Pa_GetDefaultInputDevice();
}
if (indexOut_ != paNoDevice) {
if (const auto outputDeviceInfo = Pa_GetDeviceInfo(indexOut_)) {
parent.audioFormat_.nb_channels = outputDeviceInfo->maxOutputChannels;
parent.audioFormat_.sample_rate = outputDeviceInfo->defaultSampleRate;
parent.hardwareFormatAvailable(parent.audioFormat_);
JAMI_DBG() << "PortAudioLayer initialized output using: " << outputDeviceInfo->name;
} else {
indexOut_ = paNoDevice;
}
}
if (indexIn_ != paNoDevice) {
if (const auto inputDeviceInfo = Pa_GetDeviceInfo(indexIn_)) {
parent.audioInputFormat_.nb_channels = inputDeviceInfo->maxInputChannels;
parent.audioInputFormat_.sample_rate = inputDeviceInfo->defaultSampleRate;
parent.hardwareInputFormatAvailable(parent.audioInputFormat_);
JAMI_DBG() << "PortAudioLayer initialized input using: " << inputDeviceInfo->name;
} else {
indexIn_ = paNoDevice;
}
}
initInput(parent);
initOutput(parent);
std::fill(std::begin(streams_), std::end(streams_), nullptr);
auto apiIndex = Pa_GetDefaultHostApi();
auto apiInfo = Pa_GetHostApiInfo(apiIndex);
JAMI_DBG() << "Portaudio initialized using: " << apiInfo->name;
}
int
@ -430,21 +548,16 @@ openFullDuplexStream(PaStream** stream,
JAMI_ERR("PortAudioLayer error : %s", Pa_GetErrorText(err));
}
void
PortAudioLayer::PortAudioLayerImpl::initStream(PortAudioLayer& parent)
bool
PortAudioLayer::PortAudioLayerImpl::initInputStream(PortAudioLayer& parent)
{
parent.dcblocker_.reset();
auto apiIndex = Pa_GetDefaultHostApi();
auto apiInfo = Pa_GetHostApiInfo(apiIndex);
JAMI_DBG() << "Initializing Portaudio streams using: " << apiInfo->name;
JAMI_DBG("Open PortAudio Full-duplex input/output stream");
if (indexOut_ != paNoDevice && indexIn_ != paNoDevice) {
openFullDuplexStream(
&streams_[Direction::IO],
JAMI_DBG("Open PortAudio Input Stream");
auto& stream = streams_[Direction::Input];
if (indexIn_ != paNoDevice) {
openStreamDevice(
&streams_[Direction::Input],
indexIn_,
indexOut_,
Direction::Input,
[](const void* inputBuffer,
void* outputBuffer,
unsigned long framesPerBuffer,
@ -452,76 +565,113 @@ PortAudioLayer::PortAudioLayerImpl::initStream(PortAudioLayer& parent)
PaStreamCallbackFlags statusFlags,
void* userData) -> int {
auto layer = static_cast<PortAudioLayer*>(userData);
return layer->pimpl_->paIOCallback(*layer,
static_cast<const AudioSample*>(inputBuffer),
static_cast<AudioSample*>(outputBuffer),
framesPerBuffer,
timeInfo,
statusFlags);
return layer->pimpl_->paInputCallback(*layer,
static_cast<const AudioSample*>(inputBuffer),
static_cast<AudioSample*>(outputBuffer),
framesPerBuffer,
timeInfo,
statusFlags);
},
&parent);
} else {
JAMI_DBG("Open PortAudio Output Stream");
if (indexOut_ != paNoDevice) {
openStreamDevice(
&streams_[Direction::Output],
indexOut_,
Direction::Output,
[](const void* inputBuffer,
void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData) -> int {
auto layer = static_cast<PortAudioLayer*>(userData);
return layer->pimpl_->paOutputCallback(*layer,
static_cast<const AudioSample*>(
inputBuffer),
static_cast<AudioSample*>(outputBuffer),
framesPerBuffer,
timeInfo,
statusFlags);
},
&parent);
} else {
JAMI_ERR("Error: No valid output device. There will be no sound.");
}
JAMI_DBG("Open PortAudio Input Stream");
if (indexIn_ != paNoDevice) {
openStreamDevice(
&streams_[Direction::Input],
indexIn_,
Direction::Input,
[](const void* inputBuffer,
void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData) -> int {
auto layer = static_cast<PortAudioLayer*>(userData);
return layer->pimpl_->paInputCallback(*layer,
static_cast<const AudioSample*>(
inputBuffer),
static_cast<AudioSample*>(outputBuffer),
framesPerBuffer,
timeInfo,
statusFlags);
},
&parent);
} else {
JAMI_ERR("Error: No valid input device. There will be no mic.");
}
JAMI_ERR("Error: No valid input device. There will be no mic.");
return false;
}
JAMI_DBG("Start PortAudio Streams");
for (auto& st_ptr : streams_) {
if (st_ptr) {
auto err = Pa_StartStream(st_ptr);
if (err != paNoError)
JAMI_ERR("PortAudioLayer error : %s", Pa_GetErrorText(err));
}
JAMI_DBG("Starting PortAudio Input Stream");
auto err = Pa_StartStream(stream);
if (err != paNoError) {
JAMI_ERR("PortAudioLayer error : %s", Pa_GetErrorText(err));
return false;
}
parent.recordChanged(true);
return true;
}
bool
PortAudioLayer::PortAudioLayerImpl::initOutputStream(PortAudioLayer& parent)
{
JAMI_DBG("Open PortAudio Output Stream");
auto& stream = streams_[Direction::Output];
if (indexOut_ != paNoDevice) {
openStreamDevice(
&stream,
indexOut_,
Direction::Output,
[](const void* inputBuffer,
void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData) -> int {
auto layer = static_cast<PortAudioLayer*>(userData);
return layer->pimpl_->paOutputCallback(*layer,
static_cast<const AudioSample*>(inputBuffer),
static_cast<AudioSample*>(outputBuffer),
framesPerBuffer,
timeInfo,
statusFlags);
},
&parent);
} else {
JAMI_ERR("Error: No valid output device. There will be no sound.");
return false;
}
JAMI_DBG("Starting PortAudio Output Stream");
auto err = Pa_StartStream(stream);
if (err != paNoError) {
JAMI_ERR("PortAudioLayer error : %s", Pa_GetErrorText(err));
return false;
}
parent.playbackChanged(true);
return true;
}
bool
PortAudioLayer::PortAudioLayerImpl::initFullDuplexStream(PortAudioLayer& parent)
{
if (indexOut_ == paNoDevice || indexIn_ == paNoDevice) {
JAMI_ERR("Error: Invalid input/output devices. There will be no audio.");
return false;
}
parent.dcblocker_.reset();
JAMI_DBG("Open PortAudio Full-duplex input/output stream");
auto& stream = streams_[Direction::IO];
openFullDuplexStream(
&stream,
indexIn_,
indexOut_,
[](const void* inputBuffer,
void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData) -> int {
auto layer = static_cast<PortAudioLayer*>(userData);
return layer->pimpl_->paIOCallback(*layer,
static_cast<const AudioSample*>(inputBuffer),
static_cast<AudioSample*>(outputBuffer),
framesPerBuffer,
timeInfo,
statusFlags);
},
&parent);
JAMI_DBG("Start PortAudio I/O Streams");
auto err = Pa_StartStream(stream);
if (err != paNoError) {
JAMI_ERR("PortAudioLayer error : %s", Pa_GetErrorText(err));
return false;
}
parent.recordChanged(true);
parent.playbackChanged(true);
return true;
}
int
@ -545,7 +695,6 @@ PortAudioLayer::PortAudioLayerImpl::paOutputCallback(PortAudioLayer& parent,
auto nFrames = toPlay->pointer()->nb_samples * toPlay->pointer()->channels;
std::copy_n((AudioSample*) toPlay->pointer()->extended_data[0], nFrames, outputBuffer);
return paContinue;
}