Files
pawlet_rpi4/audio/primary/StreamPrimary.cpp
T
Konsta c72ec93b0b audio: probe pcm card from property
* Valid values for the audio device property are 'jack', 'hdmi0', 'hdmi1',
  and 'dac'.
* If 'jack' is selected, first PCM card with name 'Headphones' is used.
  If 'dac' is selected, first PCM card that doesn't match the name for 3.5mm
  or HDMI devices is used. HDMI audio uses virtual ALSA devices instead of
  using tinyalsa to open a PCM card directly.
* Allow forcing specific PCM card for debugging/development purposes.
* PCM card numbers are different between Pi 4 and Pi 5 and order can change
  depending on the DAC driver that's enabled.

Pi 4:

console:/ # cat /proc/asound/cards
 0 [Headphones     ]: bcm2835_headpho - bcm2835 Headphones
                      bcm2835 Headphones
 1 [vc4hdmi0       ]: vc4-hdmi - vc4-hdmi-0
                      vc4-hdmi-0
 2 [vc4hdmi1       ]: vc4-hdmi - vc4-hdmi-1
                      vc4-hdmi-1
Pi 5 with DAC:

console:/ # cat /proc/asound/cards
 0 [vc4hdmi0       ]: vc4-hdmi - vc4-hdmi-0
                      vc4-hdmi-0
 1 [vc4hdmi1       ]: vc4-hdmi - vc4-hdmi-1
                      vc4-hdmi-1
 2 [sndrpihifiberry]: HifiberryDacp - snd_rpi_hifiberry_dacplus
                      snd_rpi_hifiberry_dacplus
2025-11-06 13:18:21 +02:00

340 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*) {
// Since not all data is actually sent to the HAL, use the position maintained by Stream class
// which accounts for all frames passed from / to the client.
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