* Fixes audio syncronization during video playback. * Account start_threshold as device specific latency for ALSA HDMI and tinyalsa [1] (3.5mm jack, DAC & USB) output. * Remove unneeded pcm call to query buffer size. [1]: https://android.googlesource.com/platform/external/tinyalsa_new/+/refs/tags/android-16.0.0_r1/include/tinyalsa/pcm.h#214
339 lines
14 KiB
C++
339 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2023 The Android Open Source Project
|
|
* Copyright (C) 2025 KonstaKANG
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#define LOG_TAG "AHAL_StreamPrimary"
|
|
|
|
#include <cstdio>
|
|
|
|
#include <android-base/file.h>
|
|
#include <android-base/logging.h>
|
|
#include <android-base/parseint.h>
|
|
#include <android-base/properties.h>
|
|
#include <audio_utils/clock.h>
|
|
#include <error/Result.h>
|
|
#include <error/expected_utils.h>
|
|
|
|
#include "core-impl/StreamPrimary.h"
|
|
|
|
using aidl::android::hardware::audio::common::SinkMetadata;
|
|
using aidl::android::hardware::audio::common::SourceMetadata;
|
|
using aidl::android::media::audio::common::AudioDevice;
|
|
using aidl::android::media::audio::common::AudioDeviceAddress;
|
|
using aidl::android::media::audio::common::AudioDeviceDescription;
|
|
using aidl::android::media::audio::common::AudioDeviceType;
|
|
using aidl::android::media::audio::common::AudioOffloadInfo;
|
|
using aidl::android::media::audio::common::MicrophoneInfo;
|
|
using android::base::GetBoolProperty;
|
|
using android::base::GetProperty;
|
|
using android::base::ReadFileToString;
|
|
|
|
namespace aidl::android::hardware::audio::core {
|
|
|
|
StreamPrimary::StreamPrimary(StreamContext* context, const Metadata& metadata)
|
|
: StreamAlsa(context, metadata, 3 /*readWriteRetries*/),
|
|
mIsAsynchronous(!!getContext().getAsyncCallback()),
|
|
mStubDriver(getContext()) {
|
|
context->startStreamDataProcessor();
|
|
}
|
|
|
|
::android::status_t StreamPrimary::init(DriverCallbackInterface* callback) {
|
|
RETURN_STATUS_IF_ERROR(mStubDriver.init(callback));
|
|
return StreamAlsa::init(callback);
|
|
}
|
|
|
|
::android::status_t StreamPrimary::drain(StreamDescriptor::DrainMode mode) {
|
|
return isStubStreamOnWorker() ? mStubDriver.drain(mode) : StreamAlsa::drain(mode);
|
|
}
|
|
|
|
::android::status_t StreamPrimary::flush() {
|
|
RETURN_STATUS_IF_ERROR(isStubStreamOnWorker() ? mStubDriver.flush() : StreamAlsa::flush());
|
|
// TODO(b/372951987): consider if this needs to be done from 'StreamInWorkerLogic::cycle'.
|
|
return mIsInput ? standby() : ::android::OK;
|
|
}
|
|
|
|
::android::status_t StreamPrimary::pause() {
|
|
return isStubStreamOnWorker() ? mStubDriver.pause() : StreamAlsa::pause();
|
|
}
|
|
|
|
::android::status_t StreamPrimary::standby() {
|
|
return isStubStreamOnWorker() ? mStubDriver.standby() : StreamAlsa::standby();
|
|
}
|
|
|
|
::android::status_t StreamPrimary::start() {
|
|
bool isStub = true, shutdownAlsaStream = false;
|
|
{
|
|
std::lock_guard l(mLock);
|
|
isStub = mAlsaDeviceId == kStubDeviceId;
|
|
shutdownAlsaStream =
|
|
mCurrAlsaDeviceId != mAlsaDeviceId && mCurrAlsaDeviceId != kStubDeviceId;
|
|
mCurrAlsaDeviceId = mAlsaDeviceId;
|
|
}
|
|
if (shutdownAlsaStream) {
|
|
StreamAlsa::shutdown(); // Close currently opened ALSA devices.
|
|
}
|
|
if (isStub) {
|
|
return mStubDriver.start();
|
|
}
|
|
RETURN_STATUS_IF_ERROR(StreamAlsa::start());
|
|
mStartTimeNs = ::android::uptimeNanos();
|
|
mFramesSinceStart = 0;
|
|
mSkipNextTransfer = false;
|
|
return ::android::OK;
|
|
}
|
|
|
|
::android::status_t StreamPrimary::transfer(void* buffer, size_t frameCount,
|
|
size_t* actualFrameCount, int32_t* latencyMs) {
|
|
if (isStubStreamOnWorker()) {
|
|
return mStubDriver.transfer(buffer, frameCount, actualFrameCount, latencyMs);
|
|
}
|
|
// This is a workaround for the emulator implementation which has a host-side buffer
|
|
// and is not being able to achieve real-time behavior similar to ADSPs (b/302587331).
|
|
if (!mSkipNextTransfer) {
|
|
RETURN_STATUS_IF_ERROR(
|
|
StreamAlsa::transfer(buffer, frameCount, actualFrameCount, latencyMs));
|
|
} else {
|
|
LOG(DEBUG) << __func__ << ": skipping transfer (" << frameCount << " frames)";
|
|
*actualFrameCount = frameCount;
|
|
if (mIsInput) memset(buffer, 0, frameCount * mFrameSizeBytes);
|
|
mSkipNextTransfer = false;
|
|
}
|
|
if (!mIsAsynchronous) {
|
|
const long bufferDurationUs =
|
|
(*actualFrameCount) * MICROS_PER_SECOND / mContext.getSampleRate();
|
|
const auto totalDurationUs =
|
|
(::android::uptimeNanos() - mStartTimeNs) / NANOS_PER_MICROSECOND;
|
|
mFramesSinceStart += *actualFrameCount;
|
|
const long totalOffsetUs =
|
|
mFramesSinceStart * MICROS_PER_SECOND / mContext.getSampleRate() - totalDurationUs;
|
|
LOG(VERBOSE) << __func__ << ": totalOffsetUs " << totalOffsetUs;
|
|
if (totalOffsetUs > 0) {
|
|
const long sleepTimeUs = std::min(totalOffsetUs, bufferDurationUs);
|
|
LOG(VERBOSE) << __func__ << ": sleeping for " << sleepTimeUs << " us";
|
|
usleep(sleepTimeUs);
|
|
} else {
|
|
mSkipNextTransfer = true;
|
|
}
|
|
} else {
|
|
LOG(VERBOSE) << __func__ << ": asynchronous transfer";
|
|
}
|
|
return ::android::OK;
|
|
}
|
|
|
|
::android::status_t StreamPrimary::refinePosition(StreamDescriptor::Position* position) {
|
|
RETURN_STATUS_IF_ERROR(StreamAlsa::refinePosition(position));
|
|
return ::android::OK;
|
|
}
|
|
|
|
void StreamPrimary::shutdown() {
|
|
StreamAlsa::shutdown();
|
|
mStubDriver.shutdown();
|
|
}
|
|
|
|
ndk::ScopedAStatus StreamPrimary::setConnectedDevices(const ConnectedDevices& devices) {
|
|
LOG(DEBUG) << __func__ << ": " << ::android::internal::ToString(devices);
|
|
if (devices.size() > 1) {
|
|
LOG(ERROR) << __func__ << ": primary stream can only be connected to one device, got: "
|
|
<< devices.size();
|
|
return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
|
|
}
|
|
{
|
|
const bool useStubDriver = devices.empty() || useStubStream(mIsInput, devices[0]);
|
|
std::lock_guard l(mLock);
|
|
mAlsaDeviceId = useStubDriver ? kStubDeviceId : getCardId();
|
|
}
|
|
if (!devices.empty()) {
|
|
auto streamDataProcessor = getContext().getStreamDataProcessor().lock();
|
|
if (streamDataProcessor != nullptr) {
|
|
streamDataProcessor->setAudioDevice(devices[0]);
|
|
}
|
|
}
|
|
return StreamAlsa::setConnectedDevices(devices);
|
|
}
|
|
|
|
std::vector<alsa::DeviceProfile> StreamPrimary::getDeviceProfiles() {
|
|
return {alsa::DeviceProfile{.card = mCurrAlsaDeviceId.first,
|
|
.device = mCurrAlsaDeviceId.second,
|
|
.direction = mIsInput ? PCM_IN : PCM_OUT,
|
|
.isExternal = false}};
|
|
}
|
|
|
|
bool StreamPrimary::isStubStream() {
|
|
std::lock_guard l(mLock);
|
|
return mAlsaDeviceId == kStubDeviceId;
|
|
}
|
|
|
|
// static
|
|
StreamPrimary::AlsaDeviceId StreamPrimary::getCardId() {
|
|
AlsaDeviceId cardAndDeviceId;
|
|
cardAndDeviceId.second = 0;
|
|
|
|
const std::string forceCard = GetProperty("persist.vendor.audio.pcm.card", "-1");
|
|
if (forceCard != "-1") {
|
|
int cardId = std::stoi(forceCard.c_str());
|
|
LOG(INFO) << "Forcing PCM card " << cardId;
|
|
cardAndDeviceId.first = cardId;
|
|
return cardAndDeviceId;
|
|
}
|
|
|
|
const std::string deviceName = GetProperty("persist.vendor.audio.device", "hdmi0");
|
|
std::string cardPath;
|
|
for (int i = 0; i < 8; i++) {
|
|
cardPath = "/proc/asound/card" + std::to_string(i) + "/id";
|
|
std::string cardName;
|
|
if (ReadFileToString(cardPath, &cardName)) {
|
|
if (deviceName == "jack" && cardName.starts_with("Headphones")) {
|
|
LOG(INFO) << "Using PCM card " << i << " for 3.5mm audio jack";
|
|
cardAndDeviceId.first = i;
|
|
return cardAndDeviceId;
|
|
} else if (deviceName == "dac" && !cardName.starts_with("Headphones")
|
|
&& !cardName.starts_with("vc4hdmi")) {
|
|
LOG(INFO) << "Using PCM card " << i << " for audio DAC " << cardName;
|
|
cardAndDeviceId.first = i;
|
|
return cardAndDeviceId;
|
|
}
|
|
}
|
|
}
|
|
|
|
LOG(INFO) << "Could not probe PCM card for " << deviceName << ", falling back to PCM card 0";
|
|
cardAndDeviceId.first = 0;
|
|
return cardAndDeviceId;
|
|
}
|
|
|
|
// static
|
|
StreamPrimary::AlsaDeviceId StreamPrimary::getCardAndDeviceId(
|
|
const std::vector<AudioDevice>& devices) {
|
|
if (devices.empty() || devices[0].address.getTag() != AudioDeviceAddress::id) {
|
|
return kDefaultCardAndDeviceId;
|
|
}
|
|
std::string deviceAddress = devices[0].address.get<AudioDeviceAddress::id>();
|
|
AlsaDeviceId cardAndDeviceId;
|
|
if (const size_t suffixPos = deviceAddress.rfind("CARD_");
|
|
suffixPos == std::string::npos ||
|
|
sscanf(deviceAddress.c_str() + suffixPos, "CARD_%d_DEV_%d", &cardAndDeviceId.first,
|
|
&cardAndDeviceId.second) != 2) {
|
|
return kDefaultCardAndDeviceId;
|
|
}
|
|
LOG(DEBUG) << __func__ << ": parsed with card id " << cardAndDeviceId.first << ", device id "
|
|
<< cardAndDeviceId.second;
|
|
return cardAndDeviceId;
|
|
}
|
|
|
|
// static
|
|
bool StreamPrimary::useStubStream(
|
|
bool isInput, const ::aidl::android::media::audio::common::AudioDevice& device) {
|
|
static const bool kSimulateInput =
|
|
GetBoolProperty("ro.boot.audio.tinyalsa.simulate_input", false);
|
|
static const bool kSimulateOutput =
|
|
GetBoolProperty("ro.boot.audio.tinyalsa.ignore_output", false);
|
|
if (isInput) {
|
|
return kSimulateInput || device.type.type == AudioDeviceType::IN_TELEPHONY_RX ||
|
|
device.type.type == AudioDeviceType::IN_FM_TUNER ||
|
|
device.type.connection == AudioDeviceDescription::CONNECTION_BUS /*deprecated */;
|
|
}
|
|
return kSimulateOutput || device.type.type == AudioDeviceType::OUT_TELEPHONY_TX ||
|
|
device.type.connection == AudioDeviceDescription::CONNECTION_BUS /*deprecated*/;
|
|
}
|
|
|
|
StreamInPrimary::StreamInPrimary(StreamContext&& context, const SinkMetadata& sinkMetadata,
|
|
const std::vector<MicrophoneInfo>& microphones)
|
|
: StreamIn(std::move(context), microphones),
|
|
StreamPrimary(&mContextInstance, sinkMetadata),
|
|
StreamInHwGainHelper(&mContextInstance) {}
|
|
|
|
ndk::ScopedAStatus StreamInPrimary::getHwGain(std::vector<float>* _aidl_return) {
|
|
if (isStubStream()) {
|
|
return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
|
|
}
|
|
if (mHwGains.empty()) {
|
|
float gain;
|
|
RETURN_STATUS_IF_ERROR(primary::PrimaryMixer::getInstance().getMicGain(&gain));
|
|
_aidl_return->resize(mChannelCount, gain);
|
|
RETURN_STATUS_IF_ERROR(setHwGainImpl(*_aidl_return));
|
|
}
|
|
return getHwGainImpl(_aidl_return);
|
|
}
|
|
|
|
ndk::ScopedAStatus StreamInPrimary::setHwGain(const std::vector<float>& in_channelGains) {
|
|
if (isStubStream()) {
|
|
LOG(DEBUG) << __func__ << ": gains " << ::android::internal::ToString(in_channelGains);
|
|
return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
|
|
}
|
|
auto currentGains = mHwGains;
|
|
RETURN_STATUS_IF_ERROR(setHwGainImpl(in_channelGains));
|
|
if (in_channelGains.size() < 1) {
|
|
LOG(FATAL) << __func__ << ": unexpected gain vector size: " << in_channelGains.size();
|
|
}
|
|
if (auto status = primary::PrimaryMixer::getInstance().setMicGain(in_channelGains[0]);
|
|
!status.isOk()) {
|
|
mHwGains = currentGains;
|
|
return status;
|
|
}
|
|
float gain;
|
|
RETURN_STATUS_IF_ERROR(primary::PrimaryMixer::getInstance().getMicGain(&gain));
|
|
// Due to rounding errors, round trip conversions between percents and indexed values may not
|
|
// match.
|
|
if (gain != in_channelGains[0]) {
|
|
LOG(WARNING) << __func__ << ": unmatched gain: set: " << in_channelGains[0]
|
|
<< ", from mixer: " << gain;
|
|
}
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
StreamOutPrimary::StreamOutPrimary(StreamContext&& context, const SourceMetadata& sourceMetadata,
|
|
const std::optional<AudioOffloadInfo>& offloadInfo)
|
|
: StreamOut(std::move(context), offloadInfo),
|
|
StreamPrimary(&mContextInstance, sourceMetadata),
|
|
StreamOutHwVolumeHelper(&mContextInstance) {}
|
|
|
|
ndk::ScopedAStatus StreamOutPrimary::getHwVolume(std::vector<float>* _aidl_return) {
|
|
if (isStubStream()) {
|
|
return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
|
|
}
|
|
if (mHwVolumes.empty()) {
|
|
RETURN_STATUS_IF_ERROR(primary::PrimaryMixer::getInstance().getVolumes(_aidl_return));
|
|
_aidl_return->resize(mChannelCount);
|
|
RETURN_STATUS_IF_ERROR(setHwVolumeImpl(*_aidl_return));
|
|
}
|
|
return getHwVolumeImpl(_aidl_return);
|
|
}
|
|
|
|
ndk::ScopedAStatus StreamOutPrimary::setHwVolume(const std::vector<float>& in_channelVolumes) {
|
|
if (isStubStream()) {
|
|
LOG(DEBUG) << __func__ << ": volumes " << ::android::internal::ToString(in_channelVolumes);
|
|
return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
|
|
}
|
|
auto currentVolumes = mHwVolumes;
|
|
RETURN_STATUS_IF_ERROR(setHwVolumeImpl(in_channelVolumes));
|
|
if (auto status = primary::PrimaryMixer::getInstance().setVolumes(in_channelVolumes);
|
|
!status.isOk()) {
|
|
mHwVolumes = currentVolumes;
|
|
return status;
|
|
}
|
|
std::vector<float> volumes;
|
|
RETURN_STATUS_IF_ERROR(primary::PrimaryMixer::getInstance().getVolumes(&volumes));
|
|
// Due to rounding errors, round trip conversions between percents and indexed values may not
|
|
// match.
|
|
if (volumes != in_channelVolumes) {
|
|
LOG(WARNING) << __func__ << ": unmatched volumes: set: "
|
|
<< ::android::internal::ToString(in_channelVolumes)
|
|
<< ", from mixer: " << ::android::internal::ToString(volumes);
|
|
}
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
} // namespace aidl::android::hardware::audio::core
|