chromium/chromecast/media/cma/backend/volume_control.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/public/volume_control.h"

#include <algorithm>
#include <cmath>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_pump_type.h"
#include "base/no_destructor.h"
#include "base/synchronization/lock.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromecast/media/audio/mixer_service/control_connection.h"
#include "chromecast/media/cma/backend/audio_buildflags.h"
#include "chromecast/media/cma/backend/saved_volumes.h"
#include "chromecast/media/cma/backend/system_volume_control.h"
#include "chromecast/media/cma/backend/volume_map.h"

#if BUILDFLAG(MIXER_IN_CAST_SHELL)
#include "chromecast/media/cma/backend/mixer/stream_mixer.h"  // nogncheck
#endif

namespace chromecast {
namespace media {

namespace {

#if !BUILDFLAG(SYSTEM_OWNS_VOLUME)
constexpr float kMinDbFS = -120.0f;
#endif

constexpr char kKeyMediaDbFS[] = "dbfs.media";
constexpr char kKeyAlarmDbFS[] = "dbfs.alarm";
constexpr char kKeyCommunicationDbFS[] = "dbfs.communication";

#if !BUILDFLAG(SYSTEM_OWNS_VOLUME)
float DbFsToScale(float db) {
  if (db <= kMinDbFS) {
    return 0.0f;
  }
  return std::pow(10, db / 20);
}
#endif

std::string ContentTypeToDbFSPath(AudioContentType type) {
  switch (type) {
    case AudioContentType::kAlarm:
      return kKeyAlarmDbFS;
    case AudioContentType::kCommunication:
      return kKeyCommunicationDbFS;
    default:
      return kKeyMediaDbFS;
  }
}

class VolumeControlInternal : public SystemVolumeControl::Delegate {
 public:
  VolumeControlInternal()
      : thread_("VolumeControl"),
        initialize_complete_event_(
            base::WaitableEvent::ResetPolicy::MANUAL,
            base::WaitableEvent::InitialState::NOT_SIGNALED) {
    // Load volume map to check that the config file is correct.
    VolumeControl::VolumeToDbFS(0.0f);

    storage_path_ = base::GetHomeDir().Append("saved_volumes");
    base::flat_map<AudioContentType, double> saved_volumes =
        LoadSavedVolumes(storage_path_);
    for (auto type : {AudioContentType::kMedia, AudioContentType::kAlarm,
                      AudioContentType::kCommunication}) {
      stored_values_.SetByDottedPath(ContentTypeToDbFSPath(type),
                                     saved_volumes[type]);
    }

    base::Thread::Options options;
    options.message_pump_type = base::MessagePumpType::IO;
    thread_.StartWithOptions(std::move(options));

    thread_.task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&VolumeControlInternal::InitializeOnThread,
                                  base::Unretained(this)));
    initialize_complete_event_.Wait();
  }

  VolumeControlInternal(const VolumeControlInternal&) = delete;
  VolumeControlInternal& operator=(const VolumeControlInternal&) = delete;

  ~VolumeControlInternal() override = default;

  void AddVolumeObserver(VolumeObserver* observer) {
    base::AutoLock lock(observer_lock_);
    volume_observers_.push_back(observer);
  }

  void RemoveVolumeObserver(VolumeObserver* observer) {
    base::AutoLock lock(observer_lock_);
    volume_observers_.erase(std::remove(volume_observers_.begin(),
                                        volume_observers_.end(), observer),
                            volume_observers_.end());
  }

  float GetVolume(AudioContentType type) {
    base::AutoLock lock(volume_lock_);
    return volumes_[type];
  }

  void SetVolume(VolumeChangeSource source,
                 AudioContentType type,
                 float level) {
    if (type == AudioContentType::kOther) {
      DLOG(ERROR) << "Can't set volume for content type kOther";
      return;
    }

    level = std::clamp(level, 0.0f, 1.0f);
    thread_.task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&VolumeControlInternal::SetVolumeOnThread,
                                  base::Unretained(this), source, type, level,
                                  false /* from_system */));
  }

  void SetVolumeMultiplier(AudioContentType type, float multiplier) {
    if (type == AudioContentType::kOther) {
      DLOG(ERROR) << "Can't set volume multiplier for content type kOther";
      return;
    }

#if BUILDFLAG(SYSTEM_OWNS_VOLUME)
    LOG(INFO) << "Ignore global volume multiplier since volume is externally "
              << "controlled";
#else
    thread_.task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&VolumeControlInternal::SetVolumeMultiplierOnThread,
                       base::Unretained(this), type, multiplier));
