blob: 4ab99c9e9b6ab2beb1da8510fd47f7cb67c776a1 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/media/audio/audio_core/audio_device_settings.h"
#include <fcntl.h>
#include <lib/zx/clock.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fbl/auto_lock.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/schema.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include "src/lib/files/directory.h"
#include "src/media/audio/audio_core/audio_driver.h"
#include "src/media/audio/audio_core/schema/audio_device_settings_schema.inl"
namespace media::audio {
namespace {
constexpr size_t kMaxSettingFileSize = (64 << 10);
constexpr uint32_t kAllSetGainFlags = fuchsia::media::SetAudioGainFlag_GainValid |
fuchsia::media::SetAudioGainFlag_MuteValid |
fuchsia::media::SetAudioGainFlag_AgcValid;
const std::string kSettingsPath = "/data/settings";
const std::string kDefaultSettingsPath = "/config/data/settings/default";
std::ostream& operator<<(std::ostream& stream, const rapidjson::ParseResult& result) {
return stream << "(offset " << result.Offset() << " : "
<< rapidjson::GetParseError_En(result.Code()) << ")";
}
std::ostream& operator<<(std::ostream& stream, const rapidjson::SchemaValidator::ValueType& value) {
// clang-format off
if (value.IsNull()) return stream << "<null>";
if (value.IsBool()) return stream << value.GetBool();
if (value.IsInt()) return stream << value.GetInt();
if (value.IsUint()) return stream << value.GetUint();
if (value.IsInt64()) return stream << value.GetInt64();
if (value.IsUint64()) return stream << value.GetUint64();
if (value.IsDouble()) return stream << value.GetDouble();
if (value.IsString()) return stream << "\"" << value.GetString() << "\"";
if (value.IsFloat()) return stream << value.GetFloat();
// clang-format on
if (value.IsArray()) {
bool first = true;
stream << "[";
for (auto iter = value.Begin(); iter != value.End(); ++iter) {
stream << (first ? "" : ", ") << *iter;
first = false;
}
return stream << "]";
}
if (value.IsObject()) {
bool first = true;
stream << "{ ";
for (auto iter = value.MemberBegin(); iter != value.MemberEnd(); ++iter) {
stream << (first ? "" : ", ") << iter->name << " : " << iter->value;
first = false;
}
return stream << " }";
}
return stream << "???";
}
} // namespace
bool AudioDeviceSettings::writes_enabled_ = true;
bool AudioDeviceSettings::initialized_ = false;
std::unique_ptr<rapidjson::SchemaDocument> AudioDeviceSettings::file_schema_;
AudioDeviceSettings::AudioDeviceSettings(const AudioDriver& drv, bool is_input)
: uid_(drv.persistent_unique_id()),
is_input_(is_input),
can_mute_(drv.hw_gain_state().can_mute),
can_agc_(drv.hw_gain_state().can_agc) {
const auto& hw = drv.hw_gain_state();
gain_state_.gain_db = hw.cur_gain;
gain_state_.muted = hw.can_mute && hw.cur_mute;
gain_state_.agc_enabled = hw.can_agc && hw.cur_agc;
}
// static
void AudioDeviceSettings::Initialize() {
FXL_DCHECK(!initialized_);
if (!writes_enabled_) {
FXL_LOG(WARNING) << "Device settings persistence is disabled; so device settings files will be "
"neither created nor updated.";
}
if (!files::CreateDirectory(kSettingsPath)) {
FXL_LOG(ERROR) << "Failed to ensure that \"" << kSettingsPath
<< "\" exists! Settings will neither be persisted nor restored.";
return;
}
rapidjson::Document schema_doc;
rapidjson::ParseResult parse_res = schema_doc.Parse(kAudioDeviceSettingsSchema.c_str());
if (parse_res.IsError()) {
FXL_LOG(ERROR) << "Failed to parse settings file JSON schema " << parse_res
<< "! Settings will neither be persisted nor restored.";
return;
}
initialized_ = true;
file_schema_ = std::make_unique<rapidjson::SchemaDocument>(schema_doc);
}
zx_status_t AudioDeviceSettings::InitFromDisk() {
// Don't bother to do any of this unless we were able to successfully
// initialize our storage subsystem.
if (!initialized_) {
Initialize();
if (!initialized_) {
return ZX_ERR_BAD_STATE;
}
}
static const struct {
const std::string& prefix;
bool is_default;
} kConfigSources[2] = {
{.prefix = kSettingsPath, .is_default = false},
{.prefix = kDefaultSettingsPath, .is_default = true},
};
FXL_DCHECK(static_cast<bool>(storage_) == false);
for (const auto& cfg_src : kConfigSources) {
// Start by attempting to open a pre-existing file which has our settings in
// it. If we cannot find such a file, or if the file exists but is invalid,
// simply create a new file and write out our current settings.
char path[256];
fbl::unique_fd storage;
CreateSettingsPath(cfg_src.prefix, path, sizeof(path));
storage.reset(open(path, cfg_src.is_default ? O_RDONLY : O_RDWR));
if (static_cast<bool>(storage)) {
zx_status_t res = Deserialize(storage);
if (res == ZX_OK) {
if (cfg_src.is_default) {
// If we just loaded and deserialized the fallback default config,
// break out of the loop and fall thru to serialization code.
break;
}
// We successfully loaded our persisted settings. Cancel our commit
// timer, hold onto the FD, and we should be good to go.
CancelCommitTimeouts();
storage_ = std::move(storage);
return ZX_OK;
} else {
storage_.reset();
if (!cfg_src.is_default) {
FXL_PLOG(INFO, res) << "Failed to read device settings at \"" << path
<< "\". Re-creating file from defaults";
unlink(path);
} else {
FXL_PLOG(INFO, res) << "Could not load default audio settings file \"" << path << "\"";
}
}
}
}
// If persisting of device settings is disabled, don't create a new file.
if (!writes_enabled_) {
return ZX_OK;
}
// We failed to load persisted settings for one reason or another.
// Create a new settings file for this device; persist our defaults there.
char path[256];
CreateSettingsPath(kSettingsPath, path, sizeof(path));
FXL_DCHECK(static_cast<bool>(storage_) == false);
storage_.reset(open(path, O_RDWR | O_CREAT));
if (!static_cast<bool>(storage_)) {
// TODO(mpuryear): define and enforce a limit for the number of settings
// files allowed to be created.
FXL_LOG(WARNING) << "Failed to create new audio settings file \"" << path << "\" (err " << errno
<< "). Settings for this device will not be persisted.";
return ZX_ERR_IO;
}
zx_status_t res = Serialize();
if (res != ZX_OK) {
FXL_PLOG(WARNING, res) << "Failed to write new settings file at \"" << path
<< "\". Settings for this device will not be persisted";
storage_.reset();
unlink(path);
}
return res;
}
void AudioDeviceSettings::InitFromClone(const AudioDeviceSettings& other) {
FXL_DCHECK(memcmp(&uid_, &other.uid_, sizeof(uid_)) == 0);
// Clone the gain settings.
fuchsia::media::AudioGainInfo gain_info;
other.GetGainInfo(&gain_info);
SetGainInfo(gain_info, kAllSetGainFlags);
// Clone misc. flags.
ignore_device_ = other.ignore_device();
disallow_auto_routing_ = other.disallow_auto_routing();
}
bool AudioDeviceSettings::SetGainInfo(const fuchsia::media::AudioGainInfo& req,
uint32_t set_flags) {
fbl::AutoLock lock(&settings_lock_);
audio_set_gain_flags_t dirtied = gain_state_dirty_flags_;
namespace fm = ::fuchsia::media;
if ((set_flags & fm::SetAudioGainFlag_GainValid) && (gain_state_.gain_db != req.gain_db)) {
gain_state_.gain_db = req.gain_db;
dirtied = static_cast<audio_set_gain_flags_t>(dirtied | AUDIO_SGF_GAIN_VALID);
}
bool mute_tgt = (req.flags & fm::AudioGainInfoFlag_Mute) != 0;
if ((set_flags & fm::SetAudioGainFlag_MuteValid) && (gain_state_.muted != mute_tgt)) {
gain_state_.muted = mute_tgt;
dirtied = static_cast<audio_set_gain_flags_t>(dirtied | AUDIO_SGF_MUTE_VALID);
}
bool agc_tgt = (req.flags & fm::AudioGainInfoFlag_AgcEnabled) != 0;
if ((set_flags & fm::SetAudioGainFlag_AgcValid) && (gain_state_.agc_enabled != agc_tgt)) {
gain_state_.agc_enabled = agc_tgt;
dirtied = static_cast<audio_set_gain_flags_t>(dirtied | AUDIO_SGF_AGC_VALID);
}
bool needs_wake = (!gain_state_dirty_flags_ && dirtied && writes_enabled_);
gain_state_dirty_flags_ = dirtied;
if (needs_wake) {
UpdateCommitTimeouts();
}
return needs_wake;
}
zx::time AudioDeviceSettings::Commit(bool force) {
// If 1) we aren't backed by storage, or 2) device settings persistence is
// disabled, or 3) the cache is clean, then there is nothing to commit.
if (!static_cast<bool>(storage_ || !writes_enabled_) ||
(next_commit_time_ == zx::time::infinite())) {
return zx::time::infinite();
}
zx::time now;
zx::clock::get(&now);
if (force || (now >= next_commit_time_)) {
Serialize();
}
return next_commit_time_;
}
void AudioDeviceSettings::GetGainInfo(fuchsia::media::AudioGainInfo* out_info) const {
FXL_DCHECK(out_info != nullptr);
// TODO(johngro): consider eliminating the acquisition of this lock. In
// theory, the only mutation of gain state happens during SetGainInfo, which
// is supposed to only be called from the AudioDeviceManager, which should be
// functionally single threaded as it is called only from the main service
// message loop. Since GetGainInfo should only be called from the device
// manager as well, we should not need the settings_lock_ to observe the
// gain state from this method.
//
// Conversely, if we had an efficient reader/writer lock, we should only need
// to obtain this lock for read which should always succeed without
// contention.
fbl::AutoLock lock(&settings_lock_);
out_info->gain_db = gain_state_.gain_db;
out_info->flags = 0;
if (can_mute_ && gain_state_.muted) {
out_info->flags |= fuchsia::media::AudioGainInfoFlag_Mute;
}
if (can_agc_) {
out_info->flags |= fuchsia::media::AudioGainInfoFlag_AgcSupported;
if (gain_state_.agc_enabled) {
out_info->flags |= fuchsia::media::AudioGainInfoFlag_AgcEnabled;
}
}
}
audio_set_gain_flags_t AudioDeviceSettings::SnapshotGainState(GainState* out_state) {
FXL_DCHECK(out_state != nullptr);
audio_set_gain_flags_t ret;
{
fbl::AutoLock lock(&settings_lock_);
*out_state = gain_state_;
ret = gain_state_dirty_flags_;
gain_state_dirty_flags_ = static_cast<audio_set_gain_flags_t>(0);
}
return ret;
}
zx_status_t AudioDeviceSettings::Deserialize(const fbl::unique_fd& storage) {
FXL_DCHECK(static_cast<bool>(storage));
if (!writes_enabled_) {
FXL_LOG(INFO) << "Reading device settings (but writes are disabled).";
}
// Figure out the size of the file, then allocate storage for reading the
// whole thing.
off_t file_size = lseek(storage.get(), 0, SEEK_END);
if ((file_size <= 0) || (static_cast<size_t>(file_size) > kMaxSettingFileSize)) {
return ZX_ERR_BAD_STATE;
}
if (lseek(storage.get(), 0, SEEK_SET) != 0) {
return ZX_ERR_IO;
}
// Allocate the buffer and read in the contents.
auto buffer = std::make_unique<char[]>(file_size + 1);
if (read(storage.get(), buffer.get(), file_size) != file_size) {
return ZX_ERR_IO;
}
buffer[file_size] = 0;
// Parse the contents
rapidjson::Document doc;
rapidjson::ParseResult parse_res = doc.ParseInsitu(buffer.get());
if (parse_res.IsError()) {
FXL_LOG(WARNING) << "Parse error " << parse_res << " when reading persisted audio settings.";
return ZX_ERR_IO_DATA_INTEGRITY;
}
// Validate that the document conforms to our schema
FXL_DCHECK(file_schema_ != nullptr);
rapidjson::SchemaValidator validator(*file_schema_);
if (!doc.Accept(validator)) {
FXL_LOG(WARNING) << "Schema validation error when reading persisted audio settings.";
FXL_LOG(WARNING) << "Error: " << validator.GetError();
return ZX_ERR_IO_DATA_INTEGRITY;
}
// Extract the gain information
fuchsia::media::AudioGainInfo gain_info;
const auto& gain_obj = doc["gain"].GetObject();
gain_info.gain_db = static_cast<float>(gain_obj["gain_db"].GetDouble());
if (gain_obj["mute"].GetBool()) {
gain_info.flags |= fuchsia::media::AudioGainInfoFlag_Mute;
}
if (gain_obj["agc"].GetBool()) {
gain_info.flags |= fuchsia::media::AudioGainInfoFlag_AgcEnabled;
}
// Apply gain settings.
SetGainInfo(gain_info, kAllSetGainFlags);
// Extract misc. flags.
ignore_device_ = doc["ignore_device"].GetBool();
disallow_auto_routing_ = doc["disallow_auto_routing"].GetBool();
// Success!
return ZX_OK;
}
zx_status_t AudioDeviceSettings::Serialize() {
CancelCommitTimeouts();
if (!writes_enabled_) {
FXL_LOG(DFATAL) << "Device settings files disabled; not writing to file.";
return ZX_ERR_BAD_STATE;
}
if (!static_cast<bool>(storage_)) {
return ZX_ERR_NOT_FOUND;
}
// Serialize our state into a string buffer.
rapidjson::StringBuffer buffer(nullptr, 4096);
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
fuchsia::media::AudioGainInfo gain_info;
GetGainInfo(&gain_info);
writer.StartObject();
writer.Key("gain");
writer.StartObject();
writer.Key("gain_db");
writer.Double(gain_info.gain_db);
writer.Key("mute");
writer.Bool(gain_info.flags & fuchsia::media::AudioGainInfoFlag_Mute);
writer.Key("agc");
writer.Bool((gain_info.flags & fuchsia::media::AudioGainInfoFlag_AgcEnabled) &&
(gain_info.flags & fuchsia::media::AudioGainInfoFlag_AgcSupported));
writer.EndObject(); // end "gain" object
writer.Key("ignore_device");
writer.Bool(ignore_device());
writer.Key("disallow_auto_routing");
writer.Bool(disallow_auto_routing());
writer.EndObject(); // end top level object
// Truncate the file down to nothing, write the data, then flush the file.
//
// TODO(johngro): We should really double buffer these settings files in case
// of power loss. Even better would be to have a fuchsia service which
// manages storing and updating settings in a transactional and reliable
// fashion along with other features like rate limiting of updates.
const char* data = buffer.GetString();
const size_t sz = buffer.GetSize();
if ((lseek(storage_.get(), 0, SEEK_SET) != 0) || (ftruncate(storage_.get(), 0) != 0) ||
(write(storage_.get(), data, sz) != static_cast<ssize_t>(sz)) ||
(fsync(storage_.get()) != 0)) {
return ZX_ERR_INTERNAL;
}
return ZX_OK;
}
void AudioDeviceSettings::UpdateCommitTimeouts() {
if (!writes_enabled_) {
FXL_LOG(DFATAL) << "Device settings files disabled; we should not be here.";
}
constexpr zx::duration kMaxUpdateDelay(ZX_SEC(5));
constexpr zx::duration kUpdateDelay(ZX_MSEC(500));
zx::time now;
zx::clock::get(&now);
if (max_commit_time_ == zx::time::infinite()) {
max_commit_time_ = now + kMaxUpdateDelay;
}
next_commit_time_ = std::min(now + kUpdateDelay, max_commit_time_);
}
void AudioDeviceSettings::CancelCommitTimeouts() {
next_commit_time_ = zx::time::infinite();
max_commit_time_ = zx::time::infinite();
}
void AudioDeviceSettings::CreateSettingsPath(const std::string& prefix, char* out_path,
size_t out_path_len) {
const uint8_t* x = uid_.data;
snprintf(out_path, out_path_len,
"%s/%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x-%s.json",
prefix.c_str(), x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11],
x[12], x[13], x[14], x[15], is_input_ ? "input" : "output");
}
} // namespace media::audio