#endif
  }

  bool IsMuted(AudioContentType type) {
    base::AutoLock lock(volume_lock_);
    return muted_[type];
  }

  void SetMuted(VolumeChangeSource source, AudioContentType type, bool muted) {
    if (type == AudioContentType::kOther) {
      DLOG(ERROR) << "Can't set mute state for content type kOther";
      return;
    }

    thread_.task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&VolumeControlInternal::SetMutedOnThread,
                                  base::Unretained(this), source, type, muted,
                                  false /* from_system */));
  }

  void SetOutputLimit(AudioContentType type, float limit) {
    if (type == AudioContentType::kOther) {
      DLOG(ERROR) << "Can't set output limit for content type kOther";
      return;
    }

    thread_.task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&VolumeControlInternal::SetOutputLimitOnThread,
                       base::Unretained(this), type, limit));
  }

  void SetPowerSaveMode(bool power_save_on) {
    thread_.task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&VolumeControlInternal::SetPowerSaveModeOnThread,
                       base::Unretained(this), power_save_on));
  }

 private:
  void InitializeOnThread() {
    DCHECK(thread_.task_runner()->BelongsToCurrentThread());
    system_volume_control_ = SystemVolumeControl::Create(this);
    mixer_ = std::make_unique<mixer_service::ControlConnection>();
    mixer_->Connect();

    saved_volumes_writer_ = std::make_unique<base::ImportantFileWriter>(
        storage_path_, thread_.task_runner(), base::Seconds(1));

    for (auto type : {AudioContentType::kMedia, AudioContentType::kAlarm,
                      AudioContentType::kCommunication}) {
      std::optional<double> dbfs =
          stored_values_.FindDouble(ContentTypeToDbFSPath(type));
      CHECK(dbfs);
      volumes_[type] = VolumeControl::DbFSToVolume(*dbfs);
      volume_multipliers_[type] = 1.0f;

#if BUILDFLAG(SYSTEM_OWNS_VOLUME)
      // ALSA owns volume; our internal mixer should not apply any scaling
      // multiplier.
      mixer_->SetVolume(type, 1.0f);
#else
      mixer_->SetVolume(type, DbFsToScale(*dbfs));
#endif

      // Note that mute state is not persisted across reboots.
      muted_[type] = false;
      mixer_->SetMuted(type, false);
    }

#if BUILDFLAG(SYSTEM_OWNS_VOLUME)
    // Read the current volume and mute state from the ALSA mixer element(s).
    volumes_[AudioContentType::kMedia] = system_volume_control_->GetVolume();
    muted_[AudioContentType::kMedia] = system_volume_control_->IsMuted();
#else
    // Make sure the ALSA mixer element correctly reflects the current volume
    // state.
    system_volume_control_->SetVolume(volumes_[AudioContentType::kMedia]);
    system_volume_control_->SetMuted(false);
#endif

    volumes_[AudioContentType::kOther] = 1.0;
    muted_[AudioContentType::kOther] = false;

    initialize_complete_event_.Signal();
  }

  void SetVolumeOnThread(VolumeChangeSource source,
                         AudioContentType type,
                         float level,
                         bool from_system) {
    DCHECK(thread_.task_runner()->BelongsToCurrentThread());
    DCHECK_NE(AudioContentType::kOther, type);
    DCHECK(!from_system || type == AudioContentType::kMedia);
    DCHECK(base::Contains(volume_multipliers_, type));

    {
      base::AutoLock lock(volume_lock_);
      if (from_system && system_volume_control_->GetRoundtripVolume(
                             volumes_[AudioContentType::kMedia]) == level) {
        return;
      }
      if (level == volumes_[type]) {
        return;
      }
      volumes_[type] = level;
    }

    float dbfs = VolumeControl::VolumeToDbFS(level);
#if !BUILDFLAG(SYSTEM_OWNS_VOLUME)
    mixer_->SetVolume(type, DbFsToScale(dbfs) * volume_multipliers_[type]);
#endif

    if (!from_system && type == AudioContentType::kMedia) {
      system_volume_control_->SetVolume(level);
    }

    {
      base::AutoLock lock(observer_lock_);
      for (VolumeObserver* observer : volume_observers_) {
        observer->OnVolumeChange(source, type, level);
      }
    }

    stored_values_.SetByDottedPath(ContentTypeToDbFSPath(type), dbfs);
    std::string output_js;
    base::JSONWriter::Write(stored_values_, &output_js);
    saved_volumes_writer_->WriteNow(std::move(output_js));
  }

  void SetVolumeMultiplierOnThread(AudioContentType type, float multiplier) {
    DCHECK(thread_.task_runner()->BelongsToCurrentThread());
    DCHECK_NE(AudioContentType::kOther, type);
#if BUILDFLAG(SYSTEM_OWNS_VOLUME)
    NOTREACHED_IN_MIGRATION();
#else
    volume_multipliers_[type] = multiplier;
    float scale =
        DbFsToScale(VolumeControl::VolumeToDbFS(volumes_[type])) * multiplier;
    mixer_->SetVolume(type, scale);
#endif
  }

  void SetMutedOnThread(VolumeChangeSource source,
                        AudioContentType type,
                        bool muted,
                        bool from_system) {
    DCHECK(thread_.task_runner()->BelongsToCurrentThread());
    DCHECK_NE(AudioContentType::kOther, type);

    {
      base::AutoLock lock(volume_lock_);
      if (muted == muted_[type]) {
        return;
      }
      muted_[type] = muted;
    }

#if !BUILDFLAG(SYSTEM_OWNS_VOLUME)
    mixer_->SetMuted(type, muted);
#endif

    if (!from_system && type == AudioContentType::kMedia) {
      system_volume_control_->SetMuted(muted);
    }

    {
      base::AutoLock lock(observer_lock_);
      for (VolumeObserver* observer : volume_observers_) {
        observer->OnMuteChange(source, type, muted);
      }
    }
  }

  void SetOutputLimitOnThread(AudioContentType type, float limit) {
    if (type == AudioContentType::kOther) {
      DLOG(ERROR) << "Can't set output limit for content type kOther";
      return;
    }

#if !BUILDFLAG(SYSTEM_OWNS_VOLUME)
    limit = std::clamp(limit, 0.0f, 1.0f);
    mixer_->SetVolumeLimit(type,
                           DbFsToScale(VolumeControl::VolumeToDbFS(limit)));

    if (type == AudioContentType::kMedia) {
      system_volume_control_->SetLimit(limit);
    }
#endif
  }

  void SetPowerSaveModeOnThread(bool power_save_on) {
    DCHECK(thread_.task_runner()->BelongsToCurrentThread());
    system_volume_control_->SetPowerSave(power_save_on);
  }

  // SystemVolumeControl::Delegate implementation:
  void OnSystemVolumeOrMuteChange(float new_volume, bool new_mute) override {
    LOG(INFO) << "System volume/mute change, new volume = " << new_volume
              << ", new mute = " << new_mute;
    DCHECK(thread_.task_runner()->BelongsToCurrentThread());
    SetVolumeOnThread(VolumeChangeSource::kUser, AudioContentType::kMedia,
                      new_volume, true /* from_system */);
    SetMutedOnThread(VolumeChangeSource::kUser, AudioContentType::kMedia,
                     new_mute, true /* from_system */);
  }

  base::FilePath storage_path_;
  base::Value::Dict stored_values_;

  base::Lock volume_lock_;
  base::flat_map<AudioContentType, float> volumes_;
  base::flat_map<AudioContentType, float> volume_multipliers_;
  base::flat_map<AudioContentType, bool> muted_;

  base::Lock observer_lock_;
  std::vector<VolumeObserver*> volume_observers_;

  base::Thread thread_;
  base::WaitableEvent initialize_complete_event_;

  std::unique_ptr<SystemVolumeControl> system_volume_control_;
  std::unique_ptr<mixer_service::ControlConnection> mixer_;
  std::unique_ptr<base::ImportantFileWriter> saved_volumes_writer_;
};

VolumeControlInternal& GetVolumeControl() {
  static base::NoDestructor<VolumeControlInternal> g_volume_control;
  return *g_volume_control;
}

}  // namespace

// static
void VolumeControl::Initialize(const std::vector<std::string>& argv) {
#if BUILDFLAG(MIXER_IN_CAST_SHELL)
  static base::NoDestructor<StreamMixer> g_mixer;
#endif
  GetVolumeControl();
}

// static
void VolumeControl::Finalize() {
  // Nothing to do.
}

// static
void VolumeControl::AddVolumeObserver(VolumeObserver* observer) {
  GetVolumeControl().AddVolumeObserver(observer);
}

// static
void VolumeControl::RemoveVolumeObserver(VolumeObserver* observer) {
  GetVolumeControl().RemoveVolumeObserver(observer);
}

// static
float VolumeControl::GetVolume(AudioContentType type) {
  return GetVolumeControl().GetVolume(type);
}

// static
void VolumeControl::SetVolume(VolumeChangeSource source,
                              AudioContentType type,
                              float level) {
  GetVolumeControl().SetVolume(source, type, level);
}

// static
void VolumeControl::SetVolumeMultiplier(AudioContentType type,
                                        float multiplier) {
  GetVolumeControl().SetVolumeMultiplier(type, multiplier);
}

// static
bool VolumeControl::IsMuted(AudioContentType type) {
  return GetVolumeControl().IsMuted(type);
}

// static
void VolumeControl::SetMuted(VolumeChangeSource source,
                             AudioContentType type,
                             bool muted) {
  GetVolumeControl().SetMuted(source, type, muted);
}

// static
void VolumeControl::SetOutputLimit(AudioContentType type, float limit) {
  GetVolumeControl().SetOutputLimit(type, limit);
}

// static
void VolumeControl::SetPowerSaveMode(bool power_save_on) {
  GetVolumeControl().SetPowerSaveMode(power_save_on);
}

}  // namespace media
}  // namespace chromecast